背包问题&&方案数&&具体方案
1. \(01\) 背包求恰好装满方案数
HERE
f[i][j]: 从前i个物品中选,体积正好为j的方案数
状态转移方程和 \(01\) 背包问题求最大价值是一样的
朴素版
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110, M = 10010;
int n, m, v[N];
int f[N][M];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++ ) cin >> v[i];
// 从前i个物品中选,体积正好为0的方案数是0
for(int i = 0; i <= n; i ++ ) f[i][0] = 1;
for(int i = 1; i <= n; i ++ )
{
for(int j = 1; j <= m; j ++ )
{
// 不选
f[i][j] = f[i - 1][j];
// 选
if(j - v[i] >= 0) f[i][j] += f[i - 1][j - v[i]];
}
}
cout << f[n][m] << endl;
return 0;
}
优化版
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110, M = 10010;
int n, m, v[N];
int f[M];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++ ) cin >> v[i];
// 从前i个物品中选,体积正好为0的方案数是0
f[0] = 1;
for(int i = 1; i <= n; i ++ )
for(int j = m; j >= v[i]; j -- )
f[j] += f[j - v[i]];
cout << f[m] << endl;
return 0;
}
2. 完全背包求恰好装满方案数
HERE
f[i][j]: 从前i个物品中选,体积正好为j的方案数
朴素版
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int M = 1010, N = 4;
int m, n = N;
int f[1 + N][M];
int v[1 + N] = {0, 10, 20, 50, 100};
int main()
{
cin >> m;
// 体积为0时什么都不选也是一种方案
for(int i = 0; i <= n; i ++ ) f[i][0] = 1;
for(int i = 1; i <= n; i ++ )
{
for(int j = 1; j <= m; j ++ )
{
// 不选
f[i][j] = f[i - 1][j];
// 选,枚举选多少件
for(int k = 1; j - k * v[i] >= 0; k ++ )
f[i][j] += f[i - 1][j - k * v[i]];
}
}
cout << f[n][m] << endl;
return 0;
}
优化版
思路和求最大价值一样
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int M = 1010, N = 4;
int m, n = N;
int f[M];
int v[1 + N] = {0, 10, 20, 50, 100};
int main()
{
cin >> m;
// 体积为0时什么都不选也是一种方案
f[0] = 1;
for(int i = 1; i <= n; i ++ )
for(int j = v[i]; j <= m; j ++ )
f[j] += f[j - v[i]];
cout << f[m] << endl;
return 0;
}
3. 01 背包求最优选法方案数
HERE
f[i][j]:从前i个物品中选,体积为j的最大价值
cnt[i][j]:从前i个物品中选,体积为j时,取最大价值时的方案数
求恰好装满的方案数与最优选法时的方案数的做法大不相同。
求恰好装满的方案数是直接通过 \(f\) 数组,用 \(dp\) 来求,但求最优选法时的方案数是跟踪记录 \(dp\) 的过程来求的。
这一点能从 \(f\) 数组的含义清楚的看到,前者 \(f\) 数组就是方案数,后者 \(f\) 数组求的是最优方案
并且需要通过额外的 \(cnt\) 数组来跟踪记录
之所以有这样的不同是因为,在求装满时的方案数时,体积等于价值,由于任意转移过程中最大价值就等于此时的体积,因此我们无需专门的 f
数组来保存最大价值,所以我们可以省去一个数组。
而当物品体积不等于价值时,装满并不意味着就取得了最大价值,此时我们需要一个数组来专门记录这个最大价值,因为状态转移过程中需要用到。
另外,本题的一个难点还在于,如何初始化 \(cnt\) 数组,通常我们容易考虑到,当体积为 \(0\) 时,我们什么都没法选物品,是一种方案。
但我们通常忘记,物品个数为 \(0\) 时,我们没物品可选,这也是一种方案!
还有就是需要注意,在装满背包求方案数问题当中,只能初始化 f[0]=0
,而不能初始化 f[1~m]=0
,因为此时背包没有装满
在非装满背包问题中则可以初始化。
朴素做法
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
typedef long long LL;
int n, m;
int f[N][N];
int cnt[N][N];
int v[N], w[N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
// 体积为0时,什么都选不了是一种方案
for(int i = 0; i <= n; i ++ ) cnt[i][0] = 1;
// 注意下面的初始化容易忽略,当没有物品可以选时,也是一种方案
for(int i = 0; i <= m; i ++ ) cnt[0][i] = 1;
for(int i = 1; i <= n; i ++ )
{
for(int j = 1; j <= m; j ++ )
{
// 不选
f[i][j] = f[i - 1][j];
cnt[i][j] = cnt[i - 1][j];
// 选
if(j >= v[i])
{
int t = f[i - 1][j - v[i]] + w[i];
if(f[i][j] == t)
cnt[i][j] = ((LL)cnt[i][j] + cnt[i - 1][j - v[i]]) % mod;
else if(f[i][j] < t)
{
f[i][j] = t;
cnt[i][j] = cnt[i - 1][j - v[i]];
}
}
}
}
cout << cnt[n][m] << endl;
return 0;
}
优化做法
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
typedef long long LL;
int n, m;
int f[N];
int cnt[N];
int v[N], w[N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
/* 关于初始化的一个坑: 错误的认为只需要初始化 cnt[0]=0;
即,考虑到当体积为0时,没法选物品,是一种方案
但通常忘了,当没有物品时,即没有物品可选时,也是一种方案
并且由于我们省去了第一维(物品),这一点更不容易发现了
cnt[0]=0; // 体积为0,没法选
cnt[1~m]=1; // 物品个数为0,没得选
*/
for(int i = 0; i <= m; i ++ ) cnt[i] = 1;
for(int i = 1; i <= n; i ++ )
{
for(int j = m; j >= v[i]; j -- )
{
int t = f[j - v[i]] + w[i];
if(f[j] == t)
cnt[j] = ((LL)cnt[j] + cnt[j - v[i]]) % mod;
else if(f[j] < t)
{
f[j] = t;
cnt[j] = cnt[j - v[i]];
}
}
}
cout << cnt[m] << endl;
return 0;
}
4. 背包问题求具体方案
HERE
字典序最小
:只需要按顺序遍历物品,所得字典序就是最小的
优化版
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 1010, M = 1010;
int n, m;
int v[N], w[N];
int f[N];
vector<int> path[M];
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 = m; j >= v[i]; j -- )
{
int t = f[j - v[i]] + w[i];
if(f[j] == t)
{
auto p = path[j - v[i]];
// f相同,取字典序最小的,因为要与最终的结果进行比较,如果是由path[j-v[i]]经过i更新的话,需要push_back(i)
// 如果不经由path[j-v[i]]更新的话,由于f不变,path[j]也无需更新
p.push_back(i);
if(p < path[j]) path[j] = p;
}
else if(f[j] < t)
{
f[j] = t;
path[j] = path[j - v[i]];
// f[j]由f[j-v[i]]通过i更新,需要push(i)
path[j].push_back(i);
}
}
}
for(auto &x : path[m]) cout << x << ' ';
cout << endl;
return 0;
}