动态规划:背包问题

16 篇文章 0 订阅
2 篇文章 0 订阅

01背包

题面

n n n种物品要放到一个袋子里,袋子的总容量为 m m m,第 i i i种物品的体积为 v i v_i vi,把它放进袋子里会获得 w i w_i wi的收益,每种物品至多能用一次,问如何选择物品,使得在物品的总体积不超过 m m m的情况下,获得最大的收益?请求出最大收益。

暴力枚举

枚举:我们可以用一个01串表示每个物品取还是不取,对于每一个串,我们取总体积小于等于m的方案,然后取总收益最大的

动态规划

当我们在考虑了前 i i i个物品取或不取,如果有两组方案的总体积相同,那么我们只需要保留总收益大的那组。

考虑前 i i i个物品,总体积为 j j j时分为两种情况:

i i i个物品没取,问题变成考虑前 i − 1 i-1 i1个物品,总体积为 j j j时的情况

i i i个物品取了,问题变成考虑前 i − 1 i-1 i1个物品,总体积为 j − v i j-v_i jvi时的情况

for (int i = 1; i <= n; i++)
    for (int j = 0; j <= m; j++)
        if (j < v[i])
            f[i][j] = f[i - 1][j];
        else
            f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);

滚动数组优化(空间优化)

考虑前 i i i个物品时的状态只和考虑前 i − 1 i-1 i1个物品时状态有关,前 i − 2 i-2 i2都不用记。

for (int i = 1; i <= n; i++)
{
    for (int j = 0; j <= m; j++)
        if (j < v[i])
            g[j] = f[j];
        else
            g[j] = max(f[j], f[j - v[i]] + w[i]);
    memcpy(f, g, sizeof g);
}

思路优化

我们将f数组和g数组合并可以发现,在j < v[i]的时候g[j] = f[j],那么我们只需要从后向前逐渐更新f数组,就能将f数组更新成g数组(如果从前向后,可能会使后面更新所需要的旧值已经被更新成新的值,那么边会发生错误)。

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]);

完全背包

题面

n n n种物品要放到一个袋子里,袋子的总容量为 m m m,第i种物品的体积为 v i v_i vi,把它放进袋子里会获得 w i w_i wi的收益,每种物品能用无限多次,问如何选择物品,使得在物品的总体积不超过 m m m的情况下,获得最大的收益?请求出最大收益。

思路

把考虑了前 i i i种物品以后,总体积为0,1,2,3,……,m时的最大收益记录下来。

考虑前 i i i个物品,总体积为 j j j时分为两种情况:

i i i个物品没取,问题变成考虑前 i − 1 i-1 i1个物品,总体积为 j j j时的情况

i i i个物品取了,问题变成考虑前 i i i个物品,总体积为 j − v i j-v_i jvi时的情况

for (int i = 1; i <= n; i++)
{
    for (int j = 0; j <= m; j++)
        if (j < v[i])
            f[i][j] = f[i - 1][j];
        else
            f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]);
}

思路优化

在更新f[i][j]的时候,f[i][j] = max(f[i-1][j],f[i][j-v[i]]+w[i]),我们需要用到f[i][j-v[i]]也就是更新过后的f[j-v[i]],所以需要从前向后更新(如果从后向前更新,有可能用到还没有进行更新的数据也就是f[i-1][j-v[i]])。

for (int i = 1; i <= n; i++)
    for (int j = v[i]; j <= m; j++)
        f[j] = max(f[j], f[j - v[i]] + w[i]);

恰好装满的完全背包问题

memset(f, 128, sizeof f);     //将f数组初始化成负无限大
f[0] = 0;
for (int i = 1; i <= n; i++)
    for (int j = v[i]; j <= m; j++)
        f[j] = max(f[j], f[j - v[i]] + w[i]);

多重背包

题面

n n n种物品要放到一个袋子里,袋子的总容量为 m m m,第 i i i种物品的体积为 v i v_i vi,把它放进袋子里会获得 w i w_i wi的收益,可以用 l i l_i li次。问如何选择物品,使得在物品的总体积不超过 m m m的情况下,获得最大的收益?请求出最大收益。

数据规模小

可以将一个物品拆分成 l i l_i li个物品,每个物品只能拿一次,转换成01背包问题解决。

for (int i = 1; i <= n; i++)
    for (int k = 1; k <= l[i]; k++)
        for (int j = m; j >= v[i]; j--)
            f[j] = max(f[j], f[j - v[i]] + w[i]);

数据规模大

定理:令 t 为最大的 i 满足 2 i + 1 − 1 ≤ n 2^{i+1}−1≤n 2i+11n,我们从 1 , 2 , 4 , . . . , 2 t , n − 2 t + 1 + 1 1,2,4,...,2^t,n−2^{t+1}+1 1,2,4,...,2t,n2t+1+1 中选一些数字相加(可以不选),可以得出任意 [0,n] 内的值,每个数字只能用一次。

对于第 i i i 种物品,它可以取 l i l_i li 次,我们可以按上述定理的方法把它拆成 O ( l o g l ) O(log l) O(logl) 个物品,每个物品是取若干次第 i i i 种物品的打包,每个打包完的物品只能取一次。从这 O ( l o g l ) O(log l) O(logl) 个物品里选一些,可以表示出第 i i i种物品不取,取 1 次,取 2 次,…,取 l i l_i li 次所有这些情况。

for (int i = 1; i <= n; i++)
{
    int res = l[i];
    for (int k = 1; k <= res; res -= k, k *= 2)
        for (int j = m; j >= v[i] * k; j--)
            f[j] = max(f[j], f[j - v[i] * k] + w[i] * k);
    for (int j = m; j >= v[i] * res; j--)
        f[j] = max(f[j], f[j - v[i] * res] + w[i] * res)
}

