概念简述
通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题(解释来自维基百科)。先对两个关键词进行下简单的概述:
- 重叠子问题,在计算过程遇到的子问题不一定是新产生的,有可能是之前计算过的并且对其进行了报错,当再次遇到相同的子问题时就不用了重复计算直接查看原有结果就可以,从而提高了算法效率。
- 最优子结构,既全局最优解可由局部最优结共同构成,也就是说可以把一个问题拆分成很多小部分,每一个部分都是最优解就可以构成最优解。
上述两个特点直接看会觉的有些抽象,接下来我们可以根据栗子进一步理解。
零一背包
实际上关于动态规划中的背包问题主有3种,包括零一背包、绝对背包、多重背包。后面两种实际上算是零一背包问题的一种延伸。下面我们先来看题目:
设现在有N样物品(每样一个), 每种物品的重量为m[i],价值为v[i]。现有一背包最大承重为W, 问:在不超过背包最大承重的情况下,背包能够装入的最大价值事多少。
先来简单分析下题目:
- 问题的结论是问最大价值也就是说在重量不超过W的前提下其最优解是?
- 假设所有物品装在一个列表中,每样物品对应一个下标,从i=0到i=n;那么在我们依次试图把物品装进背包的过程中有两种情况: 装或者不装。em...看起来像废话, 但这也是 零一背包 的由来,既装入为一,不装为零。
- 通过2的分析,我们已经试图在把问题分解成局部问题,也就是说我们需要分析在每次装或不装入物品时怎么选择才能在当前背包重量下获得最大价值,也就是局部最优解。那么如果每次选择都能的到一个当前重量下的最大价值, 那么结果就是最优解。
通过上面的2和3的简单分析我们已经发现了该问题的主要矛盾以及其主要方面。首先我们知道判断装或不装有两个条件重量和价值, 假设现在背包最大承重为100, 现在有5样物品a,b,c,d,e如下图:
当背包为空时,如果我们把a装入了背包那么此时背包重量为20,背包中的价值为v1的价值5,就是这个样子:
同样我们也可以选择把b,c,d,e放入背包中,那么他们对应重量的价值又各不相同,此时我们可以把a,b,c,d,e分别放入背包时的数据记录下来(子问题重叠):
| 物品 | 背包的重量 | 价值 |
|---|---|---|
| a | 20 | 3 |
| b | 40 | 10 |
| c | 60 | 15 |
| d | 80 | 20 |
| e | 30 | 10 |
现在我们放入第二件物品, 此时我们要从剩下的四种物品中选一个放入背包,与上面不同的是此时我们要考虑放入某物品后是否会超重, 举个栗子当我们试图把重量为40的b放入背包时,那么在放入前背包的总重量就不能超过背包的最大承重 100 - 40 = 60, 此时我们就会筛选掉背包重量超过60的方案(上表中第一次放入物品时的方案)发现d不符合,又因为条件中每样物品只有一个所以b自己不可能重复放入,所以方案如下:
| 物品 | 背包的重量 | 价值 |
|---|---|---|
| ab | 60 | 13 |
| cb | 100 | 25 |
| eb | 70 | 20 |
那么我们应该如何选择最优的方案呢?从上表我们可以看到当我们选择ab这个组合时(也就是说先放a再放b这种情况)此时背包重量为60价值为13,接着我们根据以上两个表发现, 背包重量不超过60的情况有4种,既只放了a,只放了b, 只放了c以及放了ab这四种情况中价值最高的是只放了c这种方案。也就是说,当我们在第一次放入a,第二次选择放入b时要考虑背包总重量同样不超过60的情况下这种方案是否最优,此时我们发现第一次放入c时背包中的价值为15,如果选择ab这种方案背包中的价值反而更小了, 因此我们我们很高兴的放弃了这种方案。 并记录下到第二次放物品为止,背包重量不超过60时的最大价值是15。此时我们已经列举了第二次放入的物品是b时会发生的情况。
接着明我们依次尝试剩下的物品放入背包的可能。并且会如上述过程一样记录下不同重量下的最大价值,例如重量不超过60时最大价值为15,重量不超过80时的最大价值为(abe这个组合)23 ...依次类推直到找到重量不超过100的最大价值此时就可以结束。到这里你会发现这其实是对所有可能进行枚举的过程, 并在枚举过程中记录下当前的最优解, 以便在下次遇到相同条件的情况时进行比较选出更优解。
代码实现
/*
* @param{ numbe } B 背包总重量
* @param{ array } c 物品重量
* @param{ array } v 物品价值
*/
function oneZeroBag(B,c,v) {
//物品数量
const n = c.length;
if(n === 0 || B === 0) return;
class item {
constractor(c,v) {
this.m = c;
this.v = v;
}
}
let itemList = [];
for(let i = 1; i <= n; i++) {
itemList.push(new item(c[i], v[i]));
}
const bags = [];
//对背包初始化
for(let i = 0; i <= B; i++) {
bags[i] = 0;
}
//遍历每种物品
for(let i = 1; i <= n; i++) {
const mi = itemList[i].m;
const vi = itemList[i].v;
//对背包中各种重量下的状态进行遍历
//这里是从后向前遍历
for(let j = B; j >= mi; j--) {
//判断是否放入当前物品并记录下在不超过同一重量下的最大的价值
//局部状态转移到全局
bags[j] = Math.max(bags[j],bags[j-mi] + vi);
}
}
//结果
return bags[B]
}
如果嫌啰嗦可以从//对背包初始化往下看, 要注意for(let j = B; j >= mi; j--)是从后想前遍历, 这段代码的意思是查看放入第i件物品后背包的重量可能会变为100, 99, 98,...,mi当mi === bags[j]时证明当前物品是第一个放进背包的。接着把所有的可能跟之前我们记录的相同重量下的价值进行比较,由于这里是0-1背包每种物品只有一个,也就是说之前放了现在就没有了, 从前向后遍历会出现在之前已经把物品放入背包现在又放入的情况。
结束语
在零一背包求最大价值过程中,我们把问题拆分成了每一次放入物品时求最优解,这体现了最优子结构的特性。并且我们在试图找到最优子结构的过程中会用当前状态和之前记录过的状态相比较从而得出最优解,这说明了重叠子问题的特性。
到这里整个过程就梳理完了, 第一次写如果发现理解有偏差或者错误欢迎大家指正^_^。