1、01揹包
问题描述:有n种物品,每种物品有价值v和重量w,给定一个容量为c的揹包,每种物品只能选一个,求揹包能装物品的最大价值。
解法:用dp(i)( c)来表示当前揹包剩余容量为c时,前i个物品的最大价值。
之所以用这两个状态,是因为揹包不同容量时候,选择也不一样。
状态转移情况是选择或者不选择第i个物品,状态方程为:
(注意这个选择或者不选择第i个物品,它表示组合数,与顺序无关,尽管我们是按照顺序列举i的,但是我们列举第i个的时候就默认包含了所有的0-i,顺序列举只是为了拆分子问题。这和爬楼梯问题有本质区别,爬楼梯是有顺序的,是排列数,是:dp[n]=dp[n-1]+dp[n-2])
dp(i)( c) = max( dp(i-1)( c), dp(i-1)(c-wi)+vi );
public int f(int[] v,int w[],int n){
int[][] dp = new int[n+1][C+1];//此时初始化为0,不需要单独写
for(int i = 1;i<=n;i++){
for(int c=1;c<=C;c++){
if(w[i]>c){
dp[i][c]=dp[i-1][c];
}
else{
d[i][c] = Math.max(dp[i-1][c],dp[i-1][c-w[i]]+v[i]);
}
}
return dp[n][c];
}
}
有代码可见,每一行的状态只和上一行有关系,因此可以用空间压缩:
public int f(int[] v,int w[],int n){
int[] dp = new int[C+1];
for(int i = 1;i<=n;i++){
for(int c=C;c>=w[i];c--){//倒序是为了防止覆盖
d[c] = Math.max(dp[c],dp[c-w[i]]+v[i]);
}
return dp[c];
}
}
若题目要求恰好装满的最大价值,则没恰好装满的情况都是无解的情况,此时只需要改变初始值就可以,这样,不能恰好装满的都会被无穷小填充。
见leetcode 分割等和子集(这也是亲身经历惨痛的腾讯面试题)
public int f(){
int[][] dp = new int[n+1][C+1];
for(int i =1;i<=C;i++){
dp[0][i]=Integer.MIN_VALUE;
}
dp[0][0]=0;
for(int i = 2;i<=n;i++){
for(int c=1;c<=C;c++){
if(w[i]>c){
dp[i][c]=dp[i-1][c];
}
else{
d[i][c] = Math.max(dp[i-1][c],dp[i-1][c-w[i]]+v[i]);//此处控制无解情况
//若不选第i个,则由i-1个控制,若选择,则由dp[i-1][c-w[i]]控制
}
}
return dp[n][c];
}
}
爬楼梯问题:
有n个台阶,每次可以上w=[1,2,3,4]个台阶,问从0爬到n有多少种方法。
解法:每次列举w,状态方程dp[n]=dp[n-w[0]]+dp[n-w[1]]+…+dp[n-w[m]]
此题看似和揹包问题比较相似,但是有很大区别,比如第一步走1个台阶,第二步走两个台阶,和第一步走两个第二步走一个不是同一情况。但是对于揹包,则没有先后之分,都一样。
2、完全揹包
问题描述:有n种物品,每种物品有价值v和重量w,给定一个容量为c的揹包,每种物品可以选任意个,求揹包能装物品的最大价值。
这个问题和01揹包的区别是,我们进行状态转移时候,不仅考虑选或者不选,而是选0件,选1件,一直到选C/w[i]件。
可以写出状态方程:
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-w[i]]+v[i],dp[i-1][j-2w[i]]+2v[i]...)
//dp[i][j]表示前i个物品组成最大容量为j的揹包的最大价值
此时解题的代码可以写为:
public int f(){
int[][] dp = new int[n+1][C+1];
for(int i = 1;i<=n;i++){
for(int j=1;j<=C;j++){
for(int k=0;k*w[i]<j;k++){
dp[i][j]=Math.max(dp[i][j],dp[i-1][j-k*w[i]]+k*v[i]);
}
}
return dp[n][c];
}
}
上述的复杂度是C^2 *N,考虑优化状态方程:
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-w[i]]+v[i],dp[i-1][j-2w[i]]+2v[i]...)(1)
把dp[i][j-w[i]]带入上述方程:
dp[i][j-w[i]]=Math.max(dp[i-1][j-w[i]],dp[i-1][j-2w[i]]+v[i]...)(2)
dp[i][j-w[i]]+v[i]=v[i]+Math.max(dp[i-1][j-w[i]],dp[i-1][j-2w[i]]+2v[i]...)(3)
=Math.max(dp[i-1][j-w[i]]+v[i],dp[i-1][j-2w[i]]+2v[i]...)(4)
公式(1)中的后面的项和公式(4)后面的项一样,可以合并:
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-w[i]]+v[i])
由此我们得到优化的转移方程,复杂度是C*n。
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-w[i]]+v[i])
上述方程也可以有另一种解释:对于第i个物品,我们面临两种互补的选择:要么不选i,要么至少选一个i,两种情况不仅互斥,他们的并集就是全集,所以称之为互补。至于为什么至少选一个i是这种形式:
dp[i][j-w[i]]+v[i]
因为我们要消除选择一个i带来的影响,而不管接下来会怎么样,就是至少选择一个。就像正则表达式匹配一样。正则表达式匹配
3、多重揹包
问题描述:有n种物品,每种物品有价值v和重量w,给定一个容量为c的揹包,每种物品可以选p个,求揹包能装物品的最大价值。
此问题明显是完全揹包的变形,可以和完全揹包几乎一样,只是多了一个最多p个的判断:
public int f(){
int[][] dp = new int[n+1][C+1];
for(int i = 1;i<=n;i++){
for(int j=1;j<=C;j++){
for(int k=0;k*w[i]<j && k<=p[i];k++){
dp[i][j]=Math.max(dp[i][j],dp[i-1][j-k*w[i]]+k*v[i]);
}
}
return dp[n][c];
}
}
4、分组揹包
问题描述: 物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入揹包可使这些物品的费用总和不超过揹包容量,且价值总和最大。
问题变成了每组选或者不选
状态方程为:
f[k][j]=max(f[k−1][j],f[k−1][j−c[i]]+w[i]∣物品i属于组k)