算法基础复盘笔记Day09【动态规划】—— 背包问题

❤ 作者主页:欢迎来到我的技术博客😎
❀ 个人介绍:大家好,本人热衷于Java后端开发,欢迎来交流学习哦!( ̄▽ ̄)~*
🍊 如果文章对您有帮助,记得关注点赞收藏评论⭐️⭐️⭐️
📣 您的支持将是我创作的动力,让我们一起加油进步吧!!!🎉🎉

第一章 背包问题

一、01背包问题

1. 题目描述

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

第 i 件物品的体积是 v i v_i vi,价值是 w i w_i wi

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

输入格式

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

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

输出格式

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

数据范围

0 < N , V ≤ 1000 0<N,V≤1000 0<N,V1000
0 < v i , w i ≤ 1000 0<v_i,w_i≤1000 0<vi,wi1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

8

2. 思路分析

版本一: 二维数组
(1)状态 f[i][j] 定义: i i i 个物品,背包容量是 j j j 下的最优解(最大价值)

  • 当前的状态依赖于之前的状态,可以理解为从初始状态 f[0][0] = 0 开始决策,有 N 件物品,则需要 N 次决 策,每一次对第 i i i 件物品的决策,状态 f[i][j] 不断由之前的状态更新而来。

(2) 当前背包容量不够(j < v[i]),没得选,因此前 i i i 个物品最优解即为前 i − 1 i−1 i1 个物品最优解:

  • 状态转移方程:f[i][j] = f[i - 1][j]

(3) 当前背包容量够,可以选,因此需要决策选与不选第 i i i 个物品:

  • 选:f[i][j] = f[i - 1][j - v[i]] + w[i]
  • 不选:f[i][j] = f[i - 1][j]
  • 我们的决策是如何取到最大价值,因此以上两种情况取 最大值。

代码如下:

#include<bits/stdc++.h>

using namespace std;

const int N = 1010;
int v[N];    // 体积
int w[N];    // 价值 
int f[N][N];  // f[i][j], j体积下前i个物品的最大价值 

int main() 
{
    int n, m;   
    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 = 1; j <= m; j++)
        {
            //  当前背包容量装不进第i个物品,则价值等于前i-1个物品
            if(j < v[i]) 
                f[i][j] = f[i - 1][j];
            // 能装,需进行决策是否选择第i个物品
            else    
                f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
        }           

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

    return 0;
}

版本二: 一维数组
将状态 f[i][j] 优化到一维 f[j],实际上只需要做一个等价变形。

为什么可以这样变形呢?我们定义的状态 f[i][j] 可以求得任意合法的 i i i j j j 最优解,但题目只需要求得最终状态 f[n][m] ,因此我们只需要一维的空间来更新状态。

(1) 状态 f[j] 定义:N 件物品,背包容量 j j j 下的最优解。

(2) 注意枚举背包容量 j j j 必须从 m m m 开始。

(3) 为什么一维情况下枚举背包容量需要逆序? 在二维情况下,状态 f[i][j] 是由上一轮i - 1的状态得来的,f[i][j]f[i - 1][j] 是独立的。而优化到一维后,如果我们还是正序,则有f[较小体积] 更新到f[较大体积],则有可能本应该用第i-1 轮的状态却用的是第 i 轮的状态。

(4) 例如,一维状态第 i 轮对体积为 3 的物品进行决策,则 f[7]f[4] 更新而来,这里的 f[4] 正确应该是 f[i - 1][4],但从小到大枚举j这里的 f[4] 在第i轮计算却变成了 f[i][4]。当逆序枚举背包容量j时,我们求 f[7] 同样由 f[4] 更新,但由于是逆序,这里的 f[4] 还没有在第 i 轮计算,所以此时实际计算的 f[4] 仍然是 f[i - 1][4]

(5) 简单来说,一维情况正序更新状态 f[j] 需要用到前面计算的状态已经被「污染」,逆序则不会有这样的问题。

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

for(int i = 1; i <= n; i++) 
    for(int j = m; j >= 0; j--)
    {
        if(j < v[i]) 
            f[i][j] = f[i - 1][j];  // 优化前
            f[j] = f[j];            // 优化后,该行自动成立,可省略。
        else    
            f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);  // 优化前
            f[j] = max(f[j], f[j - v[i]] + w[i]);                   // 优化后
    }  

