(随时更新)
AcWing 11. 背包问题求方案数
我的错误做法:
i, j, k : 前i个物品中,选了若干件,其体积小于等于j,且价值为k的所有方案。
属性:数量
集合划分:
包含i:其数量等于dp{i - 1, j - vi, k - wi}
不包含i:其数量等于dp{i - 1, j, k}
dp[i][j][k] = dp[i - 1][j - vi][k - wi] + dp[i - 1][j][k];
体积太大,会MLE。
yxc做法:
{i, j}集合描述 : 前i个物品中,选择了若干件,其体积为j的所有方案。
dp[i][j] : 属性:MAX价值
(错误理解)
对g来说
{i, j}集合描述 : 前i个物品中,选择了若干件,其体积为j时且价值最大的那一部分方案(很明显错误。i和j两个变量无法准确描述出这个集合)
属性:方案数
(正确理解)
{i, j}依旧是 前i个物品中,选择了若干件,其体积为j的所有方案。
g[i][j]的值为这个集合又多加了一个属性:{i, j}集合中,价值最大的方案的数量
对{i, j}进行集合划分求dp[i][j]:
包含i的子集的最大价值:dp[i - 1][j - v[i]] + w[i] = a
不包含i的子集的最大价值:dp[i - 1][j] = b
如果a > b,那么g[i][j] = g[i - 1][j]
如果a < b,那么g[i][j] = g[i - 1][j - v[i]]
如果a = b,那么g[i][j] = g[i - 1][j] + g[i - 1][j - v[i]]。
直接对集合{i, j}划分求g[i][j]会怎么样:
包含i的子集:{i - 1, j - v[i]},其最大价值的方案数量为g[i - 1][j - v[i]]
不包含i的子集:{i - 1, j},其最大价值的方案数量为g[i - 1][j]
需要知道这两个子集的最大价值谁大谁小,这样才能求出g[i][j]。也就是说,我们需要dp数组的辅助,才能求到。
/*
该题目能不能用01背包来做?
我能发现该题的算法推进过程有问题。01背包的最佳方案一定满足在下标单调递增。因此把算法推进过程中的一类方案放到一个集合中去的话,推进
过程肯定满足{0, 0} -- > {n, m}。(即最佳序列的中间序列,也满足下标单调递增)
而该题最佳方案的下标不能满足单调递增。因此,把算法推进过程中的一类方案,放到集合中去,最佳方案在推进过程,最前面的方案所在的集合,
可能是{n, s[n]}(即最佳方案的第一个能量石是n),很明显跟01背包不同。
那么如果硬做,能找到什么问题?
{i, j} : 前i个物品中,用了j时间,能够吃掉的所有能量石方案。
属性:MAX
集合划分(最后一个能量石是几号,肯定不遗漏):
最后一个是k(1 <= k <= i)
if (k != i) f{i, j - s[k]} + max(0, w[k] - l[k] * (j - s[k]))。
(问题来了,{i, j - s[k]}集合,是不是也可能还包含k?单从集合描述来看,确实可能。也就是说,该集合中,所包含的方案,比映射后的多。)
else f{i - 1, j - s[k]} + max(0, w[k] - l[k] * (j - s[k]))。
所以,需要寻找其它方法。
我们发现,最佳能量石方案,肯定满足前k个能获得能量,后n - k个不能获得能量(即能量为0)。可以看作没吃过后面
n - k个石头。
针对前k个,因为是最佳方案,所以交换两个能量石,肯定满足总能量减少或者不变;
根据这个性质列个等式,(假设之前的总时间为t)
Ei − t ∗ Li + Ej − (t + Si) ∗ Lj >= Ej − t∗Lj + Ei − (t + Sj) ∗ Li
得出:
Si ∗ Lj <= Sj ∗ Li。
针对后n - k个,我们发现,因为其能量已经随着时间的推移消减为0,因此不能够列出
Ei − t ∗ Li + Ej − (t + Si) ∗ Lj >= Ej − t∗Lj + Ei − (t + Sj) ∗ Li这个式子
因此不能够得出它们的si / li和前k个石头有什么关系。
因为题目只让我们求最大价值,因此只考虑选择前k个石头就可以了。后面n - k个石头可以看作不选。
所以,如果我们按照si / li的大小排序,并且只考虑那些能获得能量的能量石,那么就等价于01背包。即考虑,哪些石头该选,哪些石头不该选。
*/
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 105, M = 105 * 105;
struct Stone
{
int s, w, l;
} st[N];
int n, t;
int f[N][M];
bool cmp(const Stone& a, const Stone& b)
{
return (double)a.s / a.l <= (double)b.s / b.l;
}
int main()
{
cin >> t;
for (int k = 1; k <= t; k ++)
{
cin >> n;
int t = 0;
for (int i = 1; i <= n; i ++)
{
cin >> st[i].s >> st[i].w >> st[i].l;
t += st[i].s;
}
sort(st + 1, st + n + 1, cmp);
memset(f, -0x3f, sizeof f);
f[0][0] = 0;
for (int i = 1; i <= n; i ++)
for (int j = 0; j <= t; j ++)
{
f[i][j] = f[i - 1][j];
if (j >= st[i].s)
{
f[i][j] = max(f[i][j], f[i - 1][j - st[i].s] + max(0, st[i].w - st[i].l * (j - st[i].s)));
}
}
int res = -1;
for (int i = 0; i <= t; i ++) res = max(res, f[n][i]);
printf("Case #%d: %d\n", k, res);
}
return 0;
}
/*
题目让我们求一个b数组,使得 sum(a[i] × t[i]) == sum(b[i] * t[i])。
求出数组长度最短的b数组。
算法推进过程,一定是在枚举b数组,并且此时b[1……tm](tm <= m)拼凑出的所有x,这些x a数组也能拼凑出来。
换一种枚举方式。我先枚举b数组,让其等价于a[1……tn](tn <= n)。
没有思路。
题解思路:
通过找规律发现,对a数组的某一个货币,若其金额可以被其它货币拼凑出来,则将它去除掉。进行这个过程,直到货币之间不能被相互拼凑为止,
就得到了b数组。
(我为什么没有思路?因为不知道如何枚举,或者说如何求b数组。)
因此,我们用动态规划,来求出想要拼凑出金额为a[i]的货币,去除a[i]后的a数组能不能拼凑出来,即可。
但是,如果这样思考,假设拼凑出a[i]的货币,必须包含a[i + 1];拼凑出a[i + 1]的货币,必须包含a[i],这样的话,两个货币有一个相互依存的
关系。
如果我们直接将它们剔除,那么,剩余的货币,因为没有a[i + 1],所以拼凑不出a[i];因为没有a[i],所以拼凑不出a[i + 1]。
仔细考虑,这种情况是不存在的。因为a[i]和a[i + 1]有大小之分,所以不会出现这种情况。
因此不要忽略一个基本事实:a[i]如果能被拼凑出来,那么拼凑它的货币金额一定比a[i]小。
同时也有另一种考虑方法:
观察到,货币是有大小的。如果我们从小到大排列,那么拼凑出a[i]的货币a[k],k一定小于等于i。我们只需要考虑a[1……i- 1]能不能拼凑出a[i]。
*/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 105, M = 25500;
int t, n, a[N], tp[N];
int f[N][M];
int main()
{
cin >> t;
while (t --)
{
cin >> n;
for (int i = 1; i <= n; i ++) cin >> a[i];
memset(f, 0, sizeof f);
f[0][0] = 1;
for (int i = 1; i <= n; i ++)
for (int j = 0; j <= 25000; j ++)
{
f[i][j] = f[i - 1][j];
if (j - a[i] >= 0) f[i][j] += f[i][j - a[i]];
}
int res = 0;
for (int i = 1; i <= n; i ++)
{
if (f[n][a[i]] != 1) res ++;
}
cout << n - res << endl;
}
return 0;
}