学不会动态规划——背包篇

clear_tea / 2023-07-06 / 原文

前言

终于把线性动态规划学完了,本蒟蒻要开始背包了,祝我好运吧!如果文章有任何问题,欢迎评论或者私信让我知道🌹。

[NOIP2001 普及组] 装箱问题

题目描述

有一个箱子容量为 \(V\),同时有 \(n\) 个物品,每个物品有一个体积。

现在从 \(n\) 个物品中,任取若干个装入箱内(也可以不取),使箱子的剩余空间最小。输出这个最小值。

输入格式

第一行共一个整数 \(V\),表示箱子容量。

第二行共一个整数 \(n\),表示物品总数。

接下来 \(n\) 行,每行有一个正整数,表示第 \(i\) 个物品的体积。

输出格式

  • 共一行一个整数,表示箱子最小剩余空间。

样例 #1

样例输入 #1

24
6
8
3
12
7
9
7

样例输出 #1

0

我的代码

#include<iostream>
#define int long long
const int N = 2e5 + 5;
bool dp[N];
signed main(){
    int v,n;
    std::cin>>v>>n;
    int ans[n + 1];
    for(int i  =1;i <= n;i ++) {
        std::cin >> ans[i];
    }
    dp[0] = true;
    for(int i = 1;i <=n ;i ++){
        for(int j = v;j >= ans[i];j --){
            dp[j] = dp[j] || dp[j - ans[i]];
        }
    }
    for(int i = v;i >= 0;i --){
        if(dp[i]){
            std::cout<<v - i<<"\n";
            return 0;
        }
    }
    return 0;
}

解题思路

1、先考虑二维数组dp[i][j],表示在继承前面所有状态的情况下,第i个物品是否放进背包的两种状态下,背包所用的体积j

2、因为每个物品都可以选择放或者不放(注意放入背包的条件是剩余的空间充足),所有dp[i][j]可以继承dp[i-1][j]或者dp[i - 1][j - v[i]]的状态

3、不难发现,dp[i - 1][j]为true的话,dp[i][j]必定为true,dp[i][j]是在dp[i - 1][j]的基础上进行叠加。所以可以优化为一维数组dp[i],表示背包中使用了i体积的状态

4、一维数组的状态转移方程可以优化为dp[j] = dp[j] || dp[j - v[i]],需要注意的是,前面的状态会对后面的状态造成影响,所以,我们从后向前遍历。又因为继承了前面的状态,所以当j - v[i] < 0时的状态不需要进行修改。

01背包

题目描述

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

样例

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

8

我的代码

