【动态规划1】01背包,完全背包与多重背包

1. 01背包

01背包就是指背包里面每种物品只能用一次,在有限的空间内求装入物品的最大价值的问题。

1. 题面:

有一容量为m的背包。现在要从n件物品中选取若干装入背包中,每件物品i的重量为w[i],价值为p[i],定义一种可行的背包装载为:背包中物品的总重量不能超过背包的容量,并且一个物品要么全部选取,要么不选取。定义最佳装载是指所装入的物品价值最高,并且是可行的背包装载。

输入输出:
样例输入                         样例输出
11 (背包容量)                  23(总价值)
4(物品数量)
2 4 6 7(物品重量)
6 10 12 13(物品价值)

2. 递归解法

考虑到这个题目,用贪心算法显然是鼠目寸光的。所以思考其他思路。自顶向下地看这个题目,对于第n件物品而言,无非有两个状态(选择):取或者不取。如果取,在获得这件物品价值地同时,也要承担其重量,如果不取,就无需承担,当然也得不到它的价值。满足无后效性。那么,这种问题就可以递归地求解了。

//返回第n件物品的最大价值,weight为背包剩余容量
  	int dfs(int n,int weight) {
	if(n==0||weight==0) return 0;//递归终止条件
  		int a = dfs(n-1,weight]);//不取,直接返回n-1的状态,不消耗weight
		int b = dfs(n-1,weight-w[n])+v[n];//取,消耗掉w[n]空间的同时获得了v[n]的价值
		return a>b?a:b;//返回最大值
	}

3. 优化递归:记忆化搜索

对于上述递归解法,每求一次dfs(n)就要用到很多次dfs(2),dfs(3)等,而这些必须用一次算一次,增加了时间复杂度。所以,我们考虑使用一个数组把他们存起来,即记忆化搜索,又叫备忘录方法,这样就可以减小时间复杂度。

int dp[100005];
int dfs(int n,int weight) {
	if(n==0||weight==0) return 0;//递归终止条件
	if(dp[n]!=-1) return dp[n];
  	int a = dfs(n-1,weight]);//不取,直接返回n-1的状态,不消耗weight
	int b = dfs(n-1,weight-w[n])+v[n];//取,消耗掉w[n]空间的同时获得了v[n]的价值
	if(a>b) {
		dp[n] = a;
	}else dp[n] = b;
	return dp[n];//返回最大值
}

4.动态规划

对于这类无后效性,备忘录的问题,使用动态规划显然是最佳解法。对于这个题,我们使用f[i][j]作为取第i件物品时容量为j的最大利润,即可推导出状态转移方程:
f[i][j] = max(f[i-1][j],f[i-1][j-w[i]]+v[i])
由此即可写出基本代码:

for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (w[i] > j) {
                f[i][j] = f[i - 1][j];
            }
            else {
                f[i][j] = f[i - 1][j] > f[i - 1][j - w[i]] + v[i] ? f[i - 1][j] : f[i - 1][j - w[i]] + v[i];
            }
        }
   }

5.滚动数组

在这个过程中,我们发现所要的仅仅是f[n][m],即n个物品容量为最大容量m时的情况,而前面n-1的数组迭代都是无意义的。因此,可以用一维数组代替二维数组,进一步优化空间复杂度。此时的状态转移方程如下:
f[j] = max(f[j],f[j-w[i]]+v[i])
此时得到的代码:

for (int i = 1; i <= n; i++) {
        for (int j = m; j >=w[i]; j--) {
           f[j] = max(f[j],f[j-w[i]]+v[i]);
        }
   }

这里代码中的第二行使用了由后向前的遍历方式,而完全背包问题则恰好相反,是由前到后的遍历方式。举个例子,若 j=4:
f[4] = max(f[4],f[4-w[i]]+v[i]);
因为是由后到前,我们再看遍历到j=2时的情况:
f[2] = max(f[2],f[2-w[i])+v[i]);
若w[i]=2,初始化之后,因为f被置为0,先算w[4]时w[2]是为0的,这样就避免了f[2]被多次相加的问题。反之,若是完全背包问题,先计算f[2],f[2]的值已经不为0,此时计算f[4] = max(f[4],f[2]+v[i]);这样j每翻一倍,f[4]就也会翻一倍,符合完全背包问题的要求。

最简单的例子如洛谷采药,疯狂的采药,又如蓝桥杯2018年的分包子问题,2021年的天平问题等。

posted @ 2022-09-20 15:14  PARADOXS  阅读(94)  评论(0编辑  收藏  举报