通过前面三篇动态规划文章的介绍,相信你们对动态规划、分治、贪心有了充分的理解,对动态规划的 3 个核心问题、其本质也有了了解。java
纸上得来终觉浅,绝知此事要躬行。面试
那么今天开始咱们来聊聊具体的那些面试时常考的题目。算法
(尚未看过前三篇文章的同窗齐姐叫你补课啦~)数组
(一):初识动态规划spa
月黑风高的夜晚,张三开启了法外狂徒模式:他背着一个可装载重量为 W
的背包去地主家偷东西。get
地主家有 N
个物品,每一个物品有重量和价值两个属性,其中第 i
个物品的重量为 wt[i]
,价值为 val[i]
。it
问张三如今用这个背包装物品,最多能装的价值是多少?io
举例:
N = 3 //地主家有三样东西
wt = [2,1,3] //每样东西的重量
val = [4,2,3] //每样东西的价值
W = 4 //背包可装载重量
算法应该返回 6.
由于选择第一件物品和第二件物品,在重量没有超出背包容量下,所选价值最大。
若是每种物品只能选 0 个或 1 个(即要么将此物品装进包里要么不装),则此问题称为 0-1 背包问题;若是不限每种物品的数量,则称为无界(或彻底)背包问题。
今天这篇文章咱们只关注 0-1 背包问题,下一篇文章再聊彻底背包问题。
那咱们是如何选择要装入的物品的?
首先,质量很大价值很小的物品咱们先不考虑(放着地主家金银财宝珍珠首饰不偷,背出来一包煤...,那也就基本告别盗窃行业了...)
而后呢?再考虑质量大价值也大的?仍是质量较小价值也稍小的?
咱们天然而然想到:装价值/质量 比值最大的,由于这至少能说明,此物品的“价质比”最大(也即贪心算法,每次选择当前最优)
那么这样装能保证最后装入背包里的价值最优吗?
咱们先来看一个例子:
假设有 5 个物品,N = 5,每种物品的质量与价值以下:
W : 20, 30, 40, 50, 60
V : 20, 30, 44, 55, 60
V/W: 1, 1, 1.1, 1.1, 1
背包容量为 100
若是按上述策略:优先选“价质比”最大的:即第三个和第四个物品
此时质量:40+50=90
价值:44+55 =99
但咱们知道,此题更优的选择策略是:选第一个,第二个和第四个
此时质量:20+30+50=100
价值:20+30+55=105
因此,咱们的“价质比”这种贪心策略显然不是最优策略。
读过一文学懂动态规划这篇文章的读者会发现,以前文章中兑换零钱例子咱们最开始也是采起贪心策略,但最后发现贪心不是最优解,由此咱们引出了动态规划。
没错,今天这题也正是动态规划又一经典的应用。
根据动以前的文章咱们知道,动态规划的核心即:状态与状态转移方程。
那么此题的状态是什么呢?
何为状态?
说白了,状态就是已知条件。
重读题意咱们发现:此题的已知条件只有两个:
题目要求的是在知足背包容量前提下,可装入的最大价值。
那么咱们能够根据上述状态定义出 dp 数组,即:
dp[i][w]
表示:对于前i
个物品,当前背包的容量为w
,这种状况下能够装的最大价值是dp[i][w]
咱们天然而然的考虑到以下特殊状况:
当 i = 0 或 w = 0,那么:
dp0 = dp... = 0
解释:
对前 0 个物品而言,不管背包容量等于多少,装入的价值为 0;当背包容量为 0 时,不管装入前多少个物品(由于一个都装不进去),背包里的价值依旧为 0。
根据这个定义,咱们求的最终答案就是dp[N][W]
咱们如今找出了状态,并找到了 base case,那么状态之间该如何转移呢(状态转移方程)?
dpi 表示:对于前i
个物品,当前背包的容量为w
,这种状况下能够装的最大价值是dp[i][w]
。
思考:对于当前第 i 个物品:
它应该等于下面二者里的较大值:
上述两个若是能够写成如下代码:
//若是第i个物品质量大于当前背包容量 if (wt[i] > W) { dp[i][W] = dp[i-1][W]; //继承上一个结果 } else { //在“上一个结果价值”和“把当前第i个物品装入背包里所获得价值”两者里选价值较大的 dp[i][W] = Math.max(dp[i-1][W],dp[i-1][W-wt[i]] + val[i]) }
咱们接来下再用一个具体的例子,来理解状态和状态转移方程。
如今咱们有 4 个物品,物品对应的价值与质量分别如上图左侧所示:
6, 4
2,5
1, 4
8, 1
咱们首先初始化一行和一列 0,分别对应dp0 和 dpi。
那么第一个问号处应该填什么呢?
咱们根据上述表述的状态转移关系来判断:
当前第一个物品的重量 4 > 背包容量,故装不进去,因此继承上一个结果。
上一个结果是什么呢?
就是第 i - 1个物品,也就是第 0 个,和W = 1时的价值:
if (wt[i] > W) { dp[i][W] = dp[i-1][W]; //继承上一个结果 }
此时方框里的值为 0,故第一个问号这里应该填 0
如今咱们走到了当背包容量 W = 2 的时候,此时当前 i (依旧第一个物品)可否装进背包里呢?
咱们发现 4 > 2,此时仍是装不进去,那么一样继承上一个结果。
上一个结果是 i 不变(依旧是第 0 个物品),W = 2,因此结果依旧为 0。
如今来到 W = 3,发现依旧装不进去,因此填 0。
下一步到 W = 4 这里了,
此时物品重量 4 = 4(背包容量),能够装里,那么按照以前状态转移关系应该是:
else { //在“上一个结果价值”和“把当前第i个物品装入背包里所获得价值”两者里选价值较大的 dp[i][W] = Math.max(dp[i-1][W],dp[i-1][W-wt[i]] + val[i]) }
Option A:
Option B:
此时第一个物品的重量为 4,背包容量为 4,
故要想装入重量为 4 的此物品,那么背包先前的容量必须为当前背包容量 - 当前物品容量:4 - 4 = 0。
咱们随即找到在没装入此物品(重量为 4,价值为 6)以前的dp[i -1]W - wt[i]] = dp0 = 0
那么dp[i -1]W - wt[i]] + val [i] = 0 + 6 = 6
6 和 0 选择一个最大值,因此这里问号处应填入6
下一步咱们来到 W = 5 这里,此时依旧是第一个物品,质量 4 < 5(背包容量),咱们能够装里边。
而后咱们在
Option A:
Option B:
此时第一个物品的重量为 4,背包容量为 5
故要想装入重量为 4 的此物品,那么背包先前的容量必须为:当前背包容量 - 当前物品容量:5 - 4 = 1 ,
咱们随即找到在没装入此物品(重量为 4,价值为 6)以前的dp[i - 1]W - wt[i]] = dp0 = 0
那么dp[i -1]W - wt[i]] + val [i] = 0 + 6 = 6
选择一个最大值,即 6,因此此处应该填入 6
咱们根据以上状态转系关系,依次能够填出空格其它值,最后咱们获得整个 dp 数组:
V | W | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
6 | 4 | 0 | 0 | 0 | 0 | 6 | 6 | 6 |
2 | 5 | 0 | 0 | 0 | 0 | 6 | 6 | 6 |
1 | 4 | 0 | 0 | 0 | 0 | 6 | 6 | 6 |
8 | 1 | 0 | 8 | 8 | 8 | 8 | 14 | 14 |
最后的 dp4:考虑前四个物品,背包容量为 6 的状况下,可装入的最大价值,即为所求。
(注意:咱们在这里求的是 0-1 背包问题,即某一个物品只能选择 0 个或 1 个,不能多选!)
根据以上思路,咱们很容易写出代码:
两层 for 循环
for(int i = 1;i <=N;i++){ ... }
而后写入状态转移方程
for(int j = 0;j <= W;j++){ //外层循环i,若是第i个物品质量大于当前背包容量 if (wt[i] > W) { dp[i][W] = dp[i-1][W]; //继承上一个结果 } else { //在“上一个结果价值”和“把当前第i个物品装入背包里所获得价值”两者里选价值较大的 dp[i][W] = Math.max(dp[i-1][W],dp[i-1][W-wt[i]] + val[i]) } }
由此咱们给出完整代码:
class solution{ public int knapsackProblem(int[] wt,int[] val,int size){ //定义dp数组 int[][] dp = new int[wt.length][size]; //对于装入前0个物品而言,dp数组储存的总价值初始化为0 for(int i = 0;i < size;i++){ int[0][i] = 0; } //对于背包容量W=0时,装入背包的总价值初始化为0 for(int j = 0;j < size;j++){ int[j][0] = 0; } //外层循环遍历物品 for(int i = 1;i <= N;i++){ //内层循环遍历1~W(背包容量) for(int j = 0;j <= W;j++){ //外层循环i,若是第i个物品质量大于当前背包容量 if (wt[i] > W) { dp[i][W] = dp[i-1][W]; //继承上一个结果 } else { //在“上一个结果价值”和“把当前第i个物品装入背包里所获得价值”两者里选价值较大的 dp[i][W] = Math.max(dp[i-1][W],dp[i-1][W-wt[i]] + val[i]) } } } } }
只要咱们定义好了状态(dp 数组的定义),理清了状态之间是如何转移的,最后的代码水到渠成。
本文所说的这个 0-1 背包问题,Leetcode 上并无这个原题,因此对于背包问题,最重要的是它的变种。
背包问题是一大类问题的统称,很大一部分动态规划的题深层剖析均可以转换为背包问题。
因此还须要理解体会背包问题的核心思想,再将此种思想运用到其它一类背包问题的问题上。
那么背包问题还有哪些变化呢?咱们下期见~