实际上,只有当枚举的背包容量 j>= v[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]);

关于状态 f[j] 的补充说明:
二维下的状态定义 f[i][j] 是前 i i i 件物品,背包容量 j j j 下的最大价值。一维下,少了前 i i i 件物品这个维度,我们的代码中决策到第 i i i 件物品(循环到第i轮),f[j] 就是前i轮已经决策的物品且背包容量 j j j 下的最大价值。

因此当执行完循环结构后,由于已经决策了所有物品,f[j] 就是所有物品背包容量 j j j 下的最大价值。即一维 f[j] 等价于二维 f[n][j]


版本三: 优化输入
我们注意到在处理数据时,我们是一个物品一个物品,一个一个体积的枚举。

因此我们可以不必开两个数组记录体积和价值,而是边输入边处理。
代码如下:

#include<bits/stdc++.h>

using namespace std;

const int N = 1010;
int f[N];  

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

    for(int i = 1; i <= n; i++) {
        int v, w;
        cin >> v >> w;      // 边输入边处理
        for(int j = m; j >= v; j--)
            f[j] = max(f[j], f[j - v] + w);
    }

    cout << f[m] << endl;

    return 0;
}

3. 代码实现

#include <bits/stdc++.h>

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;
}

二、完全背包问题

1. 题目描述

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

i i i 种物品的体积是 v i v_i vi,价值是 w i w_i wi

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

输入格式

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

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

输出格式

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

数据范围

0 < N , V ≤ 1000 0<N,V≤1000 0<N,V1000
0 < v i , w i ≤ 1000 0<v_i,w_i≤1000 0<vi,wi1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

10

2. 思路分析

版本一: 二维数组
状态变量:f[i][j] 表示前 i i i 件物品放入容量为 j j j 的背包的最大价值。

当前背包容量为 j j j,我们要考虑 i i i 件物品能否放入?是否放入?

  1. 当前背包容量 j < w[i],不能放入,则 f[i][j] = f[i - 1][j]

  2. 当前背包容量 j > w[i],能放入,但要考虑代价

    • 若第 i i i 件物品不放入背包,则 f[i][j] = f[i - 1][j]
    • 若第 i i i 件物品放入背包,则 f[i][j] = f[i][j - w[i]] + c[i]

对于前 i i i 件物品,背包容量为 j - w[i] 时可能已经放入了第 i i i 件物品,容量为 j j j 时还可以再放入第 i i i 件物品,所以用 f[i][j - w[i]] 更新 f[i][j]

代码实现如下:

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

版本二: 一维数组
用一维数组 f[j] 只记录一行数据,让 j j j 值顺序循环,顺序更新 f[j]值。

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

实际上,只有当枚举的背包容量 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]);

3. 代码实现

#include <bits/stdc++.h>

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[i]; j <= m; j ++)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    
    cout << f[m] << endl;
    
    return 0;
}

三、多重背包问题I

1. 题目描述

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

i i i 种物品最多有 s i s_i si 件,每件体积是 v i v_i vi,价值是 w i w_i wi

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

输入格式

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

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

输出格式

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

数据范围

0 < N , V ≤ 100 0<N,V≤100 0<N,V100
0 < 0< 0<v_i,w_i,s_i ≤ 100 ≤100 100

输入样例

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

输出样例:

10

2. 思路分析

01背包:第 i i i 种物品可以取0件、取1件。
多重背包:第 i i i 种物品可以取0件、取1件、取2件…取 s i s_i si件。
多重背包问题转化为01背包求解:把第 i i i 种物品换成 s i s_i si 件01背包中的物品,每件物品的体积为 k ∗ v i k * v_i kvi,价值为 k ∗ w i k * w_i kwi(0 ≤ \leq k ≤ \leq s i s_i si)


3. 代码实现

#include <bits/stdc++.h>

using namespace std;

const int N = 110;

int n, m;
int v[N], w[N], s[N];
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
}

四、多重背包问题 II

1. 题目描述

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

i i i 种物品最多有 s i s_i si 件,每件体积是 v i v_i vi,价值是 w i w_i wi

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

输入格式

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

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

输出格式

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

数据范围

