01背包一维优化还在迷茫?进来看(数值+图像)全过程模拟

269 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情

看完如果还有疑问,评论区喷我[doge](狗头保命) ,如果没有疑问的话,别忘了点赞噢!!!

image.png

动态规划问题解决方法是拆解成小问题来解决。

01背包通俗的理解

假设你有一个女朋友,你的女朋友让你买东西(她跟你说:要最贵的),背包可以装体积为 4m³ 的物品。求用这个背包最多能装东西的最大价值。 下面有三件物品: | 物品名称 | 香水 | 眉笔 | 口红 | |--|--|--|--|--| | 价格(元) | ¥3 | ¥2 | ¥1.5 | | 体积(dm³) | 4 | 3 | 1 |

现在让你选择,用这个背包装走价值最高的物品,应该如何选择?

暴力法 (不会吧不会吧还有人用暴力法???)

好吧,那就暴力一下? 所有的选法如下:

序号物品名称总价格体积(m³)是否能装得下
¥0V = 0m³×
香水¥3V = 4m³
眉笔¥2V = 3m³
口红¥1.5V = 1m³
香水 + 口红¥4.5V = 5m³×
香水 + 眉笔¥5V = 7m³×
眉笔 + 口红¥3.5V = 4m³
香水 + 眉笔 + 口红¥6.5V = 8m³×

暴力的方法:很明显 —— 很慢 只有三个物品,就要考虑 8 种可能的情况。 如果有 n 件物品,就有 2n2^n 种可能的情况。 时间复杂度为:O(2n)O(2^n) !!!!

这不行这不行,受不了受不了,太慢了。

所以引入了动态规划的思想。

动态规划法(难道还有人不用动态规划???)

动态规划的核心思想:先解决子问题,再一步一步最终解决大问题。

那么,对于这个背包问题,我们可以先拆成小背包。

在这里插入图片描述

首先看第一行,意思就是,现在要将口红装入背包里面。 换句话说:假装现在 你女朋友 目前只让你买口红。(如果有例外的话,那就是你不听你女朋友的话,那我无话可说) 当体积为1的时候:口红 口红的体积为:1m³ 那么依次有: ①这个小背包有 1m³ 可以装得下口红 (1m³) 在这里插入图片描述

② 这个小背包有 2m³ 可以装得下口红 (1m³) 在这里插入图片描述

③ 这个小背包有 3m³ 可以装得下口红 在这里插入图片描述

④ 这个小背包有 4m³ 可以装得下口红 在这里插入图片描述

好的!看第二行,现在 你女朋友 允许你装香水了。

开始装香水整个过程为:

在这里插入图片描述

当背包容量为4的时候,可以装入香水(香水的体积为:4m³) 则更新最大价值为:¥3。

当你女朋友叫你装眉笔的时候:

在这里插入图片描述

一直到最终解,逐步修改最大值。

核心思想

dp[i, j] = max(上一个单元格的值 dp[i - 1][j] ,当前商品的价值 + 剩余空间的价值 dp[i-1][j - v[i]] + w[i])

在这里插入图片描述

原理

题意: 每件物品仅用一次,共N件物品,容量为V的背包。每件物品只能用一次。

将f[i]表示的所有选法分成两大类(划分原则:不漏)

  1. 选法中不含i,即从1~i-1中选,且总体积不超过j,即“f[i-1][j]”
  2. 选法中包含i,即从1~i中选,包含i,且总体积不超过“j”,即"f[i][j]"

可以先把第i个物品拿出来,即从第1~i-1中选,且总体积不超过“j-v[i]”

即:f[i-1][j-v[i]]+w[i]; 所以:f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);

例题

总共: 4件物品且背包容量为5

物品编号体积价值
12
24
34
45

则应该选,中间两样东西,体积之和为5,价值之和为8

最朴素的做法(二维空间)

一维表示价值,一维表示体积,两重for循环

#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int f[N][N];
int v[N], w[N];
int n, m;
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 = 1; 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;
}

可优化的点

可以观察得出: f[i]只用到了f[i-1]这一层,即f[i-2]f[0]f[i]是完全没有用的

所以第二层循环可以直接从v[i]开始

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

从二维优化成一维过程如下图

image.png

如果直接删除掉f[i]这一维即f[j]=max(f[j],f[j-v[i]]+w[i]);

如果删掉f[i]这一维,结果如下:(如果j层循环是递增的,则是错误的)

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

证明:j层循环是递增的,则是错误的

原式:f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);

