动态规划之4种背包问题

1. 0-1背包

给定n 种物品,每种物品都有重量 w i w_i wi和价值 v i v_i vi,每种物品都只有一个。另外,背包容量为W 。求解在不超过背包容量的情况下将哪些物品放入背包,才可以使背包中的物品价值之和最大。每种物品只有一个,要么不放入(0),要么放入(1),因此称之为01背包

c [i ][j]表示将前i 种物品放入容量为j 的背包中获得的最大价值

第i种物品有两种选择:

  1. 不放入:放入背包的价值不增加,问题转化为“将前i -1种物品放入容量为j 的背包中获得的最大价值”,最大价值为c [i -1][j ]
  2. 放入:在第i 种物品放入之前为第i -1阶段,相当于从第i -1阶段向第i 阶段转化。问题转化为“将前i -1种物品放入容量为j -w [i ]的背包中获得的最大价值”,此时获得的最大价值就是 c [i -1][j -w [i ]],再加上放入第i 种物品获得的价值v [i ],总价值为c [i -1][j -w [i ]]+v [i ]

若背包容量不足,则肯定不可以放入,所以价值仍为前i- 1种物品处理后的结果;若背包容量充足,则考察在放入、不放入哪种情况下获得的价值更大, 状态转移方程如下:

c [ i ] [ j ] = { c [ i − 1 ] [ j ] , j<w[i] 当前背包剩余容量不能放下物品i m a x ( c [ i − 1 ] [ j − w [ i ] ] + v [ i ] , c [ i − 1 ] [ j ] ) , 当前背包剩余容量能放下物品i,可以选择放也可以选择不放 c[i][j]= \begin{cases} c[i-1][j], & \text{j<w[i] 当前背包剩余容量不能放下物品i} \\ max(c[i-1][j-w[i]]+v[i],c[i-1][j]), & \text{当前背包剩余容量能放下物品i,可以选择放也可以选择不放} \\ \end{cases} c[i][j]={c[i1][j],max(c[i1][jw[i]]+v[i],c[i1][j]),j<w[i] 当前背包剩余容量不能放下物品i当前背包剩余容量能放下物品i,可以选择放也可以选择不放

public void zeroOneBackpack() {
		int[] w= {2,5,4,2,3};
		int[] v= {6,3,5,4,6};
		int n=w.length;
		int W=10;
		int[][]c=new int[n+1][W+1];
		//初始化第一列为0 背包容量为0-->价值为0
		for(int i=0;i<=n;i++) {
			c[i][0]=0;
		}
		//初始化第一行为0 物品个数为0-->价值为0
		for(int j=0;j<=W;j++) {
			c[0][j]=0;
		}
		for(int i=1;i<=n;i++) {
			for(int j=1;j<=W;j++) {
				if(w[i-1]>j) {//当前物品i太重了 背包容量不足 因为下标从0开始 w[i-1]表示的的第i个物品
					c[i][j]=c[i-1][j];
				}else {
					c[i][j]=Math.max(c[i-1][j],c[i-1][j-w[i-1]]+v[i-1]);
				}
			}
		}
		System.out.println("max value: "+c[n][W]);
		
		//逆向构造最优解
		int[] x=new int[n+1];//x[i]=1说明第i个物品被放入背包中
		int j=W;
		for(int i=n;i>=1;i--) {
			if(c[i][j]>c[i-1][j]) {//c [i][j]>c [i−1][j],则说明第i种物品被放入背包
				x[i]=1;
				j-=w[i-1];
			}else{
				x[i]=0;
			}
		}
		
		System.out.println("放入背包中的物品:");
		for(int i=1;i<=n;i++) {
			if(x[i]==1) {
				System.out.println("v["+(i-1)+"]="+v[i-1]);
			}
		}
	}

算法优化:

当处理第i 种物品时,只需第i -1种物品的处理结果,若不需要构造最优解,则放入第i -1种物品之前的处理结果已经没用了

dp[j ]表示将物品放入容量为j 的背包中可以获得的最大价值, 这里的第二层循环需要倒退W, 正推会导致某个物品被选中不止一次,不满足0-1背包的条件,
在这里插入图片描述

public void zeroOneBackpack() {
		int[] w= {2,5,4,2,3};
		int[] v= {6,3,5,4,6};
		int n=w.length;
		int W=10;
		int[] dp=new int[W+1];
		for(int i=1;i<=n;i++) {
			for(int j=W;j>=w[i-1];j--) {//j>=w[i-1]:当前容量能放下物品i
				dp[j]=Math.max(dp[j], dp[j-w[i-1]]+v[i-1]);
			}
		}
		System.out.println("max value: "+dp[W]);	
	}

2. 完全背包

给定n 种物品,每种物品都有重量 w i w_i wi和价值 v i v_i vi,其数量没有限制。背包容量为W ,求解在不超过背包容量的情况下如何放置物品,使背包中物品的价值之和最大

public void knapsack() {
		int[] w= {2,5,4,2,3};
		int[] v= {6,3,5,4,6};
		int n=w.length;
		int W=10;
		int[][]c=new int[n+1][W+1];
		//初始化第一列为0 背包容量为0-->价值为0
		for(int i=0;i<=n;i++) {
			c[i][0]=0;
		}
		//初始化第一行为0 物品个数为0-->价值为0
		for(int j=0;j<=W;j++) {
			c[0][j]=0;
		}
		for(int i=1;i<=n;i++) {
			for(int j=1;j<=W;j++) {
				for(int k=0;k*w[i-1]<=j;k++) {//第i个物品可能被选中多次 只要不超过背包容量
					c[i][j]=Math.max(c[i][j],c[i-1][j-k*w[i-1]]+k*v[i-1]);
					//c[i-1][j]  不选择当前物品i
					//c[i-1][j-k*w[i-1]]+k*v[i-1] 选择k个当前物品i
					//容量判断在k*w[i-1]<=j体现
				}
					
			}
		}
		System.out.println("max value: "+c[n][W]);
		// 逆向构造最优解
		int[] x=new int[n+1];
		int i=n,j=W;
		while(i>0&&j>0) {
			if(j-w[i-1]>=0&&c[i][j]==c[i][j-w[i-1]]+v[i-1]) {
				j-=w[i-1];//同一种物品可能被加入多次 可以是在同一行 c[i][j]和c[i][j-v]的状态有关
				x[i]++;
			}else {//c[i][j]=c[i-1][j]才能说明背包里面不会含有i
				i--;
			}
		}

		System.out.println("放入背包中的物品:");
		for (i = 1; i <= n; i++) {
			while(x[i]-->0) {
				System.out.println("v[" + (i - 1) + "]=" + v[i - 1]+"  w[" + (i - 1) + "]=" + w[i - 1]);
			}
		}
	}

和0-1背包优化的思路一样,完全背包也可以使用一维数组进行滚动:

public void knapsack() {
		int[] w= {2,5,4,2,3};
		int[] v= {6,3,5,4,6};
		int n=w.length;
		int W=10;
		int[] dp=new int[W+1];
		for(int i=1;i<=n;i++) {
			for(int j=w[i-1];j<=W;j++) {//j>=w[i-1]:当前容量能放下物品i
				dp[j]=Math.max(dp[j], dp[j-w[i-1]]+v[i-1]);
			}
		}
		System.out.println("max value: "+dp[W]);	
	
		
	}

完全背包也可以转化为0-1背包问题,假设某个物品重量为w, 背包容量为W,则该物品最多出现W/w(向下取整)次,即将一个重量为w物品看成W/w(向下取整)个重量为w的物品,然后按照0-1背包进行处理


3. 多重背包

给定n 种物品,每种物品都有重量 w i w_i wi和价值 v i v_i vi ,每种物品的数量都可以大于1但是有限制。第i 种物品有ci 个。背包容量为W ,求解在不超过背包容量的情况下如何放置物品,可以使背包中物品的价值之和最大。我们可以将多重背包问题通过暴力拆分或二进制拆分转化为01背包问题,也可以通过数组优化对物品数量进行限制

1. 暴力拆分
将第i 种物品看作ci 种独立的物品,每种物品只有一个,转化为01背包问题

public void knapsack() {
		int[] w= {2,5,4,2,3};
		int[] v= {6,3,5,4,6};
		int[] limit= {1,2,3,4,5};
		int n=w.length;
		int W=10;
		int[][]c=new int[n+1][W+1];
		//初始化第一列为0 背包容量为0-->价值为0
		for(int i=0;i<=n;i++) {
			c[i][0]=0;
		}
		//初始化第一行为0 物品个数为0-->价值为0
		for(int j=0;j<=W;j++) {
			c[0][j]=0;
		}
		for(int i=1;i<=n;i++) {
			for(int j=1;j<=W;j++) {
				for(int k=0;k*w[i-1]<=j&&k<=limit[i-1];k++) {//第i个物品:最多被选中limit[i-1]次并且不能超过背包容量
					c[i][j]=Math.max(c[i][j],c[i-1][j-k*w[i-1]]+k*v[i-1]);
					//c[i-1][j]  不选择当前物品i
					//c[i-1][j-k*w[i-1]]+k*v[i-1] 选择k个当前物品i
					//容量判断在k*w[i-1]<=j体现
				}
					
			}
		}
		System.out.println("max value: "+c[n][W]);
	// 逆向构造最优解
		int[] x=new int[n+1];
		int i=n,j=W;
		while(i>0&&j>0) {
			if(j-w[i-1]>=0&&c[i][j]==c[i][j-w[i-1]]+v[i-1]) {
				j-=w[i-1];//同一种物品可能被加入多次 可以是在同一行 c[i][j]和c[i][j-v]的状态有关
				x[i]++;
			}else {//c[i][j]=c[i-1][j]才能说明背包里面不会含有i
				i--;
			}
		}

		System.out.println("放入背包中的物品:");
		for (i = 1; i <= n; i++) {
			while(x[i]-->0) {
				System.out.println("v[" + (i - 1) + "]=" + v[i - 1]+"  w[" + (i - 1) + "]=" + w[i - 1]);
			}
		}
		
	}

一维数组优化:

public void knapsack() {
		int[] w = { 2, 5, 4, 2, 3 };
		int[] v = { 6, 3, 5, 4, 6 };
		int[] limit = { 1, 2, 3, 4, 5 };
		int n = w.length;
		int W = 10;
		int[] dp=new int[W+1];
 		for(int i=1;i<=n;i++) {
			for(int k=1;k<=limit[i-1];k++) {
				for(int j=W;j>=w[i-1];j--) {//j>=w[i-1]:当前容量能放下物品i
					dp[j]=Math.max(dp[j], dp[j-w[i-1]]+v[i-1]);
				}
			}
		}
		System.out.println("max value: "+dp[W]);	

	}

2. 二进制拆分
在这里插入图片描述

public void knapsack() {
		int[] w= {2,5,4,2,3};
		int[] v= {6,3,5,4,6};
		int[] limit= {1,2,3,4,5};
		int n=w.length;
		int W=10;
		int[] dp=new int[W+1];
		for(int i=1;i<=n;i++) {
			if(limit[i-1]*w[i-1]>=W) {//转化为完全背包问题 第i个物品即使使得背包容量满了也不会超出数量限制
				for(int j=w[i-1];j<=W;j++) {
					dp[j]=Math.max(dp[j],dp[j-w[i-1]]+v[i-1]);
				}
			}else {
				for(int k=1;limit[i-1]>0;k<<=1) {
					int x=Math.min(k,limit[i-1]);//k>=limit[i-1] x=1 2 4 ... 否则就是剩余的r
					for(int j=W;j>=w[i-1]*x;j--) {
						dp[j]=Math.max(dp[j],dp[j-w[i-1]*x]+v[i-1]*x);
					}
					limit[i-1]-=x;//本次选中了x个物品i
				}
			}
		}
		System.out.println("max value: "+dp[W]);
	

	}

3. 数组优化

num[j ]数组记录容量为j 时放入了多少个第i 种物品,以满足物品数量限制

public void knapsack() {
		int[] w= {2,5,4,2,3};
		int[] v= {6,3,5,4,6};
		int[] limit= {1,2,3,4,5};
		int n=w.length;
		int W=10;
		int[] dp=new int[W+1];
		for(int i=1;i<=n;i++) {
			int[] num=new int [W+1];
			for(int j=w[i-1];j<=W;j++) {
				if(dp[j]<dp[j-w[i-1]]+v[i-1]&&num[j-w[i-1]]<limit[i-1]) {
					dp[j]=dp[j-w[i-1]]+v[i-1];
					num[j]=num[j-w[i-1]]+1;
				}
			}
		}
		System.out.println("max value: "+dp[W]);
	

	}

4. 分组背包

给定n 组物品,第i 组有 c i c_i ci个物品,第i 组的第j 个物品有重量 w i j w_{ij} wij 和价值 v i j v_{ij} vij ,背包容量为W ,在不超过背包容量的情况下每组最多选择一个物品,求解如何放置物品可使背包中物品的价值之和最大

c [i ][j ]表示将前i 组物品放入容量为j 的背包中可以获得的最大价值

  1. 若不放入第i 组物品,则放入背包的价值不增加,问题转化为“将前i −1组物品放入容量为j 的背包中可以获得的最大价值”,最大价值为c [i -1][j ]

  2. 若放入第i 组的第k 个物品,则相当于从第i -1阶段向第i 阶段转移,问题转化为“将前i −1组物品放入容量为j -w [i ][k ]的背包中可以获得的最大价值”,此时获得的最大价值是c [i -1][j -w [i ][k ]],再加上放入第i 组的第k 个物品获得的价值v [i ][k ],总价值为c [i -1][j -w [i ][k ]]+v [i ][k ]

    c [ i ] [ j ] = { c [ i − 1 ] [ j ] , j<w[i][k] m a x ( c [ i − 1 ] [ j − w [ i ] [ k ] ] + v [ i ] [ k ] , c [ i − 1 ] [ j ] ) , 1 < = k < = l i m i t [ i ] 当前背包剩余容量能放下物品i,可以选择放也可以选择不放 c[i][j]= \begin{cases} c[i-1][j], & \text{j<w[i][k]} \\ max(c[i-1][j-w[i][k]]+v[i][k],c[i-1][j]), 1<=k<=limit[i]& \text{当前背包剩余容量能放下物品i,可以选择放也可以选择不放} \\ \end{cases} c[i][j]={c[i1][j],max(c[i1][jw[i][k]]+v[i][k],c[i1][j]),1<=k<=limit[i]j<w[i][k]当前背包剩余容量能放下物品i,可以选择放也可以选择不放

public void knapsack() {
		int[][] w = { { 1, 2, 3, 4, 5 }, { 3, 5, 6, 1, 7 }, { 1, 7, 9, 2, 4 } };
		int[][] v = { { 1, 2, 2, 3, 1 }, { 4, 1, 3, 5, 1 }, { 1, 3, 2, 1, 3 } };
		int n = w.length;
		int W = 10;

		int[][] c = new int[n + 1][W + 1];
		// 初始化第一列为0 背包容量为0-->价值为0
		for (int i = 0; i <= n; i++) {
			c[i][0] = 0;
		}
		// 初始化第一行为0 物品个数为0-->价值为0
		for (int j = 0; j <= W; j++) {
			c[0][j] = 0;
		}
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= W; j++) {

				for (int k = 1; k <= w[i - 1].length; k++) {
					int tmp;// 使用一个临时变量 防止c[i][j]已经得到最大值后又被较小值覆盖
					if (j < w[i - 1][k - 1]) {
						tmp = c[i - 1][j];
					} else {
						tmp = Math.max(c[i - 1][j], c[i - 1][j - w[i - 1][k - 1]] + v[i - 1][k - 1]);
					}
					if (tmp > c[i][j]) {
						c[i][j] = tmp;
					}
				}
			}
		}
		System.out.println("max value: " + c[n][W]);
		// 逆向构造最优解
		int[][] x = new int[3 + 1][5 + 1];// x[i]=1说明第i个物品被放入背包中
		int j = W;
		for (int i = 3; i >= 1; i--) {
			if (c[i][j] > c[i - 1][j]) {// c [i][j]>c [i−1][j],则说明第i组中有物品被放入背包
				for (int k = 1; k <= w[i - 1].length; k++) {// 找出是第i组中的哪个物品
					if (c[i][j] == c[i - 1][j-w[i - 1][k - 1]] + v[i - 1][k - 1]) {// 第i组的第k个物品被选中
						x[i][k] = 1;
						j -= w[i - 1][k - 1];
						break;
					}
				}
			}
		}

		System.out.println("放入背包中的物品:");
		for (int i = 1; i <= 3; i++) {
			for (j = 1; j <= 5; j++) {
				if (x[i][j] == 1) {
					System.out.println("v[" + (i - 1) + "]=" + v[i - 1][j - 1]);
				}
			}
		}

	}

一维数组优化:

public void knapsack() {
		int[][] w = { { 1, 2, 3, 4, 5 }, { 3, 5, 6, 1, 7 }, { 1, 7, 9, 2, 4 } };
		int[][] v = { { 1, 2, 2, 3, 1 }, { 4, 1, 3, 5, 1 }, { 1, 3, 2, 1, 3 } };
		int n = w.length;
		int W = 10;
		int[]dp=new int[W+1];
		for (int i = 1; i <= n; i++) {
			for(int j=W;j>=0;j--) {
				for(int k=1;k<=w[i-1].length;k++) {
					if(j>=w[i-1][k-1]) {
						dp[j]=Math.max(dp[j],dp[j-w[i-1][k-1]]+v[i-1][k-1]);
					}
				}
			}
		}
		System.out.println("max value: " + dp[W]);

	}

参考:《算法训练营-高级篇》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CodePanda@GPF

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值