单调队列

单调队列优化

时间复杂度: O ( n m l ) O(nml) O(nml)

i i i固定的情况下,我们把 j j j按照 分类,只有同一类的状态间可以进行转移;

在第 i − 1 i - 1 i1行的某一类中下标为 x x x的位置 p p p,可以转移到第 i i i 行下标在 [ x + 1 , x + l i ] [x+1,x+l_i] [x+1,x+li]中的位置;

下标为 x x x的位置会在 x + l i x+l_i x+li时刻消失,每个位置都要取最大值;

我们要转移到下标为y的位置o,对应的影响是 f [ i − 1 ] [ p ] + ( y − x ) ∗ w i f[i-1][p]+(y-x)*w_i f[i1][p]+(yx)wi,展开式子得 f [ i − 1 ] [ p ] − x ∗ w i + y ∗ w i f[i-1][p]-x*w_i+y*w_i f[i1][p]xwi+ywi,其中 y ∗ w i y*w_i ywi这项只和要转移到的位置的下标 y y y有关,与 x x x没有任何关系,我们只要在算 f [ i ] [ o ] f[i][o] f[i][o]的时候把它加上去就好了。剩下的 f [ i − 1 ] [ p ] − x ∗ w i f[i-1][p]-x*w_i f[i1][p]xwi x x x, p p p固定的时候是一个定值。

#include <bits/stdc++.h>
using namespace std;
int n, m, v, w, t, f[10001], c[10001][2];
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
    {
        scanf("%d%d%d", &v, &w, &t); // t是题目中的l,为了防止和队首位置的变量名重复,所以用t来表示
        for (int j = 0; j < v; j++) // j枚举了mod vi后是第几个分类。
        {
            int k = 0, l = 1;
            for (int p = j, x = 1; p <= m; p += v, ++x) //依次枚举第j类的位置p和它的下标x
            {
                int e = f[p] - x * w, r = x + t;
                while (k >= l && c[k][0] <= e)
                    --k;
                c[++k][0] = e;
                c[k][1] = r;
                f[p] = c[l][0] + x * w;
                while (k >= l && c[l][1] == x)
                    ++l;
            }
        }
    }
    printf("%d\n", f[m]);
}

分组背包

题面

n n n种物品要放到一个袋子里,袋子的总容量为 m m m。第 i i i个物品属于第 a i a_i ai组,每组物品我们只能从中选择一个。第i种物品的体积为 v i v_i vi,把它放进袋子里会获得 w i w_i wi的收益。问如何选择物品,使得在物品的总体积不超过 m m m的情况下,获得最大的收益?请求出最大收益。

思路

让我们来看看大概的思路,我们把考虑了前 i 组物品(编号小于等于 i 的组)以后,总体积为 0,1,…,m 时的最大收益都记下来

考虑了前 i 组物品,总体积为 j 时分为两种情况:

i i i组物品一个没取,问题变成了考虑了前 i − 1 i−1 i1组物品,总体积为 j j j时的情况;

i i i组物品取了,我们枚举取了其中的物品 k k k,问题变成了考虑了前 i − 1 i−1 i1组物品,总体积为 j − v k j−v_k jvk 时的情况;

int n, m, v[N], w[N], a[N], f[N][N];
vector<int> c[N];

for (int i = 1; i <= 1000; i++)
    for (int j = 0; j <= m; j++)
    {
        f[i][j] = f[i - 1][j];
        for (auto k : c[i])
            if (v[k] <= j)
                f[i][j] = max(f[i][j], f[i - 1][j - v[k]] + w[k]);
    }

优化

同01背包优化

int n, m, v[N], w[N], a[N], f[N];
vector<int> c[N];

for (int i = 1; i <= 1000; i++)
    for (int j = m; j; j--)
        for (auto k : c[i])
            if (v[k] <= j)
                f[j] = max(f[j], f[j - v[k]] + w[k]);

二维背包

题面

n n n种物品要放到一个袋子里,袋子的总容量为 m m m,我们一共有k点体力值。第 i i i种物品的体积为 v i v_i vi,把它放进袋子里会获得 w i w_i wi的收益,并且消耗我们 t i t_i ti点体力值,每种物品只能取一次。问如何选择物品,使得在物品的总体积不超过 m m m并且花费总体力不超过 k k k的情况下,获得最大的收益?请求出最大收益。

思路

把考虑了前 i i i个物品以后,总体积为 0 , 1 , … , m 0,1,…,m 0,1,,m,总体力为 0 , 1 , … , k 0,1,…,k 0,1,,k时的最大收益都记下来。

考虑了前 i i i个物品,总体积为 j j j,总体力为 x x x时分为两种情况:

i i i个物品没取,问题变成了考虑了前 i − 1 i−1 i1个物品,总体积为 j j j,总体力为 x x x时的情况;

i i i个物品取了,问题变成了考虑了前 i − 1 i−1 i1个物品,总体积为 j − v i j−v_i jvi,总体力为 x − t i x−t_i xti时的情况;

for (int i = 1; i <= n; i++)
    for (int j = 0; j <= m; j++)
        for (int x = 0; x <= k; x++)
        {
            f[i][j][x] = f[i - 1][j][x];
            if (j >= v[i] && x >= t[i])
                f[i][j][x] = max(f[i][j][x], f[i - 1][j - v[i]][x - t[i]] + w[i]);
        }

优化

同01背包优化

for (int i = 1; i <= n; i++)
    for (int j = m; j >= v[i]; j--)
        for (int x = k; x >= t[i]; x--)
            f[j][x] = max(f[j][x], f[j - v[i]][x - t[i]] + w[i]);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值