DP经典回顾:背包问题

在学习动态规划,但是老是把控不住状态的定义和状态方程的转移,因此复习下背包问题,希望能有所提升。先记录下常用的几种类型吧,后面再慢慢记录学习。


01背包

  1. 问题描述

有 N 件物品和一个容量为 V 的背包。放入第 i 件物品耗费的费用是 Ci,得到的价值是 Wi。求解将哪些物品装入背包可使价值总和最大。

  1. 问题分析

01背包的特点是:每种物品仅有一件,可以选择放或不放。

  • 定义状态:dp[i, v] 表示前 i 件物品恰放入一个容量为 v 的背包可以获得的最大价值。
  • 状态转移方程:(非常重要,其他变种背包都是从此方程衍伸出的)
    dp[i][j]=max(dp[i-1][j],dp[i-1][j-Ci]+Wi)

怎么理解这个方程呢?首先是将问题分解为子问题,就是若只考虑第 i 件物品的策略(放或不放),那么可以转化为一个只和前i−1件物品相关的问题。如果不放第 i 件物品,问题就转化为“前i−1件物品放入容量为v的背包中”,所以其价值为 dp[i−1, v];如果放第 i 件物品,那么问题就转化为“前i−1件物品放入剩下的容量为v−Ci的背包中”,此时能获得的最大价值就是 dp[i−1, v−Ci]+Wi。

代码如下:

static int m1_bag01(int v,int[] c,int[] w) {
		int dp[][]=new int[c.length+1][v+1];
		int N=c.length+1;
		int V=v+1;
		for (int i = 1; i < N; i++) {			
			for (int j = 0; j < V; j++) {
				//大的放不下,所以直接取上一次的结果
				if(j-c[i-1]<0)
					dp[i][j]=dp[i-1][j];
				else
					dp[i][j]=Math.max(dp[i-1][j], dp[i-1][j-c[i-1]]+w[i-1]);
			}
		}		
		return dp[c.length][v];
	}
  1. 空间复杂度优化

可以发现,上述代码的空间复杂度有点大,O(N*V)级别,如果给定背包的体积很大就容易超内存。如果只用一个数组 dp[0 . . . v ],就要保证第 i次循环结束后dp[v] 中表示的就是定义状态 dp[i][v] 。这里可以在每次主循环(从1遍历到v)中以递减顺序计算 dp[v],这样在计算 dp[v] 时使用的dp[v − Ci] 就是上一次循环保存的状态,即dp[i−1, v−Ci] 的值。

代码如下:

static int m2_bag01(int v,int[] c,int[] w) {
		int dp[]=new int[v+1];
		int N=c.length;
		for (int i = 0; i < N; i++) {
		//倒序遍历,确保计算dp[j]时,dp[j-c[i]]存储的是上一次即i-1的结果
			for (int j = v; j >=c[i]; j--) {
				dp[j]=Math.max(dp[j], dp[j-c[i]]+w[i]);
			}
		}		
		return dp[v];
	}

由于在后面的变种背包经常用到01背包的问题,因此定义一个方法,表示处理一件物品的01背包的过程:

//定义处理一个物品的01背包方法
//@参数说明:dp-状态数组,c-当前物品体积,w-当前物品价值,v-背包体积
//@返回:更新后的状态数组
	static int[] one_bag01(int[] dp,int c,int w,int v) {
		for (int j = v; j >=c; j--) {
			dp[j]=Math.max(dp[j], dp[j-c]+w);
		}
		return dp;
	}

所以01背包代码改写如下:

static int bag01(int v,int[] c,int[] w) {
		int dp[]=new int[v+1];
		int N=c.length;
		for (int i = 0; i < N; i++) 
			dp=one_bag01(dp, c[i], w[i], v);		
		return dp[v];
	}


完全背包

  1. 问题描述

有 N 种物品和一个容量为 V 的背包,每种物品都有无限件可用。放入第 i 种物品的费用是 Ci,价值是 Wi。求:将哪些物品装入背包,可使这些物品的耗费的费用总和不超过背包容量,且价值总和最大。

  1. 问题分析

转化为01背包
可以这样思考,因为第 i 种物品最多选 V/Ci件,于是可以把第 i 种物品重复V/Ci次加入到物品队列重,然后求解这个新物品队列的01背包问题。

  • 状态定义和之前一样:
    dp[i, v] 表示前 i 件物品恰放入一个容量为 v 的背包可以获得的最大价值。
  • 所以状态方程如下:
    dp[i][j]=max(dp[i-1][j-kCi]+k*Wi),其中0<=kCi<=v

总的复杂度是 O(NV Σ(V/Ci) ),还是可以优化的。

  1. 算法优化

