问题雏形
01背包题目的雏形是:
有N件物品和一个容量为V的背包。第i件物品的体积是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
从这个题目中可以看出,01背包的特点就是:每种物品仅有一件,可以选择放或不放。
其状态转移方程是:
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
对于这方方程其实并不难理解,方程之中,现在需要放置的是第i件物品,这件物品的体积是c[i],价值是w[i],因此f[i-1][v]代表的就是不将这件物品放入背包,而f[i-1][v-c[i]]+w[i]则是代表将第i件放入背包之后的总价值,比较两者的价值,得出最大的价值存入现在的背包之中。
理解了这个方程后,将方程代入实际题目的应用之中,可得
for (i = 1i <= ni++)
for (j = vj >= c[i]j--)//在这里,背包放入物品后,容量不断的减少,直到再也放不进了
f[i][j] = max(f[i - 1][j], f[i - 1][j - c[i]] + w[i])
问题描述
求出获得最大价值的方案。
注意:在本题中,所有的体积值均为整数。
算法分析
对于背包问题,通常的处理方法是搜索。
用递归来完成搜索,算法设计如下:
int make(int i, int j)//处理到第i件物品,剩余的空间为j 初始时i=m , j=背包总容量
{
if (i == 0)return 0
if (j >= c[i])//(背包剩余空间可以放下物品 i )
{
int r1 = make(i - 1, j - w[i])//第i件物品放入所能得到的价值
int r2 = make(i - 1, j)//第i件物品不放所能得到的价值
return min(r1, r2)
}
return make(i - 1, j)//放不下物品 i
}
这个算法的时间复杂度是O(n^2),我们可以做一些简单的优化。
由于本题中的所有物品的体积均为整数,经过几次的选择后背包的剩余空间可能会相等,在搜索中会重复计算这些结点,所以,如果我们把搜索过程中计算过的结点的值记录下来,以保证不重复计算的话,速度就会提高很多。这是简单的“以空间换时间”。
我们发现,由于这些计算过程中会出现重叠的结点,符合动态规划中子问题重叠的性质。
同时,可以看出如果通过第N次选择得到的是一个最优解的话,那么第N-1次选择的结果一定也是一个最优解。这符合动态规划中最优子问题的性质。
解决方案
考虑用动态规划的方法来解决,这里的:
阶段:在前N件物品中,选取若干件物品放入背包中
状态:在前N件物品中,选取若干件物品放入所剩空间为W的背包中的所能获得的最大价值
决策:第N件物品放或者不放
由此可以写出动态转移方程:
我们用f[i][j]表示在前 i 件物品中选择若干件放在已用空间为 j 的背包里所能获得的最大价值
f[i][j] = max(f[i - 1][j - W[i]] + P[i], f[i - 1][j])//j >= W[ i ]
这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,价值为f[v];如果放第i件物品,那么问题就转化为“前i-1件物品放入已用的容量为c的背包中”,此时能获得的最大价值就是f[c]再加上通过放入第i件物品获得的价值w。
这样,我们可以自底向上地得出在前M件物品中取出若干件放进背包能获得的最大价值,也就是f[m,w]
算法设计如下:
int main()
{
cin >>n >>v
for (int i = 1i <= ni++)
cin >>c[i]//价值
for (int i = 1i <= ni++)
cin >>w[i]//体积
for (int i = 1i <= ni++)
f[i][0] = 0
for (int i = 1i <= ni++)
for (int j = 1j <= vj++)
if (j >= w[i])//背包容量够大
f[i][j] = max(f[i - 1][j - w[i]] + c[i], f[i - 1][j])
else//背包容量不足
f[i][j] = f[i - 1][j]
cout <<f[n][v] <<endl
return 0
}
由于是用了一个二重循环,这个算法的时间复杂度是O(n*w)。而用搜索的时候,当出现最坏的情况,也就是所有的结点都没有重叠,那么它的时间复杂度是O(2^n)。看上去前者要快很多。但是,可以发现在搜索中计算过的结点在动态规划中也全都要计算,而且这里算得更多(有一些在最后没有派上用场的结点我们也必须计算),在这一点上好像是矛盾的。
有点问题:public static void knapsack(int[]v,int[]w,int c,int[][]m)
{
int n=v.length-1
int jMax=Math.min(w[n]-1,c)
for(int j=0j<=jMaxj++)
m[n][j]=0
for(int j=w[n]j<=cj++)
m[n][j]=v[n]
for(int i=n-1i>1i--)
{
jMax=Math.min(w[i]-1,c)
for(int j=0j<=jMaxj++)
m[i][j]=m[i+1][j]
for(int j=w[i]j<=cj++)
m[i][j]=Math.max(m[i+1][j],m[i+1][j-w[i]]+v[i])
}
m[1][c]=m[2][c]
if(c>=w[1])
m[1][c]=Math.max(m[1][c],m[2][c-w[1]]+v[1])
}
public static void traceback(int[][]m,int[]w,int c,int[]x)
{
int n=w.length-1
for(int i=1i<ni++) {
if(m[i][c]==m[i+1][c])x[i]=0
else {
x[i]=1
c-=w[i]
}
x[n]=(m[n][c]>0)?1:0
}
//int n=w.length-1
for(int i=1i<ni++)
if(m[i][c]==m[i+1][c])x[i]=0
else {
x[i]=1
c-=w[i]
}
x[n]=(m[n][c]>0)?1:0
}
周二举行的算法竞赛:从中认识到自身的缺点,对于需要用到专业的算法知识的题时,不懂该如何解决,只会一味的用暴力解题法,或者是模拟法,时间复杂多过高,最终超时。所以我因该学习专业的算法知识,先搞懂,在练习,虽然一天搞不懂几个但是,还是感觉有很大收获,主要学习了二分查找,比较简单,而且格式比较固定,递归理解概念,但实际操作起来比较困难,每一道题都有其不同的递归方法,还需要多练习相关的递归题目。动态规划的背包问题,还没学会。相较于以前的算法学习做题量大幅度减少,但是思维量却大幅上升,需要反复看,反复理解。
为蓝桥杯做准备:看了2021年的JavaB组的题,感觉对我来说还是挺难的,但是我不可能有时间去把一本算法书全看完,再去参加蓝桥杯的比赛,时间是不允许的,所以我最近学的算法知识都已贴近蓝桥杯比赛常用算法为主,也刷了一些蓝桥杯上面的题,部分题对我启发较大。
下周学习简述:肯定还是会以本周学习的方向为主方向,不会有太大的改变,但是可能会加大一些题量。