背包问题
01 背包
有 n 件物品,每个物品只能使用一次,在不超过背包体积的情况下,总价值最大是多少?
cpp
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main() // 优化前
{
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++) // 选第 i 个物品
{
for(int j = 0; j <= m; j++) // 枚举体积
{
f[i][j] = f[i - 1][j];
if(j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
}
cout << f[n][m] << endl;
return 0;
}
状态表示可以优化为一维数组!
f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
这里求 f[i][j]
使用的是 f[i-1]
层的东西,所以在优化为一维后,需要从后往前遍历 j ,这样可以确保后面使用的 f[i - 1]
是上一层未被更新的,而不是更新后的!
优化后:
cpp
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++) // 选第 i 个物品
{
for(int j = m; j >= v[i]; j--) // 枚举体积
{
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m] << endl;
return 0;
}
完全背包
每个物品可以拿无数个,求不超过背包体积的情况下,总价值最大是多少
暴力写法:可以直接采用 拿与不拿
的思想,第 i
个物品最多可以拿 k
个,在枚举物品种类和价值的基础下,再枚举这 k
种情况即可
cpp
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
for(int j = 0; j <= m; j++)
for(int k = 0; k* v[i] <= j; k++)
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
cout << f[n][m];
return 0;
}
使用数学方法经化简可以得出:f[i][j]
的最大值恰好比 f[i][j - v[i]]
多了 w[i]
,那么 f[i][j]
的状态转移方程就可以优化为:f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]);
,这样,就直接可以使用一维背包的套路来做了:
cpp
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= m; j++)
{
f[i][j] = f[i - 1][j];
if(j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
}
}
cout << f[n][m];
return 0;
}
优化为一维:但完全背包的 j 是从 v[i]
开始,往大的枚举,这个与 01 背包恰好相反,其他的一维优化后与 01 背包别无二致!
cpp
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
for(int j = v[i]; j <= m; j++)
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m];
return 0;
}
多重背包
有 N 种物品和一个容量是 V 的背包。第 i
种物品最多有 s[i]
件,每件体积是 v[i]
,价值是 w[i]
,求不超过背包容量的情况下,总价值最大时多少?
暴力解法,在枚举物品种类和体积的基础上,依次枚举符合符合条件的物品个数(k <= s[i]
&& k * v[i] < j
)
时间复杂度:O(n* m * s)
cpp
#include <iostream>
using namespace std;
const int N = 110;
int n, m;
int v[N], w[N], s[N];
int f[N][N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> s[i];
for(int i = 1; i <= n; i++)
for(int j = 0; j <= m; j++)
for(int k = 0; k <= s[i] && k * v[i] <= j; k++)
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
cout << f[n][m] << endl;
return 0;
}
优化:二进制优化
每个物品可选择的个数 s[i]
,都能通过 多个2^x + 某一个数
来表示出来,比如,76,就可以用 1 + 2 + 4 + 8 + 16 + 32 + 13
来表示,那么枚举 s[i]
个第 i 个体积为 w[i]
的数,就转化为了枚举:
一个体积为 v[i]
,价值为w[i]
的物品, 一个体积为 2 * v[i]
,价值为2 * w[i]
的物品,一个体积为 4 * v[i]
,价值为4 * w[i]
的物品 ...,一个体积为 k * v[i]
,价值为 w[i]
的物品。这样,时间复杂度就被优化为了 :O(n* m* logs)
最后就转化为了:cnt
个物品的 01 背包问题!
cpp
#include <iostream>
using namespace std;
// s[i] 2000 可以拆分为 log2000 ~= 11 个物品, 所以开 12000 个空间足够
const int N = 12000;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> m;
int cnt = 0; // 记录优化后的物品编号
for(int i = 1; i <= n; i++)
{
int a, b, s;
cin >> a >> b >> s;
int k = 1;
while(k <= s)
{
cnt ++;
v[cnt] = k * a;
w[cnt] = k * b;
s -= k;
k *= 2;
}
if(s > 0) // 剩余的不能使用 2^ x 次方表示的
{
cnt ++;
v[cnt] = s * a;
w[cnt] = s * b;
}
}
n = cnt;
for(int i = 1; i <= n; i++)
for(int j = m; j >= v[i]; j--)
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
分组背包
有 N 组物品和一个容量是 V 的背包,每组物品有若干个,同组内的物品最多只能选一个。
利用 dp 中选或不选的思路,枚举 k + 1 种情况:不选第 i 种物品;选第一个;选第二个;选第三个...
三重循环枚举,时间复杂度:O(N^3)
代码易错点:k
与 s[]
是从 0 开始还是从1开始必须想好,更新f[i][j] = f[i - 1][j];
,必须放在第三重循环外,第二重循环内!
cpp
#include <iostream>
using namespace std;
const int N = 110;
int v[N][N], w[N][N], s[N];
int m, n;
int f[N][N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
cin >> s[i]; // 输入第 i 种物品的个数
for(int j = 1; j <= s[i]; j++)
cin >> v[i][j] >> w[i][j]; // 输入第 i 种物品的 k 个体积和价值
}
for(int i = 1; i <= n; i++) // 枚举物品种类
{
for(int j = 0; j <= m; j++) // 枚举体积
{
f[i][j] = f[i - 1][j]; // 必须放外面! 因为这是不选的情况
for(int k = 1; k <= s[i]; k++) // 枚举第 i 个数的 k 种取法
if(j >= v[i][k]) f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
}
}
cout << f[n][m] << endl;
return 0;
}
优化为一维:
cpp
#include <iostream>
using namespace std;
const int N = 110;
int v[N][N], w[N][N], s[N];
int m, n;
int f[N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
cin >> s[i]; // 输入第 i 种物品的个数
for(int j = 1; j <= s[i]; j++)
cin >> v[i][j] >> w[i][j]; // 输入第 i 种物品的 k 个体积和价值
}
for(int i = 1; i <= n; i++) // 枚举物品种类
{
for(int j = m; j >= 0; j--) // 因为要用上一层的状态,因此必须从大到小
{
for(int k = 1; k <= s[i]; k++) // 枚举第 i 个数的 k 种取法
if(j >= v[i][k]) // 说明可以选
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
}
}
cout << f[m] << endl;
return 0;
}