01背包问题的理论+实战

文章目录

  • 01背包问题理论
    • 状态表示
    • 状态计算——状态转移方程
    • f(i, j)
  • 01背包问题实战
    • 优化
      • 为什么遍历背包容积的时候需要倒序
      • 如何理解一维的过程

本文是AcWing算法基础课的学习笔记,总结了有关01背包问题的理论和实际代码,部分内容参考了文章背包问题。如果觉得不错,不妨点个赞叭~

01背包问题理论

问题

N 个物品和容量为V的背包 每个物品 i 有两个属性,体积 vi 和价格 wi 每个物品最多能用一次(可用一次和零次)

问题是从N个物品中挑选一些物品,使得总体积 <= V

即背包能装得下,目标是让这些物品的总价值最大

01背包问题的理论+实战_第1张图片
01背包问题的理论+实战_第2张图片
理解动态规划最好的方式:先画图

01背包问题的理论+实战_第3张图片

状态表示

状态表示就是我们需要几维来表示我们的状态,每个状态的含义是什么(集合是哪些东西的集合、集合当中的元素需要满足哪些条件)

集合是所有选法的集合,要满足两个条件: 满足这两个条件的所有选法的集合就是f(i, j)

  • 只从前i个物品中选
  • 选出来的物品的总体积 <= j

f(n,v):表示的就是所有从前N个物品中选并且总体积不超过V的集合,即每种选法的总价值的最大值

  • 状态计算是我们如何可以一步一步地把我们的状态算出来,即f(i, j)可以怎么计算出来

状态计算——状态转移方程

01背包问题的理论+实战_第4张图片
状态计算实际上就是集合的划分,考虑如何把当前这个集合划分成更小的子集,使得每一个子集我们都可以用前面更小的集合表示出来。

选法划分为含 i 和不含 i 两个部分(即选择第i个物品和不选择第i个物品),两部分的含义:

  • 不含i:从1~ i个物品中选择且不包括i (即从1到 j-1 中选择)且总体积不超过j的总价值的最大值
  • 含i:从1~i个物品中选择且包括i且总体积不超过 j 的总价值的最大值

例子:小明考了第一名,但是老师为了照顾全班其他同学,给每个人都加了50分,这样全班的名次不会发生变化,小明仍然是第一名

我们这里也类似,把每种选法的第i个物品都去掉,不会影响我们最大值是谁
也就是从1~i-1个物品中选总体积不超过j-vi这样选法的一个集合

“含i”表示的是去掉第i个物品的最大价值;然后加上第i个物品的价值,就是我们要求的最大价值

f(i, j)

f(i, j)就是这两个情况取一个最大值,取的过程中有两个条件:

  • 不重:当且仅属于其中一个集合
  • 不漏:不存在“分完发现有些元素不属于这两个集合”

不重复这个不一定所有情况都要满足,例如这里求最大值重复也无所谓

01背包问题实战

#include 
#include 
#include 

using namespace std;

const int N = 1010, V = 1010;
int v[N], w[N]; // v[i]表示第i件物品的体积,w[i]表示第i件物品的价值
int f[N][V]; // f[i][j] 表示从1~i物品中选择总体积不大于j的最大价值

int main(){
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++){
        cin >> v[i] >> w[i]; // 读入1~i件物品的体积、价值信息
    }
    
    for (int i = 1; i <= n; i++){ // 先确定物品
        for (int j = 0; j <= m; j++){ // 遍历体积
            f[i][j] = f[i-1][j]; // 不含物品i
            if (j >= v[i]) f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]); // 含物品i(二者取最大值)
        }
    }
    
    cout << f[n][m] << endl;
    return 0;
}

优化

以下内容参考自:https://www.acwing.com/solution/content/30250/

DP优化问题主要是对DP的代码或者计算方程进行等价变形

例如这里:f(i)只用到了f(i-1)这一层,于是可以用滚动数组来做

另外,算f(i, j)时,用到的j只与 j 和 j-vi 相关,都是小于等于j的 ⭐️

于是:与其把f[i - 1]这一层拷贝到f[i]上,不如只用一个一维数组了,只用f[j](一维数组,也可以理解是一个滚动数组)。

这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。

在一维f数组中,f[j]表示:容量为j的背包,所背的物品价值可以最大为f[j]。

#include 
#include 
#include 

using namespace std;

const int N = 1010, V = 1010;
int v[N], w[N]; // v[i]表示第i件物品的体积,w[i]表示第i件物品的价值
int f[N]; // f[j] 表示容量为j的背包,所背的物品价值可以最大值

int main(){
    int n, m;
    cin >> n >> m; // n是物品总数量、m是物品总体积
    for (int i = 1; i <= n; i++){
        cin >> v[i] >> w[i];
    }
    
    for (int i = 1; i <= n; i++){ // 遍历物品,一次循环结束表示物品1~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. 为什么遍历背包容积的时候需要倒序

  2. 如何理解上述过程

为什么遍历背包容积的时候需要倒序

01背包问题的理论+实战_第5张图片
假如由3件物品,背包总体积为10

   物品    体积     价值 
  i = 1     4       5  
  i = 2     5       6  
  i = 3     6       7  

如果j层的循环时递增的(正序):

如果 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] = 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;

当进入循环 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;

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

但是,如果考虑j层循环是逆序的,就不会存在这个情况:

当还未进入循环时:
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;
当进入循环 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[6] = max(f[4], f[0] + 5); 即max(0, 5) = 5; 即f[4] = 5;
当进入循环 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;
当进入循环 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;

如何理解一维的过程

f[j]可以通过f[j - weight[i]]推导出来,f[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。

f[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:f[j])

此时f[j]有两个选择:

  • 一个是取自己f[j] 相当于 二维f数组中的f[i-1][j],即不放物品i,
  • 一个是取f[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值,

为什么要逆序,从这里就可以看出来,避免f[j-weight[i]]与同一层的f[j]重复。

你可能感兴趣的:(数据结构与算法,c++,算法,数据结构,c语言)