04动态规划进阶---揹包问题

揹包问题可能是动态规划算法中最经典的问题了,三种最常见的揹包问题分别是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件。

以上就是最常见的关于揹包问题的分析与解法,希望对读者有一定帮助,有没解释清楚的地方大家也可以百度看看大牛们对揹包问题,尤其是压缩矩阵的理解,笔者才疏学浅,要多向大佬们学习。

点赞