5.1 动态规划 | 背包问题

5.1 动态规划 | 背包问题

这是我的一个算法网课学习记录,道阻且长,好好努力

动态规划是一种思路,需要一步一步的思考。

背包问题

在这里插入图片描述

01背包

问题描述:

有一个容量为V的背包,还有n个物体。现在忽略物体实际几何形状,我们认为只要背包的剩余容量大于等于物体体积,那就可以装进背包里。每个物体都有两个属性,即体积w和价值v。
问:如何向背包装物体才能使背包中物体的总价值最大?

在这里插入图片描述

状态转移方程是f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i])

例题: ACWing 2.01背包问题

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

Ans_朴素版

使用二维进行表示

这里,我们使用v表示体积,w表示价值

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 10010;

int n, m;
int v[N], w[N]; // v表示体积 w表示价值
int f[N][N];

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 = 0; j <= m; j ++ )
        {
            f[i][j] = f[i - 1][j];
            if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]); // 右边的集合不一定存在 因为可能装不下
        }

    cout << f[n][m] << endl;

        return 0;


}
Ans_滚动数组优化

滚动数组(简单说明)Andy01_的博客-CSDN博客

通过观察dp方程来判断需要使用哪些数据,可以抛弃哪些数据,一旦找到关系,就可以用新的数据不断覆盖旧的数据量来减少空间的使用。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

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 -- ) // 倒序更新 
            f[j] = max(f[j], f[j - v[i]] + w[i]);

    cout << f[m] << endl;

    return 0;
}

一维01背包为什么倒序:

根据状态转移方程f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]),我们每次计算f[j] (即f[i][j]) 的时候都会需要f[j-v[i]] (即dp[i-1][j-v[i]])的值。因为j-v[i]j小,也就是前面的值,所以如果我们正序计算,那么f[j-v[i]]就已经更新了 (即f[i][j-v[i]]),与状态转移方程不符,因此需要倒着更新。

如果重量是负数,怎么处理?

如果重量是负数,那么上述状态转移方程中j-v[i]就会比j大,为避免先更新dp[j-v[i]]我们只能正序更新数组。

完全背包

例题:AcWing 3. 完全背包问题

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。

第 i 种物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

Ans_朴素版
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N][N];

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 = 0; j <= m; j ++ )
            for (int k = 0; k * v[i] <= j; k ++ )
                f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);

    cout << f[n][m] << endl;

    return 0;
}

由于有内部更新次序的问题,通过观察可以发现:

f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....)
f[i , j-v] = max( f[i-1,j-v] , f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , .....)
由上两式,可得出如下递推关系:
f[i][j] = max(f[i,j-v]+w , f[i-1][j])

因此可以对于核心的更新代码进行一个优化。

Ans_降维优化
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N][N];

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 = 0; j <= m; j ++ )
        {
            f[i][j] = f[i - 1][j]; // 优化了一层循环
            if (j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
        }

    cout << f[n][m] << endl;

    return 0;

}

将上面的这些代码与01背包问题的代码进行比较:

f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);//01背包

f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);//完全背包问题

可以想到使用滚动数组对于算法进行进一步的优化

Ans_滚动数组优化
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;


int n, m;
int v[N], w[N];
int f[N];

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 = v[j]; j <= m; j ++ )
            f[j] = max(f[j], f[j - v[i]] + w[i]); // 进一步优化,滚动数组

    cout << f[m] << endl;

    return 0;
}

注意,由于需要使用前面已经更新的数据,这里是从前往后更新,跟01背包问题的滚动优化不一样。第二层循环是正序。

多重背包问题

例题:AcWing 5.多重背包问题 I

有 N 种物品和一个容量是 V 的背包。

第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。

输出最大价值。

Ans_朴素版
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110; // 小的数据

int n, m;
int v[N], w[N], s[N]; // s表示这件物品有多少个
int f[N][N];

