01背包问题 题目来源:
2. 01背包问题 - AcWing题库
具体代码:
没有优化前:状态方程是二维的时候
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream> #include <algorithm> using namespace std;const int N = 1010 ;int n,m;int v[N],w[N];int f[N][N];int main (void ) { 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++){ f[i][j]=f[i-1 ][j]; if (j>=v[i]) f[i][j]=max (f[i][j],f[i-1 ][j-v[i]]+w[i]); } } cout << f[n][m]<<endl; return 0 ; }
利用滚动数组优化为一维数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <iostream> #include <algorithm> using namespace std;const int N = 1010 ;int n, m;int v[N], w[N];int f[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 = m; j >= v[i]; j -- ) f[j] = max (f[j], f[j - v[i]] + w[i]); cout << f[m] << endl; return 0 ; }
对目前的我来说只是简单理解,但是化成文字还是有点困难。等我完全理解了再做解析
其他参考博客:
dd大牛的《背包九讲》 - 贺佐安 - 博客园 (cnblogs.com) (需要仔细研读)
(56条消息) 动态规划专题——背包问题_动态规划 背包_Iareges的博客-CSDN博客
AcWing 2. 01背包问题 - AcWing
完全背包问题 状态转移方程
1 f[i,j]=max (f[i][j],f[i-1 ,j-v[i]*k]+w[i]*k);
优化:
1 2 f[i][j] = Max (f[i-1 ][j],f[i-1 ,j-v]+w,f[i-1 ][j-2 v]+2 w,f[i-1 ][j-3 v]+3 w...) (1 ) f[i][j-v]= Max ( f[i-1 ][j-v], f[i-1 ][j-2 v]+w, f[i-1 ][j-3 v]+2 w...) (2 )
对比(1)(2)可知,(1)中f[i-1,j-v]+w,f[i-1][j-2v]+2w,f[i-1][j-3v]+3w...
为(2)f[i][j-v]+w
即可
即: 优化后的完全背包为
f[i][j]=Max(f[i-1][j],f[i][j-v]+w)
完整代码如下:
1 2 3 4 5 6 7 for (int i=1 ;i<=n;i++){ for (int j=0 ;j<=m;j++){ f[i][j]=f[i-1 ][j]; if (j>=v[i])f[i][j]=Max (f[i-1 ][j],f[i][j-v[i]]+w[i]); } } cout<<f[n][m];
对比完全背包和01背包状态转移方程可知
1 2 3 4 f[i][j]=max (f[i][j],f[i-1 ][j-v[i]]+w[i]); f[i][j]=Max (f[i-1 ][j],f[i][j-v[i]]+w[i]);
只有一个地方不同,就是01背包是f[i-1][j-v[i]]
而完全背包是f[i][j-v[i]]
一个是从i-1的状态转移而来,另外一个是从i个状态转移而来,原因还是因为,完全背包可以重复选取!
完全背包也可以优化为1维:
1 2 3 4 5 6 for (int i=1 ;i<=n;i++){ for (int j=v[i];j<=m;j++){ f[j]=Max (f[j],f[j-v[i]]+w[i]); } } cout<<f[m];
注意这里对j不需要倒序,因为我们的f[j-v[i]]
与f[j]
是都在i层,而且需要从前往后推导的
多重背包问题 每件物品的个数有限制,有s[i]件
动态规划:
状态表示f[i][j]
:
集合:所有只从前i个物品中选,并且总体积不超过j的选法
属性:Max
状态计算
最朴素的三重循环
1 2 3 4 5 6 7 for (int i=1 ;i<=n;i++){ for (int j=0 ;j<=m;j++){ for (int k=0 ;k<=s[i]&&k*v[i]<=j;k++) f[i][j]=max (f[i][j],f[i-1 ][j-v[i]*k]+w[i]*k) } } cout<<f[n][m]<<endl;
优化算法:(错)
如果我们尝试使用完全背包的优化方法:
1 2 f[i][j] =max (f[i-1 ][j],f[i-1 ][j-v]+w,f[i-1 ][j-2 v]+2 w,...f[i-1 ][j-sv]+sw) (1 ) f[i][j-v] =max ( f[i-1 ][j-v] ,f[i-1 ][j-2 v]+w,...f[i-1 ][j-sv]+(s-1 )w+f[i-1 ][j-v-sv]+sw)(2 )
第二项最后的f[i-1][j-v-sv]+sw
怎么来的呢?
引用网上一个讲解
体积j,考虑第i个物品时,我们会考虑放0个,1个,2个,3个,….,一直到放不下第i个物品 或者物品不够用为止 为止(这里物品不够用就是关键了,比如我们的体积j非常大,第i个物品只有3个,那么最后一项就是 f(i - 1, j - 3*vi) + 3 * wi
对于体积 j - vi,考虑第i个物品时,第i个物品只有3个,假设j - vi的体积也都能放下它们,那么最后一项就是 f(i - 1, j - vi - 3*vi) + 3 * wi
正确的优化算法
二进制优化:不懂就去看y总视频讲解,这个太牛逼了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <iostream> #include <algorithm> using namespace std;const int N = 12010 , M = 2010 ;int n, m;int v[N], w[N];int f[M];int main () { cin >> n >> m; int cnt = 0 ; for (int i = 1 ; i <= n; i ++ ) { int a, b, s; cin >> a >> b >> s; int k = 1 ; while (k <= s) { cnt ++ ; v[cnt] = a * k; w[cnt] = b * k; s -= k; k *= 2 ; } if (s > 0 ) { cnt ++ ; v[cnt] = a * s; w[cnt] = b * s; } } n = cnt; for (int i = 1 ; i <= n; i ++ ) for (int j = m; j >= v[i]; j -- ) f[j] = max (f[j], f[j - v[i]] + w[i]); cout << f[m] << endl; return 0 ; }
分组背包问题 9. 分组背包问题 - AcWing题库
分组背包问题:
有 N组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。 每件物品的体积是 V[i][j]
,价值是 w[i][j]
,其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
状态表示f[i][j]
集合:从前i个物品中选,且总体积不大于j的所有选法
属性:Max
状态计算
状态转移方程:
1 f[i-1 ][j]=Max (f[i-1 ][j],f[i-1 ][j-v[i,k]]+w[i,k]);
对每一组中物品的选择,由于每一组中物品的价值和重量也不一样,所以可以认为是每一组的01背包问题
代码如下:
二维:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <bits/stdc++.h> using namespace std;const int N=110 ;int f[N][N]; int v[N][N],w[N][N],s[N]; int n,m,k;int main () { cin>>n>>m; for (int i=1 ;i<=n;i++){ cin>>s[i]; for (int j=0 ;j<s[i];j++){ cin>>v[i][j]>>w[i][j]; } } for (int i=1 ;i<=n;i++){ for (int j=0 ;j<=m;j++){ f[i][j]=f[i-1 ][j]; for (int k=0 ;k<s[i];k++){ if (j>=v[i][k]) f[i][j]=max (f[i][j],f[i-1 ][j-v[i][k]]+w[i][k]); } } } cout<<f[n][m]<<endl; }
一维:由于我们认为是每一组是01背包问题,所以体积从大到小,至于每一组的时候为什么先循环体积再循环物品个数,原因如下:
还是滚动数组的问题,在二维循环中是可以这么来做的,但是在压缩数组以后:
如果优化了第一维坐标只用f[j]的话 正常顺序先遍历j再枚举物品时 只会修改当前这个j 意思是枚举物品这一层循环不会互相影响 但是如果先枚举物品k = k1时进入j循环 修改了f[j-v[i][k1]
然后枚举下一个物品k = k2时 用到的f[j-v[i][k2]]
可能跟f[j-v[i][k1]]
相等,也就是被k1修改过了 而分组背包每一组最多选一个,理论上是不会互相影响的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <iostream> #include <algorithm> using namespace std;const int N = 110 ;int n, m;int v[N][N], w[N][N], s[N];int f[N];int main () { cin >> n >> m; for (int i = 1 ; i <= n; i ++ ) { cin >> s[i]; for (int j = 0 ; j < s[i]; j ++ ) cin >> v[i][j] >> w[i][j]; } for (int i = 1 ; i <= n; i ++ ) for (int j = m; j >= 0 ; j -- ) for (int k = 0 ; k < s[i]; k ++ ) if (v[i][k] <= j) f[j] = max (f[j], f[j - v[i][k]] + w[i][k]); cout << f[m] << endl; return 0 ; }
线性DP 数字三角形 898. 数字三角形 - AcWing题库
针对这个题展开线性DP记录
状态表示
集合:所有从起点走到i,j的路径
属性:Max
状态计算 对于i,j,比较来自左上方和上方的两个值
1 f[i][j]=Max (f[i-1 ][j-1 ],f[i-1 ][j])+a[i][j]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <iostream> #include <algorithm> using namespace std;const int INF=1e9 ;const int N =510 ;int n;int dp[N][N];int a[N][N];int main (void ) { cin>>n; for (int i=1 ;i<=n;i++){ for (int j=1 ;j<=i;j++){ cin>>a[i][j]; } } for (int i=0 ;i<=n;i++){ for (int j=0 ;j<=i+1 ;j++){ dp[i][j]=-INF; } } dp[1 ][1 ]=a[1 ][1 ]; for (int i=2 ;i<=n;i++){ for (int j=1 ;j<=i;j++){ dp[i][j]=max (dp[i-1 ][j-1 ]+a[i][j],dp[i-1 ][j]+a[i][j]); } } int Maxnum=-INF; for (int j=1 ;j<=n;j++){ Maxnum=max (Maxnum,dp[n][j]); } cout<<Maxnum; return 0 ; }
注意数组的初始化大小以及初始化的i,j的值!!
最长上升子序列 895. 最长上升子序列 - AcWing题库
状态表示 集合:所有以i结尾的上升子序列的最大值
属性:Max
状态计算 1 f[i]=max (f[j]+1 ) j=1 =0 ,1 ,2 ,3 ,4. .i-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <iostream> #include <algorithm> using namespace std;const int N =1010 ;int n;int dp[N];int a[N];int main (void ) { cin>>n; for (int i=1 ;i<=n;i++){ cin>>a[i]; } for (int i=1 ;i<=n;i++){ dp[i]=1 ; for (int j=1 ;j<=i-1 ;j++){ if (a[j]<a[i]) dp[i]=max (dp[i],dp[j]+1 ); } } int Maxnum=0 ; for (int i=1 ;i<=n;i++){ Maxnum=max (dp[i],Maxnum); } cout<<Maxnum; return 0 ; }
最长公共子序列 897. 最长公共子序列 - AcWing题库
状态表示 集合:f[i][j]
所有由第一个序列前i个字母中出现和第二个序列的前j个字母中出现的子序列公共子序列
属性:Max
状态计算
以a[i]和b[j]是否包含在子序列当中来作为划分依据
f[i][j]=max(f[i-1][j-1],f[i-1][j],f[i][j-1],f[i-1][j-1]+1)
但是需要说明的是
f[i-1][j]
和f[i][j-1]
表示的不是01和10这两种情况
01情况:即a序列不包含i和b的子序列包含j的最长子序列
而f[i-1][j]
的值是前a中前i-1个序列和b中前j个序列的最长子序列
如果把f[i-1][j]
用上面的图表示,其中f[i-1][j]
中11
的情况才是满足的f[i][j]
中01的情况的
所以f[i-1][j]>=f[i][j]的01情况的
,但是我们仍然可以用f[i-1][j]
来代替01的情况,因为属性是最大值 ,可以允许有交集
给出代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> #include <algorithm> using namespace std;const int N =1010 ;int n,m;char a[N],b[N];int dp[N][N];int main (void ) { cin>>n>>m; scanf ("%s%s" ,a+1 ,b+1 ); for (int i=1 ;i<=n;i++){ for (int j=1 ;j<=m;j++){ dp[i][j]=max (dp[i-1 ][j],dp[i][j-1 ]); if (a[i]==b[j]) dp[i][j]=max (dp[i][j],dp[i-1 ][j-1 ]+1 ); } } cout<<dp[n][m]; return 0 ; }
区间dp问题 282. 石子合并 - AcWing题库
在一个区间内的进行DP问题
状态表示 f[i][j]
表示i到j的一个区间
集合:所有将第i堆石子到第j堆石子合并为一堆石子的合并方式
属性:合并方式的代价最小值 Min
状态计算 这是一种错误的做法:不可以枚举端点值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <iostream> #include <algorithm> using namespace std;const int N =310 ,INF=1e9 ;int n;int dp[N][N];int a[N],s[N];int main (void ) { cin>>n; for (int i=1 ;i<=n;i++){ for (int j=1 ;j<=n;j++){ dp[i][j]=INF; } } for (int i=1 ;i<=n;i++){ cin>>a[i]; s[i]=s[i-1 ]+a[i]; dp[i][i]=a[i]; } for (int i=1 ;i<=n-1 ;i++){ for (int j=i+1 ;j<=n;j++){ dp[i][j]=s[j]-s[i-1 ]; } } for (int i=1 ;i<=n-1 ;i++){ for (int j=i+1 ;j<=n;j++){ for (int k=i;k<j;k++){ dp[i][j]=min (dp[i][j],dp[i][k]+dp[k+1 ][j]+s[j]-s[i-1 ]); } } } cout<<dp[1 ][n]; return 0 ; }
正确方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <iostream> #include <algorithm> using namespace std;const int N = 310 ;int n;int s[N];int f[N][N];int main () { scanf ("%d" , &n); for (int i = 1 ; i <= n; i ++ ) scanf ("%d" , &s[i]); for (int i = 1 ; i <= n; i ++ ) s[i] += s[i - 1 ]; for (int len = 2 ; len <= n; len ++ ) for (int i = 1 ; i + len - 1 <= n; i ++ ) { int l = i, r = i + len - 1 ; f[l][r] = 1e8 ; for (int k = l; k < r; k ++ ) f[l][r] = min (f[l][r], f[l][k] + f[k + 1 ][r] + s[r] - s[l - 1 ]); } printf ("%d\n" , f[1 ][n]); return 0 ; }
数位dp 338. 计数问题 - AcWing题库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include <iostream> #include <cstring> #include <algorithm> #include <vector> using namespace std;int get (vector<int > num,int l,int r) { int res=0 ; for (int i=l;i>=r;i--) res=res*10 +num[i]; return res; } int power10 (int i) { int res=1 ; while (i--) res*=10 ; return res; } int count (int n,int x) { vector<int > num; while (n) { num.push_back (n%10 ); n/=10 ; } n=num.size (); int res=0 ; for (int i=n-1 -!x;i>=0 ;i--) { if (i<n-1 ) { res+=get (num,n-1 ,i+1 )*power10 (i); if (!x) res-=power10 (i); } if (num[i]==x) res+=get (num,i-1 ,0 )+1 ; else if (num[i]>x) res+=power10 (i); } return res; } int main () { int a,b; while (cin>>a>>b,a) { if (a>b) swap (a,b); for (int i=0 ;i<=9 ;i++) cout<<count (b,i)-count (a-1 ,i)<<' ' ; cout<<endl; } return 0 ; }
状态压缩dp 91. 最短Hamilton路径 - AcWing题库
AcWing 91. 最短Hamilton路径(超详解) - AcWing
AcWing 91. 最短Hamilton路径(管家级详解) - AcWing
这真是个好题!!!
状态表示:
集合:f[i,j]:集合:所有从0号走到j号,走过的所有点是i的所有路径
属性:Min
状态计算:
根据倒数第二个点来求,就是从0走到j倒数第二个点是0…j-1的所有走法
假如是从 0—k—-j 从0经过k到j一共走了i个点,
k到j的长度已经确定了。0—k走过i-1个点的长度应该是
f(i-{j},k)+a(k,j);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <iostream> #include <algorithm> #include <cstring> using namespace std;const int N=20 ,M=1 <<N;int f[M][N],weight[N][N];int main (void ) { int n; cin>>n; for (int i=0 ;i<n;i++){ for (int j=0 ;j<n;j++){ cin>>weight[i][j]; } } memset (f,0x3f ,sizeof (f)); f[1 ][0 ]=0 ; for (int i=0 ;i<1 <<n;i++){ for (int j=0 ;j<n;j++){ if (i>>j&1 ){ for (int k=0 ;k<n;k++){ if (i>>k&1 ){ f[i][j]=min (f[i][j],f[i-(1 <<j)][k]+weight[k][j]); } } } } } cout<<f[(1 <<n)-1 ][n-1 ]<<endl; return 0 ; }
树形dp 状态表示:
集合 :f[u][0]
:所有以U为根的子树中选并且不选u这个点的方案
f[u][1]
:所有以u为根的子树中选,并且选u这个点的方案
属性 :Max
对于 u 的子节点 s0 s1 等
f(u,0)=max(f(s0,0),f(s1,1))+max(f(s0,0),f(s1,1))+…max(f(si,0),f(si,1)) i取遍每个子节点[子节点选上和不选都可以]
f(u,1)=f(s0,0)+f(s1,0)+…f(si,0) i取遍每个子节点 选了根节点,就不可以选子节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <iostream> #include <algorithm> #include <cstring> using namespace std;const int N = 6010 ;int n;int happy[N]; int f[N][2 ]; int e[N],ne[N],h[N],idx; bool has_father[N]; void add (int a,int b) { e[idx] = b,ne[idx] = h[a],h[a] = idx ++; } void dfs (int u) { f[u][1 ] = happy[u]; for (int i = h[u];~i;i = ne[i]){ int j = e[i]; dfs (j); f[u][0 ] += max (f[j][1 ],f[j][0 ]); f[u][1 ] += f[j][0 ]; } } int main () { scanf ("%d" ,&n); for (int i = 1 ;i <= n;i ++) scanf ("%d" ,&happy[i]); memset (h,-1 ,sizeof h); for (int i = 1 ;i < n;i ++){ int a,b; scanf ("%d%d" ,&a,&b); has_father[a] = true ; add (b,a); } int root = 1 ; while (has_father[root]) root ++; dfs (root); printf ("%d\n" ,max (f[root][0 ],f[root][1 ])); return 0 ; }
如何用邻接表来表示树
重点是三个数组 h, e ,ne
h[k]存储这个单链表的头结点,e[]存储数据域的值,ne[]存储结点的next指针
初始化
1 2 idx = 0 ; memeset (h, -1 , sizeof (h));
1 2 3 void add (int a,int b) { e[idx] = b,ne[idx] = h[a],h[a] = idx ++; }
记忆化搜索 901. 滑雪 - AcWing题库
AcWing 901. 滑雪 - AcWing
状态表示:所有从i,j开始滑的路径
属性:所有路劲的最大长度 MAX
状态计算:
分类讨论,上下左右四个方向滑雪
f[i][j]
=f[i][j+1]+1
f记忆化搜索表示的状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <iostream> #include <algorithm> #include <cstring> using namespace std;const int N =400 ;int h[N][N];int f[N][N];int MaxLenth;int n,m;int dx[4 ]={1 ,-1 ,0 ,0 },dy[4 ]={0 ,0 ,-1 ,1 }; int dp (int x,int y) { int &F=f[x][y]; if (F!=-1 ) return F; F=1 ; for (int i=0 ;i<4 ;i++){ int a=x+dx[i],b=y+dy[i]; if (a>=1 && a<=n && b>=1 && b<=m && h[a][b]<h[x][y]){ F=max (F,dp (a,b)+1 ); } } return F; } int main (void ) { cin>>n>>m; for (int i=1 ;i<=n;i++){ for (int j=1 ;j<=m;j++){ cin>>h[i][j]; } } memset (f,-1 ,sizeof (f)); for (int i=1 ;i<=n;i++){ for (int j=1 ;j<=m;j++){ MaxLenth=max (MaxLenth,dp (i,j)); } } cout<<MaxLenth; return 0 ; }
树形dp 打家劫舍 树形DP:
337. 打家劫舍 III - 力扣(LeetCode)
f[u][0]
:以节点u
为根节点且不选根节点的子树的最大金额;
f[u][1]
:以节点u
为根节点且选根节点的子树的最大金额;
f[u][0]=max(f[s1][0], f[s1][1]) + max(f[s2][0], f[s2][1])
;
f[u][1]=u.val + f[s1][0] + f[s2][0]
;
其中,s1
和s2
表示节点u
的两个子节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution {public : typedef pair<int ,int >PII; PII dfs (TreeNode*root) { if (root==NULL ) return {0 ,0 }; auto left=dfs (root->left); auto right=dfs (root->right); int res0 = max (left.first,left.second)+max (right.first,right.second); int res1 = root->val+left.first+right.first; return {res0,res1}; } int rob (TreeNode* root) { auto res=dfs (root); return max (res.first,res.second); } };