动态规划之0
《从0到1:开启事业的第一步》- 彼得·蒂尔教你创业规划 #生活技巧# #工作学习技巧# #职业生涯规划书籍#
开始背包问题讲解前,我们先粗略的了解一下动态规划。动态规划,无非就是利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存。
一、0-1背包问题给定n个重量为w1, w2, w3, … , wn, 价值为v1, v2, v3, … , vn 的物品和容量为C的背包,求这个物品中一个最有价值的子集,使得在满足背包的容量的前提下,包内的总价值最大。
0-1背包问题指的是每个物品只能使用一次。
动态规划:
第一步骤:定义数组元素的含义。开头说了,我们会用一个数组来储存历史数据,假设为dp[],然后我们要确定dp[i]代表什么意思。一般题目需要求解什么,便将其设为dp[]。
第二步骤:找出要素之间的关系,即确定转移方程。找dp[i], dp[i-1]…之间的关系,或者对于二维数组,找dp[i][j], dp[i-1][j], dp[i-1][j-1], dp[i][j-1]等等之间的关系。
第三步骤:找出初始值,即不能再分解的dp[i]。
背包问题求解思路:
第一步骤:设dp[i][j],i代表N种物体中的第i个,j代表此时背包最多负重为j,dp[i][j]代表在只能装前i种物体和包的容量为j的前提下,包内最大的总价值。
第二步骤:确定状态转移方程。
0-1背包中一个状态就是N个物体中第i个是否放入体积为j背包中,由此确定转移方程。
if (背包体积j小于物品i的体积) //背包装不下第i个物体,目前只能靠前i-1个物体装包,因为j<val[i], 所以即使包中只装物品i也装不下。 f[i][j] = f[i-1][j] else //f[i-1][j]不装物品i,f[i-1][j-Vi]装物品i,j-Vi意思是包中已经装了j-Vi,在装Vi,而且显然j-Vi>=0~(j-Vi-1)所包含的价值(递增) f[i][j] = max(f[i-1][j], f[i-1][j-Vi] + Wi) 123456
第三步骤:确定初始状态。初始状态为f[0][0−V]和f[0−N][0]都为0,前者表示前0个物品(也就是空物品)无论装入多大的包中总价值都为0,后者表示体积为0的背包啥价值的物品都装不进去。
三、程序#include<iostream> using namespace std; int main() { int nArr[6][13] = {{0}}; //全部初始化为0 int nCost[6] = {0 , 2 , 5 , 3 , 10 , 4}; //花费 int nVol[6] = {0 , 1 , 3 , 2 , 6 , 2}; //物体体积 int bagV = 12; //在只装前i种物品前提下,求重量为j的包所含有的最大价值。 for( int i = 1; i< sizeof(nCost)/sizeof(int); i++) { for( int j = 1; j<=bagV; j++) { if(j<nVol[i]) nArr[i][j] = nArr[i-1][j]; else nArr[i][j] = max(nArr[i-1][j] , nArr[i-1][j-nVol[i]] + nCost[i]); cout<<nArr[i][j]<<' '; } cout<<endl; } cout<<nArr[5][12]<<endl; return 0; }
123456789101112131415161718192021222324252601背包问题其实就可以化简为涂写下面的表格,其中每个数对应数组nArr中每个元素,初始化部分为0,然后从左上角按行求解,一直求解到右下角获取最终解nArr[5][12]。
图来自最通俗易懂的01背包问题讲解
从上述代码可知我们的空间复杂度为O(mn),我们可以看到状态转移方程右边部分,dp[i][j]貌似只与上一行(i-1)有关,而其余行我们根本没有必要去记录,因此我们只需要一个一维数组就行了,这样空间复杂度也就降到了O(n)。
若不懂,在这篇知乎上有个很详细讲解告别动态规划,连刷 40 道题,我总结了这些套路,看不懂你打我(万字长文)
我们先上优化后的代码:
#include<iostream> using namespace std; int main() { int nArr[6][13] = {{0}}; int nCost[6] = {0 , 2 , 5 , 3 , 10 , 4}; //花费 int nVol[6] = {0 , 1 , 3 , 2 , 6 , 2}; //物体体积 int bagV = 12; for( int i = 1; i< sizeof(nCost)/sizeof(int); i++) { for( int j = bagV; j>=nVol[i]; j--) { nArr[j] = max(nArr[j] , nArr[j-nVol[i]] + nCost[i]); cout<<nArr[i][j]<<' '; } cout<<endl; } cout<<nArr[5][12]<<endl; return 0; }
1234567891011121314151617181920212223看完代码我们会很疑惑,为什么第二层for循环是倒序?
先引用网上的一小段话:
f[i][v]只与f[i-1][v]和f[i-1][v-C[i]]有关,即只和i-1时刻状态有关,所以我们只需要用一维数组f[]来保存i-1时的状态f[]。
假设i-1时刻的f[]为{a0,a1,a2,…,av},难么i时刻的f[]中第v个应该为max(av,av-C[i]+W[i])即max(f[v],f[v-C[i]]+W[i]),
这就需要我们遍历V时逆序遍历,这样才能保证求i时刻f[v]时f[v-C[i]]是i-1时刻的值。如果正序遍历则当求f[v]时
其前面的f[0],f[1],…,f[v-1]都已经改变过,里面存的都不是i-1时刻的值,这样求f[v]时利用f[v-C[i]]必定是错的值。最后f[V]即为最大价值
我们引用01背包问题 总结关于为什么01背包优化成1维数组后,内层循环是逆序的?的一个例子:
设有3件物品 ,背包能容纳的总重量为10
i=1,2,3
物品号 重量© 价值(w)
i=1 4 5
i=2 7 9
i=3 5 6
f[v]=max{ f[v],f[v-c[i]]+w[i] }
如果v是顺序递增 i=1时,v=4~10 (因为v要至少大于等于c[i]嘛 不然减出个负数没意义)
原先的: f[0]=0 f[1]=0 f[2]=0 f[3]=0 f[4]=0 f[5]=0 f[6]=0 f[7]=0 f[8]=0 f[9]=0 f[10]=0
--------------------------- i=1 --------------------------------
后来的: f[0]=0 f[1]=0 f[2]=0 f[3]=0 f[4]=5 f[5]=5 f[6]=5 f[7]=5 f[8]=10 f[9]=10 f[10]=10
v=4:
f[4]=max{f[4],f[0]+5} max{0,5}=5 f[4]=5
v=5:
f[5]=max{f[5],f[1]+5} max{0,5}=5 f[5]=5
v=6:
f[6]=max{f[6],f[2]+5} max{0,5}=5 f[6]=5
v=7:
f[7]=max{f[7],f[3]+5} max{0,5}=5 f[7]=5
v=8:
f[8]=max{f[8],f[4]+5} max{0,10}=10 f[8]=10 (这里显然不对,这时i=1,只能放一件物品,然而没有一个物品的价值为10的 )
v=9:
f[9]=max{f[9],f[5]+5} max(0,10}=10 f[9]=10
v=10:
f[10]=max{f[10],f[6]+5} max{0,10}=10 f[10]=10
从上面例子中我们可能就会明白其中道理了,如果我们正序遍历,在i=1时候如果背包的容量>=2倍的第一个物体的重量,那么又会被装一遍,这与0-1背包只能装一次的原则相悖,究其原因是f[i-1]不是i-1时刻的值了,而是i时刻的值,因此需要倒序,这样才不能改变i-1时刻的值。而未优化的O(mn)方法,则不需要理会这个,因为i-1时刻的值已经被保存在二维数组里了。
其实顺序可以解决另一种问题,也是背包问题,但不是0-1背包问题,也就是每种物品可以被取无限次。
洛谷上的一道题:P1616 疯狂的采药
网址:动态规划之0 https://www.yuejiaxmz.com/news/view/333081
相关内容
动态规划的应用动态规划之最大子段和问题
算法动态规划01背包问题
旅行商问题(动态规划方法,超级详细的)
动态规划问题dp问题以及经典问题
动态规划思想的实际应用
RL笔记:动态规划(1): 策略估计和策略提升
动态规划算法在生活中的应用
动态规划在实际生活中的应用
递归和动态规划(python)