int main()
{
    cin >> n >> m;

    for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i] >> s[i];

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

    cout << f[n][m] << endl;
    return 0;
}

多重背包问题 II 优化方法详解

不能直接使用完全背包的优化方法进行优化,因为:

// 完全背包
f[i][j] = max(f[i-1][j], f[i-1][j-v]+w, f[i-1][j-2v]+2w, f[i-1][j-3v]+3w,.., f[i-1][j-kv]+kw)
f[i][j-v] = max(         f[i-1][j-v],   f[i-1][j-2v]+w,  f[i-1][j-3v]+2w,.., f[i-1][j-kv]+(k-1)w)
// 个数无限,只受背包容积j限制,而每次都是同步的 k 个 v[i]

// 多重背包
f[i,j] = max(f[i-1,j], f[i-1,j-v]+w, f[i-1,j-2v]+2w,.., f[i-1,j-sv] + sw ) 
f[i,j-v] = max(        f[i-1,j-v],   f[i-1,j-2v]+w,.., f[i-1,j-sv]+(s-1)w, f[i-1,j-(s+1)v]+sw )
// 受个数限制,永远最后多一个

完全背包由于对每种物品没有选择个数的限制,所以只要体积够用就可以一直选,没有最后一项,相当于f[i][j]f[i][j - v[i]]中对于第 i 个物品每次多拿的个数 k 是同步的,容积都是j - k *v[i]

多重背包也不受容积的限制,但是多重背包每种物品的数量的是有范围的,当容积足够时,f[i][j - v[i]]一定会多出个 f[i-1, j-(s+1)v] + sw

知道了f[i][j - v[i]]的最大值,也知道了最后一项f[i-1, j-(s+1)v] + sw的最大值,但是无法得出前面若干项的最大值,因为这是一个 max:求所有项的最大一项,所以不能选择这种优化方法。

使用二进制的优化方式:

通过二进制拆分的方式,分成logs个数,由二进制的性质,可以转化出0~s的物品的情况。

用logs个物品代替原来s个物品,转化为01背包问题,打包的每个组作为一个物品最多拿一次

Ans_二进制优化
#include <iostream>
#include <algorithm>

using namespace std;

// 2000 * log2000 == 22000
const int N = 25000, M = 2010;

int n, m;
int v[N], w[N]; // v 表示体积  w表示价值
int f[M]; // dp状态函数

int main()
{
    cin >> n >> m;

    int cnt = 0;
    for (int i = 1; i <= n; i ++ )
    {
        int a, b, s; // a 表示体积  b 表示价值  s表示个数
        cin >> a >> b >> s;

        // 进行二进制拆分打包
        int k = 1;
        while (k <= s)
        {
            cnt ++ ;
            v[cnt] = a * k;
            w[cnt] = b * k;
            s -= k;
            k *= 2;
        }
        // 二进制拆分的余数进行打包
        if (s > 0)
        {
            cnt ++ ;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }

    n = cnt;

    // 进行01背包的遍历
    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]);

    cout << f[m] << endl;

    return 0;
}

分组背包问题

例题:AcWing 9. 分组背包问题

有N组物品和一个容量是V的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是vij,价值是 wij,其中i是组号,j是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。输出最大价值。

状态计算可以这样思考,划分为:不选第i组物品,选第i组物品的第k个物品

Ans
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110;

int n, m;
int v[N][N], w[N][N], s[N];
int f[N];

int main()
{
    cin >> n >> m;

    for (int i = 1; i <= n; i ++ )
    {
        cin >> s[i];
        for (int j = 0; j < s[i]; j ++ )
            cin >> v[i][j] >> w[i][j];
    }

    for (int i = 1; i <= n; i ++ )
        for (int j = m; j >= 0; j --  ) // 从大到小枚举 因为要用到之前的数据
            for (int k = 0; k < s[i]; k ++ )
                if (v[i][k] <= j)
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);

    cout << f[m] << endl;

    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值