(0/1背包题目:对于每一件物品,只能取一次,而且有容量限制,每件物品有重量和价值,决策为取与不取)
讲完了数塔问题,咱们再来看看一个炒鸡老炒鸡老的题目:NOIP2005 PJ组 第三题:“采药”。
题目描述
对于这道题目,我们先来想想搜索怎么写(因为DP和记搜很相似)。
我们先定义f[i][j]表示剩余i时间,取了j株草药所能获得的最大价值。
那么,冗余搜索是什么?
让我们来思考:如果在取到第x株草药时花费了t时间,而且目前所获得的价值比f[t][x]所要小,那么接下来无论怎么操作它最后得出的ans一定比用f[t][x]所进行操作的ans小?
答案就是如此(大家可以思考一下为什么)。
接下来附上记搜代码:
void dfs(int t, int x, int val) // 当前的状态: 背包还剩t的空间, 现在采到第X棵草药, 目前的总价值为val
{
if(val <= f[t][x]) return; // 记忆化: 如果当前的价值小于之前某一次, 那就直接退出
f[t][x] = val; // 更新记忆化数组
if(x == n) // 采到最后一棵了
{
if(val > ans) ans = val; // 与答案取最大值
return;
}
dfs(t, x+1, val); // 不取这一棵
if(t >= w[x]) dfs(t - w[x], x+1, val + v[x]); // 取这一棵, 前提是当前的空间足够
}
写完了记搜,然后我们来看看DP的实现方式
我们先来回顾一下动态规划的意义:只记录状态的最优值,并用最优值来推导其他的最优值。
然后我们通过刚刚的记搜来设计我们的状态:
f[i][j]表示:已经决定了前i株草药,用了j的时间,所能得到的最大价值。
先来看顺推:
“我这个状态的下一步去向何方”:决定下一个物品取还是不取。
>不取:状态转移为f[i+1][j]
>取:状态转移为f[i+1][j-w[i+1]](需要满足重量约束)
再来看逆推:
“我这个状态从何而来”:决定我这个物品取不取
>不取:由f[i-1][j]推导而来
>取:由f[i-1][j-w[i]]推导而来(需要满足重量约束)
先附上顺推代码:
for(int i = 0; i < n; ++ i)
for(int j = 0; j <= t; ++ j)
{
// 顺推: 考虑当前状态, 已经采了i棵草药, 目前占据的空间大小为j
// 第一种方法: 不采这一棵, 则下一步的状态则是(i+1, j)
f[i+1][j] = max(f[i+1][j], f[i][j]);
// 第二种方法: 采这一棵, 则需要满足空间足够大
if(j + w[i] <= t) // 约束 : 空间大小足够
// 下一步的状态则是(i+1, j+w[i])
f[i+1][j+w[i]] = max(f[i+1][j+w[i]], f[i][j]+v[i]);
}
ans = f[n][t]; // 答案
cout << ans << endl;
现在附上逆推代码:
for(int i = 1; i <= n; ++ i)
for(int j = 0; j <= t; ++ j)
{
// 逆推: 考虑是从什么状态到达我这里的(i, j)
f[i][j] = f[i-1][j]; // 如果我这棵草药不取, 那么从状态(i-1, j)可以达到这个状态
// 如果我取了这棵草药, 那个从状态(i-1, j-w[i]), 再加上这棵草药, 就可以达到这个状态
if(j >= w[i]) f[i][j] = max(f[i][j], f[i-1][j-w[i]] + v[i]); // 但需要注意, 约束是需要满足的
}
ans = 0;
for(int i = 0; i <= t; ++ i) ans = max(ans, f[n][i]);
cout << ans << endl; // 输出答案
接下来我们来考虑数组压缩:
*数组压缩:即用一个一维数组来代替二维数组
-观察方程,我们可以发现,f[i]仅仅是由f[i-1]决定的,也就是说,前面的大多数状态对后面均无影响。
f[1] | ... | ... | ... | ... | ... | ... |
f[2] | ... | ... | ... | ... | ... | ... |
f[3] | 0 | 0 | f[3][3]=12 | 0 | f[3][5]=15 | 0 |
f[4] | f[4][1]=? | f[4][2]=? | f[4][3]=? | f[4][4]=? | f[4][5]=? | f[4][6]=? |
f[5] | ||||||
f[6] |
如上表,f[4]中的所有状态只和f[3]中的两个状态f[3][3]和f[3][5]有关而f[3][1],f[3][2]等等对于f[4]中的任意一个值都没有影响,所以我们可以仅仅保留前一行的状态。
那如何压缩呢?我们用一种特别简单的方法:将j倒着枚举。
附上代码:
for(int i = 1; i <= n; ++ i)
for(int j = t; j >= 0; -- j) // 在使用压缩状态的时候, 需要注意枚举的方向
{ // 由于是01背包, 所以j要倒过来枚举 (注意每时每刻数组里存的是f[i-1][j]还是f[i][j])
// f[i][j] = f[i-1][j]; -> f[j] = f[j]
//if(j >= w[i]) f[i][j] = max(f[i][j], f[i-1][j-w[i]] + v[i]);
if(j >= w[i]) f[j] = max(f[j], f[j - w[i]] + v[i]);
}
上述就是采药的全部讲解,实际上采药是一道全裸的0/1背包题目,对于所有全裸的0/1背包,均可用上述的三种代码来实现。
结束了?
没有。
实际上,对于0/1背包的代码打法。还有另外一种:
for(int i = 1; i <= n; ++ i)
for(int j = t; j >= w[i]; -- j) // 在使用压缩状态的时候, 需要注意枚举的方向
{
f[j] = max(f[j], f[j - w[i]] + v[i]);
}
有没有发现什么不一样?
没错,第二层for循环,把“0”改为w[i],可以省掉一个if判断,是不是很厉(méi)害(yòng)?
好了,以上就是采药及0/1背包的全部讲解。