原题地址:
http://poj.org/problem?id=3624
0-1背包问题描述如下:
有一个容量为M的背包,和N个物品。这些物品分别有两个属性,体积w和价值v,每种物品只有一个。要求用这个背包装下价值尽可能多的物品,求该最大价值,背包可以不被装满。
因为最优解中,每个物品都有两种可能的情况,即在背包中或者不存在(背包中有0个该物品或者 1个),所以我们把这个问题称为0-1背包问题。
解题思路
0-1背包问题时动态规划里最基础也相对重要的题型,是《背包九讲》里的第一篇,优快云上有大神做了总结,我觉得讲的非常清楚,在这里引用一下。
有一下几点是关键:
核心状态转移方程:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]);dp[i, j]表示前i个物体面对剩余容量为j时背包的最大价值,w[i]代表物体i的重量,v[i]代表物体i的价值;如果第i个物体不放入背包(可能放不下),则背包的最大价值等于前i-1个物体面对容量v的最大价值;如果第i个物体选择放入,则背包的最大价值等于前i-1个物体面对容量j-w[i]的最大价值加上物体i的价值v[i]。
一般采用二维数组(状态转移矩阵)dp[i][j]来记录各个子问题的最优状态,其中dp[i][j]表示前i个物体面对容量j背包的最大价值。此时的时间复杂度为O(N*M),空间复杂度也为O(N*V);附上trace函数打印最优解序列,即取了哪几个物品。
当需要的空间太大时,比如这道题,N=3405不算大,但是M接近12885时这个空间需求就会非常大,导致Memory Limit Exceeded,所以必须要对空间进行优化,主要的思想就是:在状态转移的过程中(i-1 to i),当前状态(i)只与前一状态(i-1)时的解有关,那么之前存储的状态信息(i-2,i-3…)已经没有用途,可以舍弃来节约空间。
空间优化版的状态转移方程:
dp2[j] = max(dp2[j], dp2[j-w[i]] + v[i]);第i次循环结束后dp2[j]中所表示的就是使用二维数组时的dp[i][j],即前i个物体面对容量j时的最大价值。
由于dp2[j]是由dp[i-1][j]和dp[i-1][j-w[i]]这两个状态得来的,对于前者,当第i次循环之前时,dp2[j]实际上就是dp[i-1][j],而对于后者,若j递增,则dp2[j-w[i]]代表的是更新后的dp[i][j-w[i]]而不是旧的的dp[i-1][j-w[i]],因此为了保护dp[i-1][j-w[i]]不被覆盖,必须让j逆序递减。(其实就是避免重复放入物品i,注意和完全背包的区别!)
空间优化版本最后是求解不出来最优解序列的,但是能求出最优解,也就是最大价值
背包问题最优解的两种问法:“恰好装满背包” “不需要背包装满”。区别这两种问法的实现方式在于矩阵初始化的不同。
- 不需要装满:初始化时将dp[0][0..M]全部设为0,意味着最开始0也是合法的解,之后的过程中不装满也是合法的解。
- 必须装满:初始化时除了dp[0][0]外,dp[0][1..M]全部设为-∞,意味着最开始没装满就是非法的解,之后的过程中有空余位置的dp[i][j]是非法的,值接近-∞。
AC代码
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 3405;
const int maxm = 12885;
int w[maxn],v[maxn]; //物品重量w/价值v[1...n]
int dp[maxn][maxm];
void print_trace(int n, int m)
{
cout << "最终的二维状态转移矩阵:" << endl;
for (int i = 0; i <= n; ++i)
{
for (int j = 0; j <= m; ++j)
{
cout << dp[i][j] << ' ';
}
cout << endl;
}
int i = n, j = m;
bool flag[maxn] = {0};
while (dp[i][j] != 0)
{
if (dp[i][j] == dp[i-1][j-w[i]] + v[i]) //取了第i个物品
{
flag[i] = 1;
j -= w[i];
}
--i;
}
cout << "取了第几个物品:(1..n) " << endl;
for (int i = 1; i <= n; ++i)
if(flag[i])
cout << i << ": " << w[i] << ' ' << v[i] << endl;
}
///0-1背包动态规划解法:dp[i][j]表示前i件物品,背包剩余容量为j时,所取得的最大价值
void solve(int n, int m)
{
for (int j = 0; j <= m; ++j)
dp[0][j] = 0;
for (int i = 1; i <= n; ++i)
{
for (int j = 0; j <= m; ++j)
{
if (w[i] > j) //第i件物品超过剩余重量时不加入
dp[i][j] = dp[i-1][j];
else
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]); //不加或加
}
}
cout << "最大价值:";
cout << dp[n][m] << endl;
print_trace(n, m);
}
///0-1背包的空间优化解法:用一维数组dp2[j]表示dp[i][j]
void solve_better(int n, int m)
{
int dp2[maxm];
for (int j = 0; j <= m; ++j)
dp2[j] = 0;
for (int i = 1; i <= n; ++i)
///注意逆序的意义:由于dp[i][j]取决于dp[i-1][j]和dp[i-1][j-w[i]]
///对于后者,若j递增,则dp2[j-w[i]]代表的是更新后的dp[i][j-w[i]]而不是旧的的dp[i-1][j-w[i]]
for (int j = m; j >= w[i]; --j) //仅当剩余重量能装下第i件物品才更新
dp2[j] = max(dp2[j], dp2[j-w[i]] + v[i]);
//cout << "最大价值:";
cout << dp2[m] << endl;
/*
cout << "最终的一维状态转移矩阵:" << endl;
for (int j = 0; j <= m; ++j)
cout << dp2[j] << ' ';
cout << endl;
*/
}
int main()
{
ios::sync_with_stdio(false);
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; ++i)
cin >> w[i] >> v[i];
//cout << "\n-------空间未优化-------" << endl;
//solve(n, m);
//cout << "\n-------空间优化-------" << endl;
solve_better(n, m);
return 0;
}
算法复杂度:O(N*M) 空间复杂度:O(M)
耗时:266ms
测试结果
注意上面的二维矩阵和下面的一维向量是相同的 :)

本文详细解析0-1背包问题及其动态规划解决方案,包括基本概念、状态转移方程、二维数组实现及空间优化技巧,并提供AC代码示例。
890

被折叠的 条评论
为什么被折叠?



