动态规划之背包问题

(随时更新)
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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值