背包问题是动态规划中最经典的问题,很多题⽬或多或少都有背包问题的影⼦。它的基本形式是:给定⼀组物品,每个物品有体积和价值,在不超过背包容量的情况下,选择物品使得总价值最⼤。
背包问题有多种变体,主要包括:
- 01背包问题:每种物品只能选或不选(选0次或1次)。
- 完全背包问题:每种物品可以选择⽆限次。
- 多重背包问题:每种物品有数量限制。
- 分组背包问题:物品被分为若⼲组,每组只能选⼀个物品。
- 混合背包:以上四种背包问题混在⼀起。
- 多维费⽤的背包问题:限定条件不⽌有体积,还会有其他因素(⽐如重量)。
除了经典的总价值最⼤问题,还会有:
- ⽅案总数。
- 最优⽅案。
- ⽅案可⾏性。
- 输出具体⽅案。
01背包
我们先解决第⼀问:
- 状态表⽰:
dp[i][j]
表⽰:从前i个物品中挑选,总体积「不超过」j,所有的选法中,能挑选出来的最⼤
价值。
那么dp[n][v]
就是我们要的结果。 - 状态转移⽅程:
线性dp 状态转移⽅程分析⽅式,⼀般都是根据「最后⼀步」的状况,来分情况讨论:
a. 不选第i个物品:相当于就是去前i-1个物品中挑选,并且总体积不超过j。此时dp[i][j] = dp[i - 1][j]
b. 选择第i个物品:那么我就只能去前i-1个物品中,挑选总体积不超过j-v[i]
的物品。
此时dp[i][j] = dp[i - 1][j - v[i]] + w[i]
。但是这种状态不⼀定存在,因此需要特判⼀下。
综上,状态转移⽅程为:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i])
- 初始化:
直接填表,第⼀⾏的0 不影响结果。 - 填表顺序:
根据「状态转移⽅程」,我们仅需「从上往下」填表即可
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
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-1][j-v[i]] + w[i]);
}
}
}
cout << f[n][m] << endl;
return 0;
}
接下来解决第⼆问:
第⼆问仅需修改⼀下初始化以及最终结果即可。
- 初始化:
因为有可能凑不⻬j体积的物品,因此我们把不合法的状态设置为负⽆穷。这样在取最⼤值的时候,就不会考虑到这个位置的值。负⽆穷⼀般设置为-0x3f3f3ff3即可。
然后把dp[0][0] = 0
修改成0,因为这是⼀个合法的状态,最⼤价值是0 ,也让后续填表是正确
的。 - 返回值:
在最后拿结果的时候,也要判断⼀下最后⼀个位置是不是⼩于0 ,因为有可能凑不⻬。
不能判断是否等于-0x3f3f3f3f,因为这个位置的值会被更新,只不过之前的值太⼩,导致更
新后还是⼩于0的
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
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-1][j-v[i]] + w[i]);
}
}
}
cout << f[n][m] << endl;
memset(f, -0x3f, sizeof f);
f[0][0] = 0;
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-1][j-v[i]] + w[i]);
}
}
}
if (f[n][m] < 0) cout << 0 << endl;
else cout << f[n][m] << endl;
return 0;
}
空间优化:
- 考虑是否要修改遍历顺序
- 直接在原始代码上,删掉第一维即可
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
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 = m; j >= v[i]; j--)
{
f[j] = max(f[j], f[j-v[i]] + w[i]);
}
}
cout << f[m] << endl;
memset(f, -0x3f, sizeof f);
f[0] = 0;
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]);
}
}
if (f[m] < 0) cout << 0 << endl;
else cout << f[m] << endl;
return 0;
}
P1048 [NOIP 2005 普及组] 采药 - 洛谷
基本01 背包问题,将时间看成体积,就是标准的不放满的01 背包问题
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int t[N], w[N];
int f[N][N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> m >> n;
for (int i = 1; i <= n; i++) cin >> t[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 >= t[i]) f[i][j] = max(f[i][j], f[i-1][j-t[i]] + w[i]);
}
}
cout << f[n][m] << endl;
return 0;
}
空间优化:
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int t[N], w[N];
int f[N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> m >> n;
for (int i = 1; i <= n; i++) cin >> t[i] >> w[i];
for (int i = 1; i <= n; i++)
{
for (int j = m; j >= t[i]; j--)
{
f[j] = max(f[j], f[j-t[i]] + w[i]);
}
}
cout << f[m] << endl;
return 0;
}
P1164 小A点菜 - 洛谷
背包问题求⽅案数,稍微修改⼀个状态表⽰,然后根据具体问题分析状态转移⽅程和初始化即可。
- 状态表⽰:
dp[i][j]
表⽰:从前i 个菜中挑选,总价钱恰好等于j ,此时的总⽅案数。 - 状态转移⽅程:
针对a[i]
选或者不选,分两种情况讨论:
a. 如果不选a[i]
:相当于去前i-1个菜中挑选,总价钱恰好为j的⽅案数,此时的⽅案数就
是dp[i - 1][j]
;
b. 如果选a[i]
:那么应该去前i-1个菜中挑选,总价值恰好为j-a[i]
,此时的⽅案数就是dp[i - 1][j - a[i]]
;
因为我们要的是总⽅案数,于是dp[i][j] = dp[i - 1][j] + dp[i - 1][j - a[i]]
。
注意第⼆个状态可能不存在,要注意判断⼀下j ≥ a[i]
。 - 初始化:
dp[0][0]
,如果没有物品,想凑成总体积为0是可⾏的,啥也不选就是⼀种⽅案。当然,这个状态也是为了让后⾯的值是正确的。
其余位置的值是0 就不影响填表的正确性。 - 填表顺序:
从上往下每⼀⾏,每⼀⾏从左往右。
空间优化版本:每⼀⾏从右往左
#include <bits/stdc++.h>
using namespace std;
const int N = 110, M = 10010;
int n, m;
int a[N];
int f[N][M];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
f[0][0] = 1;
for (int i = 1; i <= n; i++)
{
for (int j = m; j >= 0; j--)
{
f[i][j] = f[i-1][j];
if (j >= a[i]) f[i][j] += f[i-1][j-a[i]];
}
}
cout << f[n][m] << endl;
return 0;
}
空间优化:
#include <bits/stdc++.h>
using namespace std;
const int N = 110, M = 10010;
int n, m;
int a[N];
int f[M];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
f[0] = 1;
for (int i = 1; i <= n; i++)
{
for (int j = m; j >= a[i]; j--)
{
f[j] += f[j-a[i]];
}
}
cout << f[m] << endl;
return 0;
}
P2946 [USACO09MAR] Cow Frisbee Team S - 洛谷
01 背包问题变形。
- 状态表⽰:
dp[i][j]
表⽰:从前i 头奶⽜中挑选,总和模f 之后为j 时,⼀共有多少种组合。
那么dp[n][0] - 1
就是最终结果。(因为动态规划会把全都不选这种情况也考虑进去,所以要剪掉) - 状态转移⽅程:
对于第i 头奶⽜选或者不选,可以分为两种情况讨论:
a. 如果不选a[i]
:此时的总⽅案数就是去[1, i - 1]
⾥⾯凑余数正好是j ,也就是dp[i - 1][j]
;
b. 如果选a[i]
:此时已经有⼀个余数为a[i] % f
,只⽤再去前⾯凑⼀个j - a[i] % f
即可。但是直接减可能会减出来⼀个负数,我们要把它补正,最终凑的数为(j - a[i] % f + f) % f
那么总⽅案数就是dp[i - 1][(j - a[i] % f + f) % f]
。
因为要的总⽅案数,所以状态转移⽅程就是上⾯两种情况的总和。 - 初始化:
dp[0][0] = 1
:什么也不选的时候,总和是0 ,余数也是0 ,属于⼀种⽅案,也是为了后续填表是正确的。 - 填表顺序:
从上往下每⼀⾏,每⼀⾏从左往右
#include <bits/stdc++.h>
using namespace std;
const int N = 2010, M = 1010, MOD = 1e8;
int n, m;
int a[N];
int f[N][M];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
f[0][0] = 1;
for (int i = 1; i <= n; i++)
{
for (int j = 0; j < m; j++)
{
f[i][j] = (f[i-1][j] + f[i-1][((j - a[i] % m)%m +m) %m]) %MOD;
}
}
cout << f[n][0] - 1 << endl;
return 0;
}