揹包问题可能是动态规划算法中最经典的问题了,三种最常见的揹包问题分别是0-1揹包问题,完全揹包问题和多重揹包问题。关于这三种揹包的讲解网上有很多,但是很多只是给出状态转移方程或写出伪代码,或者是只给出0-1揹包和完全揹包的代码但并没有多重揹包的代码,因此本文在这里将系统地总结这三种揹包问题的最佳解法及相应java代码。
题1.经典的0-1揹包问题。给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。比如说,现在有一个揹包,它一共能装50的物品,现在有三种物品,重量分别10,20,30,价值分别为60,100,120,每种物品最多选一次,问如何选可以在不超过揹包容量的情况下获取的价值最大。
首先分析本题的状态转移方程,设dp[i][j]表示取到第i件物品,揹包容量为j时,揹包所装物品的价值,对于每一件物品,要不选要么不选,则
dp[i][j]=max{dp[i-1][j],dp[i-1][j-w[i]]+v[i]}.
根据上述状态转移方程,可写出最基本的执行代码如下:
public class DP4 {
/**
* @param wj
*/
//0-1揹包问题,非压缩矩阵
public static int func1(int capacity,int n,int[] w,int[] v){
if(n==1) return v[0];
int [][] dp=new int[n][capacity+1];//注意dp列长度为capacity+1
for(int i=0;i<n;i++){
for(int j=w[i];j<=capacity;j++){
if(i>=1){//防止dp数组下标越界
dp[i][j]=Math.max(dp[i-1][j], dp[i-1][j-w[i]]+v[i]);
}
else dp[i][j]=v[i];
}
}
return dp[n-1][capacity];
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int capacity=50;
int [] w={10,20,30};
int [] v={60,100,120};
int n=3;//物品数量
System.out.println(func1(capacity,n, w, v));//220
}
}
代码执行结果为220.
可以看到上述方法使用了一个二维数组来解决问题,虽然最直观最容易理解,但空间复杂度比较高。于是考虑能否将二维数组压缩为一维数组来解决问题。幸运地是,通过逆序方式,可以将二维数组压缩为一维数组,读者可以手动模拟一下算法执行过程,可以看到当将j逆序循环时,后一个状态值恰好等于前一个状态的执行结果,状态转移方程为dp[j]=max{dp[j],dp[j-w[i]]+v[i]},相当于省去了i之后,等号右边的dp[j]和dp[j-w[i]]+v[i]依然相当于dp[i-1][j]和dp[i-1][j-w[i]]+v[i]。有疑惑的话可以手动模拟算法过程看看,笔者曾手动模拟过,觉得这种方法确实很神奇。相应代码如下:
public class DP4 {
/**
* @param wj
*/
//0-1揹包问题,压缩矩阵
public static int func2(int capacity,int n,int[] w,int[] v){
int [] dp=new int[capacity+1];//注意dp列长度为capacity+1
for(int i=0;i<n;i++){
for(int j=capacity;j>=w[i];j--){
dp[j]=Math.max(dp[j], dp[j-w[i]]+v[i]);
}
}
return dp[capacity];
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int capacity=50;
int [] w={10,20,30};
int [] v={60,100,120};
int n=3;
System.out.println(func2(capacity,n, w, v));//220
}
}
代码执行结果为220。
可以看到上述方法将二维数组压缩为一维,空间复杂度大大降低。
题2.完全揹包问题。有了0-1揹包的基础,现在来看看完全揹包问题。所谓完全揹包就是在0-1揹包的基础上,每件物品都有无数件,且每件物品可以取多件,求能装入揹包的最大价值。
本题的状态转移方程为:dp[i][j]=max{dp[i-1][j],dp[i-1][j-k*w[i]]+k*v[i]},其中k<=capacity/w[i]。如果用这个转移方程来求解是比较麻烦,我们同样可以考虑压缩矩阵,压缩之后的状态转移方程为:dp[j]=max{dp[j],dp[j-w[i]]+v[i]},注意这里的j不再是逆序而是顺序,这是与0-1揹包代码的唯一区别,有疑惑的可以手动模拟计算过程,相应代码如下:
public class DP4 {
/**
* @param wj
*/
//完全揹包问题,压缩矩阵
public static int func3(int capacity,int n,int[] w,int[] v){
int[] dp=new int[capacity+1];//注意dp列长度为capacity+1
for(int i=0;i<n;i++){
for(int j=w[i];j<=capacity;j++){
dp[j]=Math.max(dp[j], dp[j-w[i]]+v[i]);
}
}
return dp[capacity];
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int capacity=50;
int [] w={10,20,30};
int [] v={60,100,120};
int n=3;
System.out.println(func3(capacity,n, w, v));//300
}
}
代码执行结果为300.即第一件物品取了5件。
题3.多重揹包问题。本题是完全揹包的变形,即现在每种物品有num[i]件,每种物品可取多件但不能超过num[i]件,求能装入揹包的最大价值。
本题的状态转移方程与完全揹包相似,dp[i][j]=max{dp[i-1][j],dp[i-1][j-k*w[i]]+k*v[i]},其中k<=num[i].我们依然采用压缩矩阵的方法,得出一维矩阵下的状态转移方程为:
dp[j]=max{dp[j],dp[j-w[i]]+v[i]},需要注意的是,这里的j需要逆序,而且需要多一个循环用来存放第i个物品的数量num[i],代码如下:
public class DP4 {
/**
* @param wj
*/
//多重揹包问题,压缩矩阵
public static int func4(int capacity,int n,int[] w,int[] v,int[] num){
int[] dp=new int[capacity+1];//注意dp列长度为capacity+1
for(int i=0;i<n;i++){
for(int j=0;j<num[i];j++){
for(int k=capacity;k>=w[i];k--){
dp[k]=Math.max(dp[k], dp[k-w[i]]+v[i]);
}
}
}
return dp[capacity];
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int capacity=50;
int [] w={10,20,30};
int [] v={60,100,120};
int [] num={3,4,5};//多重揹包问题,各物品数量
int n=3;
System.out.println(func4(capacity, n, w, v, num));//280
}
}
代码的执行结果为:280,相当于第一件物品取3件,第二件物品取1件。
以上就是最常见的关于揹包问题的分析与解法,希望对读者有一定帮助,有没解释清楚的地方大家也可以百度看看大牛们对揹包问题,尤其是压缩矩阵的理解,笔者才疏学浅,要多向大佬们学习。