01背包,完全背包求方案数与具体方案
对于背包问题求方案数与具体方案,我想分为两种题型:
- 恰好型问题—— \(n\) 个数,每个 \(a_{i}\) 满足一定范围(为定值,或取值范围是一个区间等),且\(a_{1} + a_{2} + ... + a_{n}=k\) 的 \((a_{1}, a_{2},...,a_{n})\) 个数,以及求其中一组解。
- 经典背包问题—— \(n\) 个物品,第 \(i\) 个物品价值为 \(w[i]\),体积为 \(v[i]\),背包体积为 \(V\),求获得最大价值的方案数,以及一组方案。
恰好型问题
求方案数:设 \(dp[i][j]\):考虑前 \(i\) 个数,总和恰好为 \(j\) 的 \((a_{1}, a_{2},...,a_{i})\) 方案数。
\(init\):\(dp[0][0]=1\)
\(trans\):决策 \(a_{i}\) 时,有:考虑前 \(i\) 个数的方案数 = 考虑前 \(i-1\) 个数并不选 \(a_{i}\)的方案数 + 考虑前 \(i-1\) 个数并选 \(a_{i}\)的方案数,转移式为:
一维形式:
只看是否可行:
\(a_{i}\) 不是定值,而是可取多个数时,枚举每个数即可。
对于01背包与完全背包,上述转移形式是完全相同的,唯一不同的在于体积的正倒序枚举。
求出其中一个具体方案:求具体方案的核心在于确认好某个状态是由前面哪个状态转移而来的。可以利用 是否可行 这个递推式来做——需要将上式改为另一种繁琐的形式,以确认是否真的转移过来:
if(dp[j - a[i]] == 1 && dp[j] == 0){
dp[j] = 1;
path[j] = {j - a[i], i};
}
改写为上述形式时,就可以确认 \(dp[j]\) 这一状态的计算确实是由 \(dp[j-a[i]]\) 通过选择了一个 \(a[i]\) 转移而来。(注意:01背包和完全背包都可以这样做,完全背包只不过是对于某一个 \(a_{i}\) 会选多次)
这样就可以通过第 3 行的写法(\(path[j]\) 表示 [\(dp[j]\)的转移来源,转移时选了哪个物品])来记录转移路径。又因为所有的 \(dp[j]\) 的初始起源均为 \(dp[0]\),因此任意一种状态回溯的最终状态也一定是 \(dp[0]\),而回溯过程中的所有物品,就是一种方案。
除这个转移式外,应该也能用其他的转移式来做,以后想到了再补充。
some problems
经典背包问题
与上述问题不同,背包形式不仅将等式改为了不等式(\(v_{1} + v_{2} + ... + v_{n}<=V\)),而且还要满足另一种约束——最大化总价值。
求解方案数是类似的,定义\(dp[i][j]\):考虑前 \(i\) 个物品,背包体积为 \(j\),总价值最大的方案数。
\(init: dp[0][i]=1, i \in [0,V]\)
\(trans:\) 由于和价值相关,因此方案数的转移需要伴随在 计算最大化价值 的代码中,具体地:
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1010;
const int mod = 1e9 + 7;
int n, V;
int v[maxn], w[maxn];
int dp[maxn][maxn];
int cnt[maxn][maxn];// cnt[i][j] 对应 dp[i][j]状态下的方案数
int main()
{
cin >> n >> V;
for(int i = 1; i <= n; i++){
cin >> v[i] >> w[i];
}
for (int i = 0; i <= V; i++)//从前0个物品中选,相当于任何体积下都只有不选一种方案(此时取价值的情况只有0,也是最大价值),cnt==1
{
cnt[0][i] = 1;
}
for (int i = 1; i <= n; i++){
for (int j = 0; j <= V; j++){
if (j < v[i] || dp[i - 1][j] > dp[i - 1][j - v[i]] + w[i]){ // 1
cnt[i][j] = cnt[i - 1][j];
}
else if (dp[i - 1][j] < dp[i - 1][j - v[i]] + w[i]){ // 2
cnt[i][j] = cnt[i - 1][j - v[i]];
}
else if (dp[i - 1][j] == dp[i - 1][j - v[i]] + w[i]){ // 3
cnt[i][j] = (cnt[i - 1][j] + cnt[i - 1][j - v[i]]) % mod;
}
dp[i][j] = dp[i - 1][j];
if(j >= v[i]) dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
}
}
cout << cnt[n][V] << endl;
return 0;
}
可以看出上面的三个转移的特点—— \(dp[i][j]\) 只会由 \(dp[i-1][j]\) 和 \(dp[i-1][j-v[i]]+w[i]\) 两项转移而来,看哪一项最大就从那一项转移过来;特殊地,若二者相等,则说明选不选第 \(i\) 个物品都能得到最大价值,两种方案数加起来一起转移即可。
一维形式:
if(dp[j] < dp[j - v[i]] + w[i]){
cnt[j] = cnt[j - v[i]]
}
else if(dp[j] == dp[j - v[i]] + w[i]){
cnt[j] += cnt[j - v[i]];
}
//else{ // 可省略
//}
01背包与完全背包区别也仅在于体积的正倒序枚举。
求解具体方案和上面也是类似的——看 \(dp[i][j]\) 等于 \(dp[i-1][j]\) 还是等于 \(dp[i-1][j-v[i]] + w[i]\) ,便可判定由谁转移而来,进而决定选不选这个物品。从最终状态 \(dp[n][V]\) 回溯转移即可。
若规定选择物品总体积恰好为 \(V\),求最大化总价值的方案,则只需要将求普通 \(dp\) 数组改为求解恰好体积形式(\(dp[1\backsim V]=-inf,dp[0]=0\))即可——保证转移式求解的是规定形式,求具体方案的思路都是直接从最终状态回溯。