目录
数字三角形模型
摘花生
最低通行费
方格取数(洛谷)
传纸条(洛谷)
最长上升子序列模型
最长上升子序列(洛谷)&最长递增子序列(leetcode)
leetcode674. 最长连续递增序列
leetcode718. 最长重复子数组
怪盗基德的帽子
登山
合唱队形(洛谷)
友好城市(洛谷)
最大上升子序列和
背包问题分析:
01背包:
Leetcode 416. 分割等和子集
Leetcode 1049. 最后一块石头的重量 II
Leetcode 494. 目标和
Leetcode 474.一和零
完全背包:
Leetcde 518. 零钱兑换 II
Leetcode 377.组合总数Ⅳ
leetcode 322.零钱兑换
leetcode279.完全平方数
leetcode129.单词拆分
多重背包:
多重背包看做01背包
多重背包二进制优化
单调队列优化
状态机
leetcode089.打家劫舍
leetcode买股票的最佳时机4
leetcode121. 买卖股票的最佳时机
leetcode122. 买卖股票的最佳时机 2
leetcode123. 买卖股票的最佳时机 3
leetcode买卖股票的最佳时机含冷冻期
leetcode买卖股票的最佳时机含手续费
树形dp
leetcode 打家劫舍3
leetcode二叉树的直径
leetcode二叉树中的最大路径和
持续更新
数字三角形模型
摘花生
#include<iostream>
using namespace std;
const int N=110;
int r,c;
int T;
int w[N][N],res[N][N];
int main(){
scanf("%d",&T);
while(T--){
scanf("%d%d",&r,&c);
for(int i=1;i<=r;i++)
for(int j=1;j<=c;j++)
scanf("%d",&w[i][j]);
for(int i=1;i<=r;i++)
for(int j=1;j<=c;j++)
res[i][j]=max(res[i-1][j]+w[i][j],res[i][j-1]+w[i][j]);
cout<<res[r][c]<<endl;
}
return 0;
};
最低通行费
注意min的问题一定要考虑边界问题
#include<iostream>
#define INF 1e9;
using namespace std;
const int N=110;
int r;
int w[N][N],res[N][N];
int main(){
scanf("%d",&r);
for(int i=1;i<=r;i++)
for(int j=1;j<=r;j++)
scanf("%d",&w[i][j]);
for(int i=1;i<=r;i++){
for(int j=1;j<=r;j++){
if(i==1&&j==1) res[i][j]=w[i][j];//不能直接写res[1][1]=w[1][1],因为for循环里面有res[1][1]=INF;
else{
res[i][j]=INF;//
if(i>1) res[i][j]=min(res[i][j],res[i-1][j]+w[i][j]);// 只有不在第一行的时候,才可以从上面过来
if(j>1) res[i][j]=min(res[i][j],res[i][j-1]+w[i][j]);// 只有不在第一列的时候,才可以从左面过来
}
}
}
cout<<res[r][r]<<endl;
return 0;
};
方格取数(洛谷)
[NOIP2000 提高组] 方格取数 - 洛谷
#include<iostream>
using namespace std;
const int N=15;
int n;
int w[N][N];
int f[N*2][N][N];
int main(){
scanf("%d",&n);
int x,y,r;
while(1){
scanf("%d%d%d",&x,&y,&r);
if(x==0&&y==0&r==0) break;
w[x][y]=r;
}
for(int k=2;k<=n+n;k++){
for(int i1=1;i1<=n;i1++){
for(int i2=1;i2<=n;i2++){
int j1=k-i1,j2=k-i2;
if(j1>=1&&j1<=n&&j2>=1&&j2<=n){
int t=w[i1][j1];
if(i1!=i2) t+=w[i2][j2];
int &x = f[k][i1][i2];
x = max(x, f[k - 1][i1 - 1][i2 - 1] + t);
x = max(x, f[k - 1][i1 - 1][i2] + t);
x = max(x, f[k - 1][i1][i2 - 1] + t);
x = max(x, f[k - 1][i1][i2] + t);
}
}
}
}
printf("%d\n", f[n + n][n][n]);
return 0;
};
传纸条(洛谷)
[NOIP2008 提高组] 传纸条 - 洛谷
注意:此题是正方形,所以i1,i2的限制只有1<=i1,i2<=n。下面一道题变成长方形(m*n)的时候,就会有限制:
同理,i2范围与i1一致
#include <iostream>
using namespace std;
const int N=60;
int f[N*2][N][N];
int w[N][N];
int m,n;
int main(){
scanf("%d%d",&m,&n);
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
scanf("%d",&w[i][j]);
for(int k=2;k<=m+n;k++){
for(int i1=max(1,k-n);i1<=min(k-1,m);i1++){
for(int i2=max(1,k-n);i2<=min(k-1,m);i2++){
int j1=k-i1,j2=k-i2;
int t=w[i1][j1];
if(i1!=i2) t+=w[i2][j2];
int &x=f[k][i1][i2];
x=max(x,f[k-1][i1-1][i2]+t);
x=max(x,f[k-1][i1][i2-1]+t);
x=max(x,f[k-1][i1][i2]+t);
x=max(x,f[k-1][i1-1][i2-1]+t);
}
}
}
cout<<f[m+n][m][m];//注意两个路线横坐标都是n
return 0;
}
最长上升子序列模型
最长上升子序列(洛谷)&最长递增子序列(leetcode)
最长上升子序列 - 洛谷
300.最长递增子序列
dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度
状态转移方程:
位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。
所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
洛谷题解:
#include <iostream>
using namespace std;
const int N=5050;
int f[N];
int a[N];
int m,n;
int main(){
cin>>n;
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++){
f[i]=1;
for(int j=1;j<i;j++){
if(a[j]<a[i]){
f[i]=max(f[i],f[j]+1);
}
}
}
int res=0;
for(int i=1;i<=n;i++) res=max(res,f[i]);
cout<<res;
return 0;
}
leetcode题解:
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size();
vector<int> dp(n+1,0);
for(int i=0;i<n;i++){
dp[i]=1;
for(int j=0;j<i;j++){
if(nums[i]>nums[j]){
dp[i]=max(dp[i],dp[j]+1);
}
}
}
int res=0;
for(int i=0;i<dp.size();i++){
res=max(res,dp[i]);
}
return res;
}
};
leetcode674. 最长连续递增序列
674. 最长连续递增序列
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int n=nums.size();
vector<int> dp(n+1,0);
dp[0]=1;
for(int i=1;i<n;i++){
if(nums[i]>nums[i-1]){
dp[i]=dp[i-1]+1;
}
else{
dp[i]=1;
}
}
int res=0;
for(int i=0;i<dp.size();i++){
res=max(res,dp[i]);
}
return res;
}
};
leetcode718. 最长重复子数组
最长重复子数组
定义dp[i][j]为 以下标i为结尾的A,和以下标j 为结尾的B,最长重复子数组长度。
确定递推公式:
根据dp[i][j]的定义,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。
即当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;
例如:【1,2,3,4】与【9,1,2,3】
当i=1,j=2时候,相等,由于连续,那么i退一格,j退一格,刚好就是dp【i】【j】的更新。
根据递推公式可以看出,遍历i 和 j 要从1开始!
这里代码用:i-1是为了方便,不想初始化,直接把0视为无用位,从1开始推。
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp (nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
int result = 0;
for (int i = 1; i <= nums1.size(); i++) {
for (int j = 1; j <= nums2.size(); j++) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
if (dp[i][j] > result) result = dp[i][j];
}
}
return result;
}
};
怪盗基德的帽子
本质:做了正反两次的LIS(最长上升子序列),进行比较大小,找出最长递增子序列
#include <iostream>
using namespace std;
const int N=110;
int w[N],f[N];
int main(){
int k;
cin>>k;
int n;
while(k--){
cin>>n;
for(int i=1;i<=n;i++) cin>>w[i];
//正向
int res=0;
for(int i=1;i<=n;i++){
f[i]=1;
for(int j=1;j<i;j++){
if(w[j]<w[i]){
f[i]=max(f[i],f[j]+1);
}
}
res=max(res,f[i]);
}
//反向
for(int i=n;i>=1;i--){
f[i]=1;
for(int j=n;j>i;j--){
if(w[j]<w[i]){
f[i]=max(f[i],f[j]+1);
}
}
res=max(res,f[i]);
}
cout<<res<<endl;
}
return 0;
}
登山
与上题一模一样,上题是单边找最大,这题是把两边加起来-1就可以,本质一样。
#include <iostream>
using namespace std;
const int N = 1010;
int n;
int h[N];
int f[N], g[N];
int main(){
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &h[i]);
for (int i = 0; i < n; i ++ )
{
f[i] = 1;
for (int j = 0; j < i; j ++ )
if (h[i] > h[j])
f[i] = max(f[i], f[j] + 1);
}
for (int i = n - 1; i >= 0; i -- )
{
g[i] = 1;
for (int j = n - 1; j > i; j -- )
if (h[i] > h[j])
g[i] = max(g[i], g[j] + 1);
}
int res = 0;
for (int i = 0; i < n; i ++ ) res = max(res, f[i] + g[i] - 1);
printf("%d\n", res);
return 0;
}
合唱队形(洛谷)
[NOIP2004 提高组] 合唱队形 - 洛谷
和登山一模一样,但是记得减一就好,模型就是:求前后的最大上升子序列,减去总共的就是。
#include <iostream>
using namespace std;
const int N = 110;
int n;
int h[N];
int f[N], g[N];
int main(){
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &h[i]);
for (int i = 1; i <=n; i ++ )
{
f[i] = 1;
for (int j = 1; j < i; j ++ )
if (h[i] > h[j])
f[i] = max(f[i], f[j] + 1);
}
for (int i = n;i; i -- )
{
g[i] = 1;
for (int j = n; j > i; j -- )
if (h[i] > h[j])
g[i] = max(g[i], g[j] + 1);
}
int res = 0;
for (int i = 1; i <=n; i ++ ) res = max(res, f[i] + g[i] - 1);
int k=n-res;
printf("%d\n", k);
return 0;
}
友好城市(洛谷)
友好城市 - 洛谷
单调上升子序列,只有满足如上要求才可以
所以如果有一个序列排好序,则它一定是递增的,只要知道另一个对应的序列
的最长上升子序列,就是它最大的不重叠数量。
方法与思路:(举个栗子,图示)
用这样的想法我们能得到如下代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 200010;
int n;
typedef pair<int,int> pii;
pii w[N];
int f[N];
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++){
int x,y;
scanf("%d%d",&x,&y);
w[i]={x,y};
}
int res=0;
sort(w,w+n);
for(int i=0;i<n;i++){
f[i]=1;
for(int j=0;j<i;j++){
if(w[j].second<w[i].second){
f[i]=max(f[i],f[j]+1);
}
}
res=max(res,f[i]);
}
printf("%d",res);
return 0;
}
然后就会发现如下的东西:
超时:
我刚刚利用动态规划复杂度为,我们需要另外找一种方案去优化:
法1:
在我们每处理数列中的一位时,我们都要遍历数组找到值小于当前数中的f值的最大值,再用其加一作为现数字的值。如下,就是我们每一次记录的f【i】的值。
那么,如果我们维护一个取值集合,储存可能的最优解, 就可以优化算法的时间复杂度。
将一个数放入取值集合的条件是什么?如果两个数a、b ,当他们的f值相同,且a<b,那么a对于后来的数来说,显然比b优。(运用了优先队列的思想)。例如:图中3,1 用1一定比用3好,因为更小。
我们用函数图像能更直观理解:
所以我们只需要那么我们储存下对每一个f值来说的最小原数字值,在处理完新数字后将新数字与与其f值相同的数字比较大小,若小于则更新,若大于接在e数组后面。
每次我们都查找第一个比当前数大的位置在哪里,用lower_bound即可(二分法)。
用p做记录,记录尾插次数,也就是最长递增子序列,此时优化为了:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 200010;
int n;
typedef pair<int,int> pii;
pii w[N];
int e[N];
int p;
bool cmp(pii x,pii y){
return x.first<y.first;
};
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++){
int x,y;
scanf("%d%d",&x,&y);
w[i]={x,y};
}
sort(w,w+n,cmp);
for(int i=0;i<n;i++){
if(e[p]<w[i].second) {
p++;
e[p]=w[i].second;
}
else{
e[lower_bound(e,e+p,w[i].second)-e]=w[i].second;
}
}
cout<<p<<endl;
return 0;
}
然后就会得到:
最大上升子序列和
只是在第一个最长上升子序列上面加上了w【i】 ,几乎没有什么变化。
#include<bits/stdc++.h>
using namespace std;
int n,a[1005],f[1005],ans;
int main()
{
cin >> n;
for(int i = 1;i <= n;i++) cin >> a[i];
for(int i = 1;i <= n;i++)
{
f[i] = a[i];
for(int j = 1;j < i;j++)
if(a[j] < a[i]) f[i] = max(f[i],f[j] + a[i]);
ans = max(ans,f[i]);
}
cout << ans;
return 0;
}
背包问题分析:
01背包:
二维:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int v[MAXN]; // 体积
int w[MAXN]; // 价值
int f[MAXN][MAXN]; // f[i][j], j体积下前i个物品的最大价值
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
{
// 当前背包容量装不进第i个物品,则价值等于前i-1个物品
if(j < v[i])
f[i][j] = f[i - 1][j];
// 能装,需进行决策是否选择第i个物品
else
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
滚动数组一维优化:
#include<bits/stdc++.h>
using namespace std;
int f[1001],n,v,c[1001],w[1001];
int main()
{
cin >> n >> v;
for(int i = 1;i <= n;i++) cin >> c[i] >> w[i];
for(int i = 1;i <= n;i++)
for(int vv = v;vv >= c[i];vv--)
f[vv] = max(f[vv],f[vv - c[i]] + w[i]);
cout << f[v];
return 0;
}
Leetcode 416. 分割等和子集
Leetcode 416. 分割等和子集
#include<bits/stdc++.h>
using namespace std;
#include <math.h>
class Solution {
const int N=20001;
public:
bool canPartition(vector<int>& nums) {
int res=0;
for(int i=0;i<nums.size();i++) res+=nums[i];
if(res%2!=0) return false;
int dp[N];
memset(dp,0,sizeof(dp));
for(int i=0;i<nums.size();i++){
for(int j=res/2;j;j--){
if(j>=nums[i]){
dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]);
}else{
dp[j]=dp[j];
}
}
}
return dp[res/2]==res/2;
}
};
Leetcode 1049. 最后一块石头的重量 II
Leetcode 1049. 最后一块石头的重量 II
#include<bits/stdc++.h>
using namespace std;
const int N=15001;
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int dp[N];
memset(dp,0,sizeof(dp));
int res=0;
for(int i=0;i<stones.size();i++) res+=stones[i];
int r=res/2;
for(int i=0;i<stones.size();i++){
for(int j=r;j>=stones[i];j--){
dp[j]=max(dp[j],dp[j-stones[i]]+stones[i]);
}
}
return (res-dp[r])-dp[r];
}
};
Leetcode 494. 目标和
Leetcode 494. 目标和
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum=0;
for(int i=0;i<nums.size();i++) sum+=nums[i];
int z=(sum+target);
if(sum<abs(target)||z%2!=0) return 0;
int pos=z/2;
vector<int> dp(pos + 1, 0);
dp[0]=1;
for(int i=0;i<nums.size();i++){
for(int j=pos;j>=nums[i];j--){
dp[j]+=dp[j-nums[i]];
}
}
return dp[pos];
}
};
Leetcode 474.一和零
Leetcode 474.一和零
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> dp(m + 1, vector<int> (n + 1, 0));
for (string str : strs) { // 遍历物品
int oneNum = 0, zeroNum = 0;
for (char c : str) {
if (c == '0') zeroNum++;
else oneNum++;
}
for (int i = m; i >= zeroNum; i--) {
for (int j = n; j >= oneNum; j--) {
dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
}
}
}
return dp[m][n];
}
};
完全背包:
优化:
总结:
f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);//01背包
f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);//完全背包问题
二维:
#include<iostream>
using namespace std;
const int N = 10001;
int n, m;
int f[N][N], v[N], w[N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++ )
cin >> v[i] >> w[i];
for(int i = 1; i <= n; i ++ )
{
for(int j = 0; j <= m; j ++ )
{
if(v[i] <= j)
f[i][j] =max(f[i - 1][j], f[i][j - v[i]] + w[i]);
else
f[i][j] = f[i - 1][j];
}
}
cout << f[n][m] << endl;
}
滚动数组1维优化:
#include<bits/stdc++.h>
using namespace std;
int f[1001],n,v,c[1001],w[1001];
int main()
{
cin >> n >> v;
for(int i = 1;i <= n;i++) cin >> c[i] >> w[i];
for(int i = 1;i <= n;i++)
for(int vv = v;vv >= c[i];vv--)
f[vv] = max(f[vv],f[vv - c[i]] + w[i]);
//这里要正序,你可以理解为:f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);
//这里是i的f,而不是i-1的,所以要与01背包不同(i-1)则需要逆序
cout << f[v];
return 0;
}
注意:多重背包的遍历顺序
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
for i:物品
for j:背包大小
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
for j:背包大小
for i:物品
为什么:第一个理解成固定一个物品,再遍历背包看是否能满足,第二个应该是固定一个背包空间,遍历物品看是否能满足。
例如背包大小为3,·物品有1,2
如果按照第一种方式:先看1,再加入2,只会有{1,2}这种
如果按照第一种方式:背包大小为0,背包大小为1,背包大小为2,只就有{1,2},{2,1}这两种
所以:第一种求的是组合数,第二种求的是排列数。518,377就是上述两种不一样的方法。
Leetcde 518. 零钱兑换 II
Leetcde 518. 零钱兑换 II
class Solution {
public:
int change(int amount, vector<int>& coins) {
int n=coins.size();
vector<int> dp(amount+1,0);
dp[0]=1;
for(int i=0;i<n;i++){
for(int j=coins[i];j<=amount;j++){
dp[j]+=dp[j-coins[i]];
}
}
return dp[amount];
}
};
Leetcode 377.组合总数Ⅳ
Leetcode 377.组合总数Ⅳ
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
int n=nums.size();
vector<int> dp(target+1,0);
dp[0]=1;
for(int j=0;j<=target;j++){
for(int i=0;i<n;i++){
if(j>=nums[i]&& dp[j] < INT_MAX - dp[j - nums[i]])
dp[j]+=dp[j-nums[i]];
}
}
return dp[target];
}
};
leetcode 322.零钱兑换
322.零钱兑换
主要在于递推公式,但是不同的是初始化要一个特别大的数字,然后dp[0]=0;
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
const int INT_INF=1e9;
vector<int> dp(amount+1,INT_INF);
int n=coins.size();
dp[0]=0;
if(amount==0) return 0;
for(int i=0;i<n;i++){
for(int j=coins[i];j<=amount;j++){
dp[j]=min(dp[j],dp[j-coins[i]]+1);
}
}
if(dp[amount]==INT_INF) return -1;
return dp[amount];
}
};
leetcode279.完全平方数
279.完全平方数
我们抽象以下:
那么立马就变成完全背包问题:
物品就是:从1开始一直到100(数据范围),重量就是,价值就是i,然后dp[j]表示,表示第j个数需要的最少的数据个数。
和上面那个题一模一样。
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n+1,INT_MAX);
dp[0]=0;
for(int i=1;i<=sqrt(n);i++){
for(int j=pow(i,2);j<=n;j++){
dp[j]=min(dp[j],dp[j-pow(i,2)]+1);
}
}
return dp[n];
}
};
leetcode129.单词拆分
129.单词拆分
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());//为了find到截取的字符串
vector<bool> v(s.size()+1,0);
v[0]=1;
for(int i=1;i<=s.size();i++){ //字符从0~i开始遍历,指针为i,背包大小
for(int j=0;j<i;j++){ //i-j用来截取,只有当v[j]==1并且存在截取的子串时才可以
string sub=s.substr(j,i-j);
if(v[j]&&wordSet.find(sub)!=wordSet.end()){
v[i]=1;
}
}
}
return v[s.size()];
}
};
多重背包:
多重背包看做01背包
这个思路就是把多重背包看成是01背包:
#include<bits/stdc++.h>
using namespace std;
int n,vv,s[101],v[101],w[101],dp[101];
int main()
{
cin >> n >> vv;
for(int i = 1;i <= n;i++) cin >> v[i] >> w[i] >> s[i];
for(int i = 1;i <= n;i++)//遍历是第几个物品
{
for(int j = 0;j < s[i];j++)//遍历第i个物品用了j遍
{
for(int k = vv;k >= v[i];k--)
//逆序,遍历从最大背包体积开始,背包大小为k是,向背包放物品。
{
dp[k] = max(dp[k],dp[k - v[i]] + w[i]);//递推公式,体积为k时,不选这个物品,就是继承之前的i-1的dp[k],选这个物品,就是背包大小减去v[i],加上w[i]的价值。
}
}
}
cout << dp[vv];
return 0;
}
多重背包二进制优化
二进制怎么表示这个10呢 10 = 1 + 2 + 4 + 3,再比如7 就可以用 1 + 2 + 4来表示,只需要枚举3次。这就是我们二进制优化的思想。
比如:第一件物品有v[i]=2(体积),w[i]=3(价值),s[i]=12(数量);可以拆分为:4件如下图所示的物品:(其实本质还是01背包,只不过这时候我们合并了一下,让其装的更快了)
#include<bits/stdc++.h>
using namespace std;
int dp[2001],n,V,v,w,s;
int main()
{
cin >> n >> V;
for(int i = 0;i < n;i++)
{
cin >> v >> w >> s;//第i个物品,体积,价值,个数
for(int k = 1;k <= s;k <<= 1)// 以 k <<= 1 实际上是将 k 的值乘以 2。
{
for(int j = V;j >= k*v;j--)
{
dp[j] = max(dp[j],dp[j-k*v]+k*w);//把第i件多拆分成几件,再做01背包 }
s -= k;
}
if(s != 0)
{
for(int j = V;j >= s*v;j--)
{
dp[j] = max(dp[j],dp[j-s*v]+s*w);
}
}
}
cout << dp[V];
return 0;
}
单调队列优化
本题题解来源:AcWing 6. 多重背包问题 III【单调队列优化+图示】 - AcWing
按照完全背包的思路:我们列举如下:
r表示j
具体图示:
/*
时间复杂度的分析,我觉得通过代码很难看出来,
可以通过它的计算过程以及它计算的大体次数来体会。
比如总体积为V = 10,某个物品对应v=3
以一个物品为例,我们计算的时候,是把这个物品按照对v取余的结果来分类的。
v' = 0是一类,这一类有 0, 3, 6, 9 (v'表示当前正在求的体积)
v' = 1是一类,这一类有 1, 4, 7, 10
v' = 2是一类,这一类有 2, 5, 8
我们通过单调队列优化,只是在每一类中进行优化(滑动窗口求最值)
对于每个物品,我们都会求一遍v' = 0 ~ v' = 10,只是再求的过程中把它们分类了
一共n个物品,我们会对 物品1 求一遍 v' = 0 ~ v' = 10
对 物品2 求一遍 v' = 0 ~ v' = 10
....
对 物品n 求一遍 v' = 0 ~ v' = 10
总共实际求了 n * (v'的最大值), 即 n*m次
所以时间复杂度是O(n*m)的
*/
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 20020;
int f[N], g[N], q[N];
int n, m;
int main(){
scanf("%d%d", &n, &m);
for(int i = 0;i < n;i ++){
int v, w, s;
scanf("%d%d%d", &v, &w, &s);
memcpy(g, f, sizeof f);
for(int c = 0;c < v;c ++){ // 遍历余数
int hh = 0, tt = -1;
for(int j = c;j <= m;j += v){ // 遍历余数为c这一类的 体积
// 当前层的f[j] 暂时等于 上一层的g[j] 相当于 f[i][j] = f[i-1][j]; 也就是s = 0情况
f[j] = g[j];
// 这里一共有s+1个元素,s=0也算一个,所以这里不是j - s*v + 1
if(hh <= tt && j - s*v > q[hh]) hh ++; // 队列存的是下标,也是体积
// 队列中最大的(s!=0的其中一个) 和 s=0的进行比较
if(hh <= tt) f[j] = max(f[j], g[q[hh]] + (j-q[hh])/v*w);
// q[tt]这个体积下的价值,再加上与j体积相差的体积数的价值,才能与g[j]进行对等比较
while(hh <= tt && g[q[tt]] + (j - q[tt])/v*w <= g[j]) tt --;
q[++ tt] = j;
}
}
}
cout << f[m] << endl;
}
状态机
leetcode089.打家劫舍
089.打家劫舍
用普通线性dp:
从0~i家店铺最大收益:dp[i]=max(dp[i-1],dp[i-2]+nums[i]);
class Solution {
public:
int rob(vector<int>& nums) {
int n=nums.size();
vector<int> dp(n+1);
dp[0]=nums[0];
if(n==1) {
return dp[0];
}
dp[1]=max(nums[0],nums[1]);
if(n==2){
return dp[1];
}
for(int i=2;i<n;i++){
dp[i]=max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[n-1];
}
};
状态机思路:
引入两个状态:
f(i)-------->f(i,0)(未选择最后一个店铺)
|
|
------------>f(i,1)(选择最后一个店铺)
class Solution {
public:
int rob(vector<int>& nums) {
vector<vector<int>> dp(nums.size()+1,vector<int>(2,0));
dp[0][0]=0;
dp[0][1]=nums[0];
for(int i=1;i<nums.size();i++){
dp[i][0]=max(dp[i-1][0],dp[i-1][1]);
dp[i][1]=dp[i-1][0]+nums[i];
}
return max(dp[nums.size()-1][0],dp[nums.size()-1][1]);
}
};
213.打家劫舍2(补充线性dp)
对打家劫舍1进行分类讨论,分为:选第一个,还是选最后一个,其他都是一模一样。分为0~n-2与1~n-1,两个部分,也就是考虑nums[0],考虑nums[n-1],这两个数的分类讨论。(因为选了0不能选n-1,选了n-1,不能选0)
class Solution {
public:
int robrange(vector<int>& nums,int l,int r){
vector<int> dp(nums.size()+1,0);
dp[l]=nums[l],dp[l+1]=max(nums[l],nums[l+1]);
for(int i=l+2;i<=r;i++){
dp[i]=max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[r];
}
int rob(vector<int>& nums) {
int n=nums.size();
if(n==1) return nums[0];
if(n==2) return max(nums[0],nums[1]);
return max(robrange(nums,0,n-2),robrange(nums,1,n-1));
}
};
leetcode买股票的最佳时机4
买股票的最佳时机4
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
vector<vector<vector<int>>> dp(prices.size()+1,vector<vector<int>>(k+1,vector<int>(2,-1e9)));
//dp[prices.size()+1][k+1][2];
//考虑0次交易
for(int i=0;i<=prices.size();i++) dp[i][0][0]=0;
//dp[i][0][1]=-1e9;表示状态不合法
//考虑有交易
//dp[0][1][0]=-1e9;
dp[0][1][1]=-prices[0];
for(int i=1;i<prices.size();i++){
for(int j=1;j<=k;j++){
dp[i][j][0]=max(dp[i-1][j][0],dp[i-1][j][1]+prices[i]);
dp[i][j][1]=max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i]);
}
}
int res=0;
for(int i=0;i<=k;i++) res=max(res,dp[prices.size()-1][i][0]);
return res;
}
};
在这个基础上,我们直接搞定前面的几道股票题:
leetcode121. 买卖股票的最佳时机
121. 买卖股票的最佳时机
变化:
dp[i][0]=max(dp[i-1][0] , dp[i-1][1]+prices[i]);
dp[i][1]=max(dp[i-1][1] , -prices[i]);//因为限定交易次数为1,只能交易1次
class Solution {
public:
int maxProfit(vector<int>& prices) {
const int inf=-1e9;
vector<vector<int>> dp(prices.size()+1,vector<int>(2,inf));
dp[0][0]=0;
dp[0][1]=-prices[0];
for(int i=1;i<prices.size();i++){
dp[i][0]=max(dp[i-1][0] , dp[i-1][1]+prices[i]);
dp[i][1]=max(dp[i-1][1] , -prices[i]);//因为限定交易次数为1,只能交易1次
}
return max(dp[prices.size()-1][0],dp[prices.size()-1][1]);
}
};
leetcode122. 买卖股票的最佳时机 2
122. 买卖股票的最佳时机 II
变化:
dp[i][0]=max(dp[i-1][0] , dp[i-1][1]+prices[i]);
dp[i][1]=max(dp[i-1][1] , dp[i-1][0]-prices[i]);//无限交易次数
class Solution {
public:
int maxProfit(vector<int>& prices) {
const int inf=-1e9;
vector<vector<int>> dp(prices.size()+1,vector<int>(2,inf));
dp[0][0]=0;
dp[0][1]=-prices[0];
for(int i=1;i<prices.size();i++){
dp[i][0]=max(dp[i-1][0] , dp[i-1][1]+prices[i]);
dp[i][1]=max(dp[i-1][1] , dp[i-1][0]-prices[i]);//无限交易次数
}
return max(dp[prices.size()-1][0],dp[prices.size()-1][1]);
}
};
leetcode123. 买卖股票的最佳时机 3
123. 买卖股票的最佳时机 III
变化:
int k=2;即可,其他均不变
class Solution {
int k=2;
public:
int maxProfit( vector<int>& prices) {
vector<vector<vector<int>>> dp(prices.size()+1,vector<vector<int>>(k+1,vector<int>(2,-1e9)));
//dp[prices.size()+1][k+1][2];
//考虑0次交易
for(int i=0;i<=prices.size();i++) dp[i][0][0]=0;
//dp[i][0][1]=-1e9;表示状态不合法
//考虑有交易
//dp[0][1][0]=-1e9;
dp[0][1][1]=-prices[0];
for(int i=1;i<prices.size();i++){
for(int j=1;j<=k;j++){
dp[i][j][0]=max(dp[i-1][j][0],dp[i-1][j][1]+prices[i]);
dp[i][j][1]=max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i]);
}
}
int res=0;
for(int i=0;i<=k;i++) res=max(res,dp[prices.size()-1][i][0]);
return res;
}
};
leetcode买卖股票的最佳时机含冷冻期
买卖股票的最佳时间含冷冻期
class Solution {
public:
int maxProfit(vector<int>& prices) {
const int inf=-1e9;
vector<vector<int>> dp(prices.size(),vector<int>(3,inf));
dp[0][0]=0;
dp[0][1]=-prices[0];//第0天买入
//dp[0][2]=-1e9;
for(int i=1;i<prices.size();i++){
dp[i][0]=max(dp[i-1][0],dp[i-1][2]);
dp[i][1]=max(dp[i-1][0]-prices[i],dp[i-1][1]);
dp[i][2]=dp[i-1][1]+prices[i];
}
return max(dp[prices.size()-1][0],dp[prices.size()-1][2]);
}
};
leetcode买卖股票的最佳时机含手续费
买卖股票的最佳时机含手续费
与股票2完全一样,
dp[i][0]=max(dp[i-1][0] , dp[i-1][1]+prices[i]-fee) //卖出股票时完成一次交易,支付手续费
dp[i][1]=max(dp[i-1][1] , dp[i-1][0]-prices[i])
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
const int inf=-1e9;
vector<vector<int>> dp(prices.size()+1,vector<int>(2,inf));
dp[0][0]=0;
dp[0][1]=-prices[0];
for(int i=1;i<prices.size();i++){
dp[i][0]=max(dp[i-1][0] , dp[i-1][1]+prices[i]-fee);
dp[i][1]=max(dp[i-1][1] , dp[i-1][0]-prices[i]);//无限交易次数
}
return max(dp[prices.size()-1][0],dp[prices.size()-1][1]);
}
};
树形dp
leetcode 打家劫舍3
打家劫舍3
dp数组(这里写成PII)的含义:first记录不偷该节点所得到的的最大金钱,second记录偷该节点所得到的的最大金钱。
首先明确的是使用后序遍历。 因为要通过递归函数的返回值来做下一步计算。
通过递归左节点,得到左节点偷与不偷的金钱。
通过递归右节点,得到右节点偷与不偷的金钱。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
typedef pair<int,int> PII;
class Solution {
public:
int rob(TreeNode* root) {
PII r=robTree(root);
int result=max(r.first,r.second);
return result;
}
PII robTree(TreeNode*root){
if(root==nullptr) return {0,0};
PII l=robTree(root->left);
PII r=robTree(root->right);
int rob_yes=root->val+l.first+r.first;
int rob_not=max(l.second,l.first)+max(r.first,r.second);// 不偷cur,那么可以偷也可以不偷左右节点,则取较大的情况
return {rob_not,rob_yes};
}
};
leetcode二叉树的直径
二叉树的直径
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
int re=0;
public:
int diameterOfBinaryTree(TreeNode* root) {
int r=find(root);
return re;
}
int find(TreeNode* root){
if(root==nullptr) return -1;
int l=find(root->left)+1;
int r=find(root->right)+1;
re=max(re,l+r);
return max(l,r);
}
};
leetcode二叉树中的最大路径和
二叉树中的最大路径和
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
int re=-1e9;
public:
int maxPathSum(TreeNode* root) {
int r=maxTreeReturnlrAndSum(root);
return re;
}
int maxTreeReturnlrAndSum(TreeNode*root){
if(root==nullptr) return 0;
int l=maxTreeReturnlrAndSum(root->left);
int r=maxTreeReturnlrAndSum(root->right);
re=max(re,l+r+root->val);
return max(max(l+root->val,r+root->val),0);//由于返回值会有负数,所以我们还需要和0,取一个最大值,因为如果是负数,我们可以选择不要
}
};