#include<iostream>
#include<math.h>
#define int long long
const int N = 1e3 + 5;
int dp[N];
signed main(){
    int V,n,ans = 0;
    std::cin >> n >> V;
    int v[n + 1],w[n + 1];
    for(int i = 1;i <= n;i ++) {
        std::cin >> v[i] >> w[i];
    }
    for(int i = 1;i <=n ;i ++){
        for(int j = V; j >= v[i]; j --){
            dp[j] = std::max(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    std::cout<<dp[V]<<"\n";
    return 0;
}

解题思路

基本和上面一样,只不过数组内存的值变成了当前价值。

完全背包

题目描述

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

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

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

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤1000
0<vi,wi≤1000

样例

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

10

解题思路

完全背包其实就是01背包的bug版,还记得01背包的内层循环是从哪开始的吗?从后开始遍历是为了规避前面放入物品的操作对后面的影响,因为01背包的物品只能放入一次,是不是感觉晕乎乎的,不知道我在说啥?那就画个表来理解一下

因为dp[6] = max(dp[6],dp[6 - 3] + w[3]),同理可知dp[9] = 12。不难发现,这样表示的含义就是每个物品都能放无限次,恰好符合完全背包的题目定义。

多重背包1⃣️

题目描述

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

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

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

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

输出格式

输出一个整数,表示最大价值。

样例

输入样例

4 5
1 2 3
2 4 1
3 4 3
4 5 2

输出样例:

10

我的代码

#include<iostream>
#include<math.h>
#define int long long
const int N = 1e3 + 5;
int dp[N];
signed main(){
    int V,n;
    std::cin >> n >> V;
    int v[n + 1],w[n + 1],s[n + 1];
    for(int i = 1;i <= n;i ++) {
        std::cin >> v[i] >> w[i] >> s[i];
    }
    for(int i = 1;i <= n;i ++){
        for(int k = 1;k <= s[i];k ++) {
            for (int j = V;j >= v[i] * k;j --) {
                dp[j] = std::max(dp[j], dp[j - v[i]]+ w[i]);
            }
        }
    }
    std::cout<<dp[V]<<"\n";
    return 0;
}

解题思路

不难发现是完全背包的一点点小变化的版本,从不限制个数到限制个数,只需要把完全背包的朴素版稍作修改就可以了。

蒟蒻被坑全过程:
本蒟蒻因为刚刚在01背包被坑了,所以记忆深刻,写完全背包的时候直接就想到了优化版,导致本蒟蒻好像并不会朴素版的完全背包。在自己搓多重背包的时候,把第二层循环和第三层循环写反了,wa了不知道多久之后,打开了神犇的blog(指路),抱着试试的心态,把位置换了一下,成功AC了,但是本蒟蒻人菜胆大,始终觉得先按个数放,再看剩余空间思路上没有问题,肯定是某个小细节没有考虑到!

One hour later……在神犇的提点下,本蒟蒻幡然醒悟。

回忆一下,当前消耗体积j下的最优的状态必定是由消耗体积j - v[i]的最优状态的转化来的,所以……第二层和第三层循环不能交换

好的,事情出现了转机。

在写……的部分的时候,我还是感觉怪怪的,就拿被我缝缝补补几次的代码出来,想跑一组数据来输出整个变化过程的表格,来看看到底在哪里出了问题。结果发现这组数据居然对了,交上去测评试了一下,居然AC了。所以我真的是有小细节没有考虑到:1、k必须从1开始,如果从0开始的话,k = 0时会全部被覆盖2、状态转移方程为:dp[j] = std::max(dp[j], dp[j - v[i]]+ w[i])而非dp[j] = std::max(dp[j], dp[j - v[i] * k]+ w[i] * k)

这里是本蒟蒻的调了不知道多久的代码

#include<iostream>
#include<math.h>
#define int long long
const int N = 1e3 + 5;
int dp[N];
signed main(){
    int V,n;
    std::cin >> n >> V;
    int v[n + 1],w[n + 1],s[n + 1];
    for(int i = 1;i <= n;i ++) {
        std::cin >> v[i] >> w[i] >> s[i];
    }
    for(int i = 1;i <= n;i ++){
        for(int k = 1;k <= s[i];k ++) {
            for (int j = V;j >= v[i] * k;j --) {
                dp[j] = std::max(dp[j], dp[j - v[i]]+ w[i]);
            }
        }
    }
    std::cout<<dp[V]<<"\n";
    return 0;
}

多重背包2⃣️

题目描述

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

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

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

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N≤1000
0<V≤2000
0<vi,wi,si≤2000

提示:

本题考查多重背包的二进制优化方法。

样例

输入样例

4 5
1 2 3
2 4 1
3 4 3
4 5 2

输出样例:

10

我的代码

#include<iostream>
#include<math.h>
#define int long long
const int N = 1e5 + 5;
int dp[N];
int v[N],w[N];
signed main(){
    int V,n;
    std::cin >> n >> V;
    int cnt = 0;
    for(int i = 1;i <= n;i ++){
        int a,b,s;
        std::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;
            s -= s;
        }
    }
    for(int i = 1;i <= cnt;i ++){
        for (int j = V;j >= v[i];j --) {
            dp[j] = std::max(dp[j], dp[j - v[i]]+ w[i]);
        }
    }
    std::cout<<dp[V]<<"\n";
    return 0;
}

解题思路

刚开始完全没思路,看了神犇的题解都不理解什么意思,理解了好一会才知道啥意思,咱们这就重点以蒟蒻的视角,来解释一下,我们不妨设物品的体积为v,价值为w,
有a个,那么我们可以将a用二进制表示即a = 2 ^ 0 + 2 ^ 1 + 2 ^ 2 + .... + 2 ^ n + t(余数),将其分别作为新的物品存储起来,代码上可以看得很清楚。不知道有没有萌新跟我一样,不理解这样分组是不是不能能覆盖到全部的情况。那我们画个表就清楚了!

可以看到如果当取3给为最优时,我们可以取a[0]和a[1],因为2进制可以表示所有10进制的数,所以无论最优应该取几,都能由a拆分的数组组合而成,类似于01背包的操作。

后记

虽然还有其他的背包的内容,但是时间有限,背包的内容暂时告一段落啦,完结撒花🎉

如果有任何问题欢迎随时告诉我,感激不尽🌹