上述状态方程其实等价为:dp[i][j]=max(dp[i-1][j],dp[i][j-Ci]+Wi),用类似01背包的空间优化方法,可以用一维数组表示。代码也非常类似,就是把逆序遍历改为正序遍历即可。先看代码,如下:

static int fullbag(int v,int[] c,int[] w) {
	int dp[]=new int[v+1];
	int N=c.length;
	for (int i = 0; i < N; i++) {
		//正序遍历,确保物品可以重复选
		for (int j = c[i]; j <V; j++) {
			dp[j]=Math.max(dp[j], dp[j-c[i]]+w[i]);
		}
	}		
	return dp[v];
}	

为什么正序遍历就是完全背包了呢?01背包逆序遍历说白了是为了保证每件物品只选一次,因为在选第i件物品用到的之前的状态dp[v − Ci]是没更新过的(或者说是在上一个状态更新的),说明其是没选过第i件物品。而完全背包的特点恰是每种物品可选无限件,所以在加选一件第 i 种物品时,正需要一个可能已选入第 i 种物品的子结果dp[i, v − Ci],所以就可以并且必须采用j以v递增的顺序循环。

同样在后面的变种背包经常用到完全背包的问题,因此定义一个方法,表示处理一件物品完全背包的过程:

//定义处理一个物品的完全背包方法
//@参数说明:dp-状态数组,c-当前物品体积,w-当前物品价值,v-背包体积
//@返回:更新后的状态数组
static int[] one_fullbag(int[] dp,int c,int w,int v) {
	for (int j = c; j<=v; j++) {
		dp[j]=Math.max(dp[j], dp[j-c]+w);
	}
	return dp;
}

因此完全背包代码更新如下:

static int fullbag(int v,int[] c,int[] w) {
	int dp[]=new int[v+1];
	int N=c.length;
	for (int i = 0; i < N; i++) 
		dp=one_fullbag(dp, c[i], w[i], v);
	return dp[v];
}


多重背包

  1. 问题描述

有 N 种物品和一个容量为V的背包。第 i 种物品最多有 Mi 件可用,每件耗费的空间是 Ci,价值是 Wi。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。

  1. 问题分析

首先是一般思路,转化为01背包。其实这个状态方程和完全背包的一样,只不过每件物品的个数有限制而已。

  • 状态转移方程:
    dp[i][j]=max(dp[i-1][j-kCi]+k*Wi),其中0<=k<=mi

换个思路,多重背包其实可以认为是01背包和完全背包的结合,因为当物品 i 的Ci*Mi比v大时,处理这件物品就是个完全背包的过程,如比v小,那就可以转化为k件物品 i 的01背包问题。那么解题就很清晰了。

  1. 代码优化

虽然换了个思路,但是处理起来的时间复杂度还是有点大,这里采用一个二进制优化的思路:
将第 i 种物品分成若干件 01 背包中的物品,其中每件物品有一个系数。这件物品的费用和价值均是原来的费用和价值乘以这个系数。令这些系数分别为1, 2, 22. . . 2k−1, Mi − 2k + 1,且 k 是满足 Mi − 2k + 1 > 0 的最大整数。例如,如果 Mi 为 13,则相应的 k = 3,这种最多取 13 件的物品应被分成系数分别为 1, 2, 4, 6 的四件物品。分成的这几件物品的系数和为 Mi,表明不可能取多于 Mi 件的第 i 种物品。这种方法也能保证对于 0 . . . Mi 间的每一个整数,均可以用若干个系数的和表示。
通过上诉方法,可以将处理每件物品的时间复杂度降低为O(v*logMi)的级别

代码如下:

//v-背包容量,c-物品体积,w-物品价值,m-每个物品最多使用次数
static int mulbag(int v,int[] c,int[] w,int[] m) {	
	int dp[]=new int[v+1];
	int N=c.length;
	//处理N件物品
	for (int i = 0; i < N; i++) {
		//对于当前物品
		//如果c[i]*m[i],转化为完全背包问题
		if(c[i]*m[i]>v) {
			dp=one_fullbag(dp, c[i], w[i], v);
		}
		//否则转化为01背包
		else {
			int k=1;
			//采用二进制思想分解
			while (k<m[i]) {					
				dp=one_bag01(dp, k*c[i], k*w[i], v);
				m[i]-=k;
				k=k*k;
			}
			dp=one_bag01(dp, m[i]*c[i], m[i]*w[i], v);
		}
	}		
	return dp[v];
}	

待续…

参考资料

https://www.cnblogs.com/-guz/p/9866118.html
https://github.com/tianyicui/pack

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Hilbob

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

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

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

打赏作者

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

抵扣说明:

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

余额充值