改成一维:f[j]=max(f[j],f[j-v[i]]+w[i]);

由于f[i][]只跟上一状态(f[i-1][])有关 上面两个式子 :这一状态(左)=上一状态(右)

f[i][j]是由f[i-1][j-v[i]]推出来的现在进行空间优化,那么必须要保证f[j]要由f[j-v[i]]推出来的。

如果j层循环是递增的,则相当于f[i][j]变得是由f[i][j-v[i]]推出来的,而不是f[i-1][j-v[i]]推出来的。

优化过程数值全模拟

例子:假设有3件物品,背包的总体积为10

物品编号体积价值
45
56
67

因为f[0][j]总共0件物品,所以最大价值为0,即f[0][j]==0成立

    如果 j 层循环是递增的: 
    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]);
        }
    }

当还未进入循环时:

f[0]=f[1]=f[2]=f[3]=f[4]=f[5]=f[6]=f[7]=f[8]=f[9]=f[10]=0;

当进入循环i==1时:

f[4]=max(f[4],f[0]+5);即max(0,5)=5;即f[4]=5;

f[5]=max(f[5],f[1]+5);即max(0,5)=5;即f[5]=5;

f[6]=max(f[6],f[2]+5);即max(0,5)=5;即f[6]=5;

f[7]=max(f[7],f[3]+5); 即max(0,5)=5;即f[7]=5;

!!!!!!!!!!!!!重 点 来 了!!!!!!!!!!!!!!!!

f[8] = max(f[8], f[4] + 5); 即max(0, 5 + 5) = 10; 即f[8] = 10;(这里的f[4]已经被f[0]更新过了,所以×)

这里就已经出错了,因为此时处于i==1这一层,即物品只有一件,不存在单件物品满足价值为10

所以已经出错了。

如果j层循环是逆序的:

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

数值模拟过程如下:

1. 还未进入循环

    f[0] = 0;  f[1] = 0;  f[2] = 0;  f[3] = 0;  f[4] = 0;  
    f[5] = 0;  f[6] = 0;  f[7] = 0;  f[8] = 0;  f[9] = 0; f[10] = 0;

2. 当进入循环 i==1

w[i] = 5; v[i] = 4;

j=10:f[10]=max(f[10],f[6]+5);即max(0,5)=5;即f[10]=5;

j=9:f[9]=max(f[9],f[5]+5);即max(0,5)=5;即f[9]=5;

j=8:f[8]=max(f[8],f[4]+5);即max(0,5)=5;即f[8]=5;

j=7:f[7]=max(f[7],f[3]+5);即max(0,5)=5;即f[7]=5;

j=6:f[6]=max(f[6],f[2]+5);即max(0,5)=5;即f[6]=5;

j=5:f[5]=max(f[5],f[1]+5);即max(0,5)=5;即f[5]=5;

j=4:f[4]=max(f[4],f[0]+5);即max(0,5)=5;即f[4]=5;

3. 当进入循环 i==2:

w[i]=6;v[i]=5;

j=10:f[10]=max(f[10],f[5]+6);即max(5,11)=11;即f[10]=11;

j=9:f[9]=max(f[9],f[4]+6);即max(5,11)=5;即f[9]=11;

j=8:f[8]=max(f[8],f[3]+6);即max(5,6)=6;即f[8]=6;

j=7:f[7]=max(f[7],f[2]+6);即max(5,6)=6;即f[7]=6;

j=6:f[6]=max(f[6],f[1]+6);即max(5,6)=6;即f[6]=6;

j=5:f[5]=max(f[5],f[0]+6);即max(5,6)=6;即f[5]=6;

4. 当进入循环 i==3:

w[i]=7;v[i]=6;

j=10:f[10]=max(f[10],f[4]+7);即max(11,12)=12;即f[10]=12;

j=9:f[9]=max(f[9],f[3]+6);即max(11,6)=11;即f[9]=11;

j=8:f[8]=max(f[8],f[2]+6);即max(6,6)=6;即f[8]= 6;

j=7:f[7]=max(f[7],f[1]+6);即max(6,6)=6;即f[7]=6;

j=6:f[6]=max(f[6],f[0]+6);即max(6,6)=6;即f[6]=6;

就模拟一下发现没有错误,即逆序就可以解决这个优化的问题了

优化后的代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int f[N];
int v[N], w[N];
int n, m;
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;
}

我猜看到这里的人应该不多,但是既然都看到这里了,不管文章写的o不ok,有没有忘记点赞???