0 < N ≤ 1000 0<N≤1000 0<N1000
0 < V ≤ 2000 0<V≤2000 0<V2000
0 < 0< 0<v_i,w_i,s_i ≤ 2000 ≤2000 2000
提示:

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

输入样例

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

输出样例:

10

2. 思路分析

二进制优化方法的例子:
假设有50个苹果,现在要取 n n n 个苹果( n n n ≤ \leq 50),如何取?朴素的作家应该是将苹果一个一个拿出来,知道 n n n 个苹果被取出来。

二进制优化的思想就是:再假设有5个苹果和6只箱子,利用箱子继续某些预备工作,可以在每个箱子中放 2 k 2^k 2k(k ≥ \geq 0)个苹果,也就是1、2、4、8、16、19(剩余的数),取任意 n n n 个苹果时,只需要推出几只箱子就可以了。例如:取20个苹果,只需要拿出2个箱子(1、19),这样的话原来20次操作的现在变成2次操作就可以了。

二进制拆分思想:
将第 i i i 种物品拆分成若干件物品,每件物品的体积和价值乘以一个拆分系数(1, 2 1 2^1 21, 2 2 2^2 22 2 k − 1 2^{k-1} 2k1, s i − 2 k + 1 s_i - 2^k + 1 si2k+1),就可以转化成01背包的物品求解。

例如: s i = 12 s_i = 12 si=12,拆分系数为 1 , 2 , 4 , 5 1,2,4,5 1,2,4,5,转化成4件01背包的物品: ( v i , w i ) (v_i, w_i) (vi,wi) ( 2 v i , 2 w i ) (2v_i, 2w_i) (2vi,2wi) ( 4 v i , 4 w i ) (4v_i, 4w_i) (4vi,4wi) ( 5 v i , 5 w i ) (5v_i, 5w_i) (5vi,5wi)


3. 代码实现

#include <bits/stdc++.h>

using namespace std;

//逐一枚举到最大是 N * logN
const int N = 12010, M = 2010;

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

int main()
{
    cin >> n >> m;
    
    int cnt = 0; //分组的类别
    for (int i = 1; i <= n; i ++)
    {
        int 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个
            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;
}

五、分组背包问题

1. 题目描述

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

每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 v i j v_{ij} vij,价值是 w i j w_{ij} wij,其中 i i i 是组号, j j j 是组内编号。

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

输出最大价值。

输入格式

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

接下来有 N 组数据:

  • 每组数据第一行有一个整数 S i S_i Si,表示第 i i i 个物品组的物品数量;
  • 每组数据接下来有 S i S_i Si 行,每行有两个整数 v i j , w i j v_{ij},w_{ij} vij,wij,用空格隔开,分别表示第 i i i 个物品组的第 j j j 个物品的体积和价值;

输出格式

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

数据范围

0 < N , V ≤ 100 0<N,V≤100 0<N,V100
0 < S i ≤ 100 0<S_i≤100 0<Si100
$0< v i j , w i j v_{ij},w_{ij} vij,wij≤100$

输入样例

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

输出样例:

8

2. 思路分析

最大价值应该是物品组 i i i 和背包容量 j j j 的函数,用 f[i][j] 表示前 i i i 组物品,能放入容量为 j j j 的背包的最大价值。

朴素算法应该是循环物品组,循环背包容量,对第 i i i 组物品,容量为 j j j 的背包,有 s + 1 s + 1 s+1 种选法,

max(f[i - 1][j], f[i - 1][j - v 1 v_1 v1] + w 1 w_1 w1, f[i - 1][j - v 2 v_2 v2] + w 2 w_2 w2,…,f[i - 1][j - v 5 v_5 v5] + w 5 w_5 w5)

代码如下:

	// 背包问题,朴素算法
    for (int i = 1; i <= n; i ++) //物品
        for (int j = 1; j <= m; j ++) //体积
            for (int k = 0; k < s[i]; k ++) //决策
                if (v[i][k] <= j)
                    f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
    
    cout << f[n][m] << endl;

可以优化为一维数组:

    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;

3. 代码实现

#include <bits/stdc++.h>

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;
}

创作不易,如果有帮助到你,请给文章点个赞和收藏,让更多的人看到!!!
关注博主不迷路,内容持续更新中。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java技术一点通

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值