经过一周的学习,在高师姐飞一般的速度指导下,我终于见识了DP的强大。
这次先讲背包:
01背包:(ZeroOne_Pack)
概念:有n个物品,重量为w[i],价值为v[i],要从这些物品里挑选出总重量不超过W的物品,求最大价值。
朴素的方法就是搜索,针对每个背包是否可以放和放与不放三种形态,这样时间复杂度是多少呢?,相当于求集合的子集吧,O(2^n),n太大就不行了。从《挑战程序设计》书上可以看出,实际上我们多算了很多,那么这些多算了的我们就可以先把它存起来,以后直接用,这就是记忆化搜索,时间复杂度就变为O(nW)了。
但事实上,我们是通过记忆化搜索来总结出DP的递推式,这是最难的一步,我也学的不好,看着题我也是真心不会推导啊。
递推式:
首先要理解dp[i+1][j]的意思:表示选择前i个物品不超过j重量的最大价值。
然后想一想,顺推得到:
dp[0][j]=0(初始化);
dp[i+1][i]={ dp[i][j] (j < w[i]), max(dp[i][j], dp[i][j - w[i]] + v[i])(else);
实际上很多时候都是递推式的由来就是那句话:针对每个背包是否可以放和放与不放三种形态。
代码如下:一维dp的代码
for(int i=0;i<n;i++)
for(int j=W;j>=w[i];j++)
dp[j]=max(dp[j],dp[j-W[i]]+v[i]);
注意第二个循环,为什么是从后面往前面推,我也说不准,或许是dp后面的值是有前面的值推导而来,所以要从后面往前面推导。
01背包之2 :
解决的是W很大而v[i]相对较小的时候。
递推式:
这类问题是将dp[i+1][j]理解为选择前i种物品,使得价值不超过j的最小重量。时间复杂度就为O(n*sum(vi)(0<=i<=n))
顺推得到:
dp[0][0]=0,dp[0][j]=INF(极大值)(初始化);
dp[i+1][j]=min(dp[i][j],dp[i][j - v[i]] + w[i]) (else);
这个答案就应该注意了,应是令dp[n][j]<=W的最大值 j 。(这个我当时却是没有想到)
代码如下:
for(int i=0;i<n;i++)
for(int j=0;j<=max_n*max_v;j++)
if(j<v[i]) dp[i+1][j]=dp[i][j];
else dp[i+1][j]=min(dp[i][j],dp[i][j-v[i]]+W[i]);
完全背包:(Complete_Pack)
概念:有n个物品,重量为w[i],价值为v[i],这些物品可以取多次,要从这些物品里挑选出总重量不超过W的物品,求最大价值。
相对上一个题来说,就是一个物品可以选多次了,这样的话,就需要判断每种物品选多少个为最佳,就引入一个k来代替个数。
递推式:
dp[i+1][j]表示选取前 i 种物品使得重量不超过j的最大价值。
顺推得到:
dp[0][j]=0(初始化);
dp[i+1][j]={max(dp[i+1][j],max(dp[i][j - k*w[i]] + k* v[i]) (k>=0) ) }
这样便是对这句话“针对这种物品是否可以放和放多少两种形态”的最好诠释。
代码如下:
for(int i=0;i<n;i++)
for(int j=0;j<=W;j++)
for(int k=0;k*w[i]<=j;k++)
dp[i+1][j]=max(dp[i+1][j],dp[i][j-k*w[i]]+k*v[i]);
再想一想,看到代码发现是三重循环,时间复杂度是O(nW^2),那么有更节省复杂度的思想吗?
有的:
//完全背包,推导过程:
max { dp[i][j - k * w[i]] + k * v[i] | (0 <= k) }
=max( dp[i][j], max { dp[i][j - k * w[i]] + k * v[i] | (1 <= k) } ) //要使k的取值范围回到k >= 0,则:
//令 t = k-1,t>=0,k=t+1;
=max( dp[i][j], max { dp[i][j - (t + 1) * w[i]] + (t + 1) * v[i] | (0 <= t) } )
=max( dp[i][j], max { dp[i][(j - w[i]) - t * w[i]] + t * v[i] + v[i] | (0 <= t) } )
=max( dp[i][j], max { dp[i+1][j - w[i]] + v[i] } )//是将前i项算过的递推到i + 1项,实质t * w[i] 已被算过的,原书上写的i + 1,但没怎么看懂,不应该是比较i与i - 1吧
//或许是,既由i 项推导而来,那么就应该与i + 1项做比较。真的不是很懂啊。
由此可知:一个数从小到大以此去另一个数的k倍(k>=0),实则是这个数从小到大以此去减这个数,当然,这个答案应该是递推的,就是说它算过后已经记录下了,后面再用时便是这个道理。
这样时间复杂度就和01背包一样了
那么递推式优化后的代码如下:(一维dp数组)
for(int i=0;i<n;i++)
for(int j=w[i];j<=W;j++)
dp[i][j]=max(dp[i][j],dp[i][j-w[i]]+v[i]);
想想这个为什么是从前面往后推呢?可能是dp是与自身做比较,并且它存在一个多次选择物品的概念,这样推就把多次选择也记录到数组里面了。
多重背包:
概念:有n个物品,重量为w[i],价值为v[i],每个物品a[i]个,要从这些物品里挑选出总重量不超过W的物品,求最大价值。
这个问题相对上一个来说很像,但却又很大差别,虽说可以用上面问题的第一种方法解决,但时间复杂度肯定过不了,因此就有了个技巧。
技巧:将a[i]分成1,2,4,8,16,……,2^(k-1),a[i]-2^(k-1)。
这样有什么效果呢?首先确定一件事,我们一次枚举这些数,是否将a[i]的每个数都枚举到了(这点我也没想通,但我的理解是,但我们在后面枚举其他数时,我们根据dp里的值就可以达到枚举所有数的效果),确定之后,那么时间复杂度是不是降低了,由O(nWa[i])降为O(nWlog(a[i]))。
于是我们的递推式和完全背包的第一种情况一样。
递推式:
dp[i+1][j]表示选取前 i 种物品使得重量不超过j的最大价值。
顺推得到:
dp[0][j]=0(初始化);
dp[i+1][j]={max(dp[i+1][j],max(dp[i][j - k*w[i]] + k* v[i]) (k>=0) ) }
这样也是对这句话“针对这种物品是否可以放和放多少两种形态”的最好诠释。
那么代码如下:
for(int i=0;i<n;i++)
{
if(a[i]*w[i]<W)
complete(w[i],v[i],W);
else
{
int k=1;
while(k<a[i])
{
ZeroOne(k*w[i],k*v[i],W);
a[i]-=k;
k<<=1;
}
ZeroOne(a[i]*w[i],a[i]*v[i],W);
}
}
OK!!!背包问题就此结束,我也还没练过什么题,正准备着呢。。。到时候做了题再来看看有没有要补充的。。。take it easy 。。。