基础知识点
首先设置一个二维数组dp[][],令dp[i][j]表示前i个物品装进容量为j的背包能获得的最大价值。通过设置这么一个二维数组,dp[n][m]的值就是完全背包问题的解。
只考虑第i件物品时,可将情况分为 是否放入第i件物品 两种:
- 对于容量为j的背包,如果不放入第i件物品,那么这个问题就转换成将前i-1个物品放入容量为j的背包的问题,即dp[i][j] = dp[i-1][j]。
- 对于容量为j的背包,如果放入第i件物品,那么当前背包的容量就变成了j-w[i],并得到这个物品的价值v[i]。但是由于第i件物品仍然可以取,所以并不是转移到dp[i-1][j-w[i]],而是转移到dp[i][j-w[i]]之后这个问题就转化成将前i-1个物品放入容量为j-w[i]的背包问题,即dp[i][j] = dp[i][j-w[i]]+v[i]。
从以上两种情况可以得到状态转移方程:dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]]+v[i])。转移时要注意j-w[i]的值是否为非负值,若为负则代表当前的容量无法放入第i件物品,不能进行转移。
边界情况处理:dp[i][0] = dp[0][j] = 0 (0<=i<=n, 0<=j<=m)
观察状态转移的特点,可以发现dp[i][j]的转移仅与dp[i][j-w[i]]和dp[i-1][j]有关,即仅与二维数组中本行的上一行有关。根据这个特点,可以将原本的二维数组优化为一维数组,并用如下的方式完成状态转移:dp[j] = max(dp[j], dp[j-w[i]]+v[i])。为了保证状态正确转移,必须保证在每次更新中确定状态dp[j]时,dp[j-w[i]]已被修改。这就需要在每次更新中,正序遍历所有j值。
延伸知识点
求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。
- 如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。
- 如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设为0。
为什么呢?可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。
例题——Piggy-Bank
代码
#include <iostream>
#include <climits>
using namespace std;
const int MAXN = 501;
const int MAXM = 10001;
const int INF = 5e8+1;
int main(){
int T,E,F,N,P[MAXN],W[MAXN];
long long dp[MAXM];
cin>>T;
while(T--){
cin>>E>>F>>N;
for(int i=1;i<=N;i++){
cin>>P[i]>>W[i];
}
dp[0]=0;
for(int i=1;i<=F-E;i++){
dp[i] = INF;
}
for(int i=1;i<=N;i++){
for(int j=W[i];j<=F-E;j++){
dp[j] = min(dp[j],dp[j-W[i]]+P[i]);
}
}
if(dp[F-E]==INF) cout<<"This is impossible."<<endl;
else cout<<"The minimum amount of money in the piggy-bank is "<<dp[F-E]<<"."<<endl;
}
return 0;
}