[背包] 背包问题算法模板(模板)

0. 前言

背包问题是众多 dp 问题的母题,是一个很重要的知识点。该博文基于背包九讲总结,会将背包九讲内容及模板题全部总结一般,也是鉴于学习进度,目前仅总结了 01 背包及优化模型,完全背包,多重背包,分组背包。

初次系统学习背包问题,总结不够到位,往各位同学批评指正!十分感谢~


背包问题共性:

  • n 个物品,一个容量为 v 的背包
  • 每个物品两个属性,体积 vi,价值 wi
  • 要求在这些物品中挑总体积不大于 v 的物品并使背包装入物品的总价值 w 最大。不一定需要装满背包

1. 01背包

特点:

  • 每件物品最多只用一次

2. 01背包问题

在这里插入图片描述

朴素01背包

思路:

  • 状态表示:f[i][j] 从前 i 个物品中选择且总体积不大于 j 的最大价值
  • 状态计算:
    • 将整个状态划分成两类:
    • 不选第 i 个物品f[i][j] = f[i-1][j]
    • 选第 i 个物品f[i][j] = f[i-1][j-v[i]]+w[i]
      • 假设前 i-1 个物品的总体积为 v,现在加上 v[i] 后小于等于总体积 j,即 v+v[i]<=j,则有 v <= j - v[i],就以 j >= v[i] 作为判断条件
    • 故状态转移方程为:f[i][j]=max(f[i-1][j], f[i-1][j-v[i]]+w[i])
  • 状态初始化:
    • f[0][0~m] = 0 表示在选择 0 件物品时对于任何体积来讲,其最大价值均为 0

代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1005;

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];	// 不选第i件物品的情况一定存在
            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;
}

如上分析过程及代码即为最朴素的 01 背包问题的解决方案。


滚动数组优化

  • 能发现,f[i][j] 的状态转移仅使用到了 f[i-1][...],故可以采用滚动数组来做。即当前层的状态转移仅与上一层有关
  • 当前层是 i & 1,上一层是 i-1 & 1

滚动数组优化代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1005;

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

一维终极优化

其实,从状态转移方程 f[i][j]=max(f[i-1][j], f[i-1][j-v[i]]+w[i]) 能够知道第一维 f[i] 只用到了 f[i-1],且第二维不论是 f[i-1][j] 还是 f[i-1][j-v[i]] 第二维总是小于等于 j 的。基于滚动数组的思想,我们完全可以将其改为一维数组来再度优化空间和代码:

  • 基于最朴素二维数组版本,我们可以直接删掉第一维,则 f[N][N] 变为 f[N],仅枚举体积
  • 则朴素代码中的 f[i][j]=f[i-1][j] 变为 f[j]=f[j] 成为恒等式,则可以直接删除
  • if 判断中 j >= v[i],此时仅有一维,当 j < v[i] 时,这个判断条件是没有意义的。故我们可以直接让 jv[i] 开始,就可以删掉 if 判断了
  • 此时,如果直接删掉第一维,则变为: f[j]=max(f[j],f[j-v[i]]+w[i]),这个转移方程其实和之前是不等价的。可将其在一维含义下还原成两维的含义,对比在优化过程中是否改变了原意。
    • 首先第 i 层算的 f[i][j] = max(f[i][j],...),第 i 层算的 f[j] 一定是 f[i][j],但是由于一维中是 f[j-v[i]]j-v[i] 一定是严格小于 j 的,且我们的 j从小到大进行枚举体积的,j-v[i] 会在 j 之前被计算,那么一维中的 f[j-v[i]] 实际上是第 i 层的 j-v[i],其等价于 f[i][j-v[i],而实际上应该是 f[i-1][j-v[i]]
    • 故我们可以改变 j 的循环顺序来解决这个问题,让 jmv[i] 进行枚举,即从大到小枚举体积,那么当我们更新体积 j 时,这个 j-v[i] 还没被更新过,那么它就存的是 i-1 层的 j-v[i] 这样就等价于之前的状态转移方程了
  • 至此,我们就完成了 01 背包问题的终极写法

一维 01 背包终极写法代码:

 #include <iostream>
#include <algorithm>

using namespace std;

const int N = 1005;

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

2. 完全背包

特点:

  • 每件物品可以使用无限次

3. 完全背包问题

在这里插入图片描述

朴素完全背包

思路:

  • 状态表示:f[i][j] 从前 i 个物品中选择且总体积不大于 j 的最大价值
  • 状态计算:
    • 将整个状态划分成 k 类:
    • 选第 i 个物品 0 次f[i][j] = f[i-1][j]
    • 选第 i 个物品 1 次f[i][j] = f[i-1][j-v[i]]+w[i]
    • 选第 i 个物品 2 次f[i][j] = f[i-1][j-2*v[i]]+2*w[i]
    • 选第 i 个物品 k 次f[i][j] = f[i-1][j-k*v[i]]+k*w[i]
    • 故状态转移方程为:f[i][j]=max(f[i][j], f[i-1][j-k*v[i]]+k*w[i]))
  • 状态初始化:
    • f[0][0~m] = 0 表示在选择 0 件物品时对于任何体积来讲,其最大价值均为 0

这个时间复杂度是相当高的,是 O ( n 3 ) O(n^3) O(n3),当 n = 1000 n = 1000 n=1000 时,计算量达到 1 e 9 1e9 1e9,妥妥的超时。不过这也是朴素做法的基本思想。

朴素思想的超时代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1005;

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] + k * w[i]);
            }
        }
    }
    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-2v]+2w, f[i-1][j-3v]+3w,...)
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-4v]+3w,...)
f[i][j-v]+w=max(           f[i-1][j-v]+w, f[i-1][j-2v]+2w, f[i-1][j-3v]+3w, f[i-1][j-4v]+4w,...)
故:
f[i][j]    =max(f[i-1][j], f[i][j-v]+w);

这是一个经典的优化,可以优化掉一维的状态,时间复杂度优化为 O ( n 2 ) O(n^2) O(n2)

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1005;

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

对比下完全背包与 01 背包的转移方程:

  • 01 背包: f[i][j]=max(f[i-1][j], f[i-1][j-v]+w)
  • 完全背包:f[i][j]=max(f[i-1][j], f[i][j-v]+w)

01 背包从 i-1 转移过来,完全背包从 i 转移过来,就这一点不同。那么在此枚举体积就不需要从大到小枚举了,因为当前是 f[i][[j] = f[i][j-v]+w 就是从当前第 i 层转移过来的,被提前计算的 f[i][j-v] 刚好帮助了状态转移。

在此简单总结:除了完全背包问题一维优化后,其体积是从小到大循环的。其余大部分背包问题一维优化后都是从大到小循环的。在此包括 01 背包,分组背包、多重未优化、混合、有依赖等等。其中多重背包问题写法很多,单调队列版本从小到大循环 如果空间是两维的话,空间随便循环,循环顺序也都是随意。且优化后顺序为 物品、体积、决策

那么完全背包也完全可以优化成一维:

一维终极优化

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1005;

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

这里的优化就不需要从大到小枚举 j,这里的 f[j-v[i]] 就是需要第 i 层的 j-v[i],在当前 i 层,j-v[i] 一定是先于 j 被更新的,满足要求。

3. 多重背包

特点:

  • 每件物品有独立的个数限制,不得超过最大数量限制

4. 多重背包问题 I

在这里插入图片描述
思路:

  • 状态表示:f[i][j] 从前 i 个物品中选择且总体积不大于 j 的最大价值
  • 状态计算:
    • 将整个状态划分成 s[i]+1 类:
    • 选第 i 个物品 0 次f[i][j] = f[i-1][j]
    • 选第 i 个物品 1 次f[i][j] = f[i-1][j-v[i]]+w[i]
    • 选第 i 个物品 2 次f[i][j] = f[i-1][j-2*v[i]]+2*w[i]
    • 选第 i 个物品 s[i]f[i][j] = f[i-1][j-s[i]*v[i]]+s[i]*w[i] 最终就是该物品数量的最大限制。十分类似于完全背包问题
    • 故状态转移方程为:f[i][j]=max(f[i][j], f[i-1][j-k*v[i]]+k*w[i])), k=0, 1, 2,...
  • 状态初始化:
    • f[0][0~m] = 0 表示在选择 0 件物品时对于任何体积来讲,其最大价值均为 0

朴素版本的多重背包问题与朴素版本的完全背包问题思想一模一样,代码稍作改动即可。时间复杂度仍为 O ( n 3 ) O(n^3) O(n3),这里的 n = 100 n=100 n=100,故计算次数为 1 e 6 1e6 1e6,还是可以的。

朴素多重背包

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 105;

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

5. 多重背包问题 II

在这里插入图片描述
仍为多重背包问题,但数据范围增大,仍采用 O ( n 3 ) O(n^3) O(n3) 暴力则将达到 1000 ∗ 2000 ∗ 2000 = 4 e 9 1000 * 2000 * 2000 = 4e9 100020002000=4e9 40 亿的计算量,超时。

考虑展开状态转移方程,以完全背包问题优化方式进行优化:

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-v]f[i][j] 后面一部分是比较相似的,但是其中 f[i][j-v] 多了 f[i-1][j-(s-1)v]+sw 这一项,即现在我们已知 f[i][j-v] 的最大值,需要求解 f[i][j-v] 展开项中除过 f[i-1][j-(s-1)v]+sw 的最大值。这个操作是无法实现的,取最大值操作是不支持减法的。这就相当于给定一堆数的最大值,让你求解其中部分数的最大值,这是无法直接求得的。所以,我们无法直接使用完全背包优化方式来优化多重背包问题。


二进制优化

在此有一种神奇且经典的优化方式,称为:二进制优化方式

  • 假设某组物品有 1023 个,那么我们真的需要枚举 1023 次吗?
  • 这里可以采用二进制倍增的思想,将 1023 个物品进行打包,然后拼凑出 1~1023 中的任意数量的物品
  • 思想类比快速幂,将 O ( n ) O(n) O(n) 优化到 O ( l o g n ) O(logn) O(logn)
  • 即,若第 i 个物品数量为 s 个,优化流程为:
    • s 拆分成二进制下的打包物品,s[i] 个就会变成 log s[i]
    • 然后对打包出来的物品做一遍 01 背包就行了,每个打包物品只能选 1 次。因为这些打包物品可以拼凑出来所有的情况

那么原来的算法时间复杂度为 O ( n v s ) O(nvs) O(nvs),现在为 O ( n v l o g s ) O(nvlogs) O(nvlogs)。成功优化, 1000 ∗ 2000 ∗ l o g 2000 = 2 e 7 1000*2000*log2000=2e7 10002000log2000=2e7,刚好能过。

这就是多重背包问题的经典优化:二进制优化。

二进制优化思想代码:

#include <iostream>
#include <algorithm>

using namespace std;

// 一共1000个物品,每个物品最多2000件,2^11=2048 数组大小开1000*11=11000
const int N = 11005, M = 2005; 

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

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;                  // 从 1 开始打包
        while (k <= s) {            
            cnt ++;                 // 记录新打包物品编号,每次打包k个第i个物品
            v[cnt] = a * k;         // k个一组,体积变大k倍
            w[cnt] = b * k;         // k个一组,价值变大k倍
            s -= k;                 // i物品总个数一次性减少k个
            k *= 2;                 // 倍增
        }
        if (s > 0) {                // 补上剩下的物品
            cnt ++;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }
    
    n = cnt;    // 更新现在有的物品总数,将其转化为01背包问题,每个物品独立且只能选1次
    
    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;
}

单调队列优化

高能预警
高能预警
高能预警

多重背包单调队列优化,请自行百度男人八题,楼教主

6. 多重背包问题 III

在这里插入图片描述
前置知识:

这个问题是背包问题中最难的问题之一,还有一个是有依赖的背包问题,其属于树形 dp。本题属于单调队列优化,将多重背包问题做到了 O ( n m ) O(nm) O(nm) 的时间复杂度。因为每个元素在单调队列中只进队一次出队一次所有是 m 次,外层循环枚举 n 次,所以总的时间复杂度是 O ( n m ) O(nm) O(nm) 的。是一个了不起的优化!

我能理解该问题,板书很潦草很潦草…推荐几个 dalao 题解,他们写的很清楚。

在这里插入图片描述
感觉写了一堆废话…

单调队列优化,第三维枚举体积:

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 2e4+5;

int n, m;
int f[N], g[N], q[N];

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; ++i) {
        int v, w, s;
        cin >> v >> w >> s;
        memcpy(g, f, sizeof f);                 // i-1层的状态备份,滚动数组
        for (int j = 0; j < v; ++j) {           // 枚举余数
            int hh = 0, tt = -1;                // 单调队列定义
            for (int k = j; k <= m; k += v) {   // 枚举第i个物品所占背包体积,类比数轴
                if (hh <= tt && q[hh] < k - s * v) hh ++;   // 滑出窗口,队头出队。此时队列元素超过了s个
                // 单调队列出队,维护队列单调性
                while (hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) tt --;
                if (hh <= tt) f[k] = max(f[k], g[q[hh]] + (k - q[hh]) / v * w);     // 更新最大值
                q[++tt] = k;                    // 插入当前元素
            }
        }
    }
    cout << f[m] << endl;
    return 0;
}

简单改写,第三维枚举个数代码:

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 2e4+5;

int n, m;
int f[N], g[N], q[N];

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; ++i) {
        int v, w, s;
        cin >> v >> w >> s;
        memcpy(g, f, sizeof f);                 
        for (int j = 0; j < v; ++j) {           
            int hh = 0, tt = -1;                
            for (int k = 0; k <= (m - j) / v; k ++) {       // 枚举个数,可直接理解成在数轴上的下标
                if (hh <= tt && k - q[hh] > s) hh ++;   
                while (hh <= tt && g[q[tt] * v + j] - q[tt] * w <= g[k * v + j] - k * w)  tt --;
                q[++tt] = k;  		// 先入队
                f[k * v + j] = max(f[k * v + j], g[q[hh] * v + j] + (k - q[hh]) * w);  // 在此不必再判断,队头即为最大元素
            }
        }
    }
    cout << f[m] << endl;
    return 0;
}

4. 分组背包

特点:

  • 物品有 n 组,每组物品有若干个
  • 每组物品最多只能选一件物品

9. 分组背包问题

在这里插入图片描述
完全背包问题:枚举第 i 件物品选几个
分组背包问题:枚举第 i 组物品选哪个

思路:

  • 状态表示:f[i][j] 从前 i 组物品中选择且总体积不大于 j 的最大价值
  • 状态计算:
    • 针对第 i 组物品,将整个状态划分成 s[i]+1 类:
    • 不选第 i 组物品f[i][j] = f[i-1][j]
    • 选第 i 组物品的第一个物品f[i][j] = f[i-1][j-v[1]]+w[1]
    • 选第 i 组物品的第二个物品f[i][j] = f[i-1][j-v[2]]+w[2]
    • 选第 i 组物品的第 s[i] 个物品f[i][j] = f[i-1][j-v[s[i]]+w[s[i]] 最终就是该物品数量的最大限制。十分类似于完全背包问题
    • 故状态转移方程为:f[i][j]=max(f[i][j], f[i-1][j-v[k]]+w[k])), k=0, 1, 2,...s[i]
  • 状态初始化:
    • f[0][0~m] = 0 表示在选择 0 件物品时对于任何体积来讲,其最大价值均为 0

同理,分组背包问题也是可以从二维优化到一维的。其实只需要谨记一点:

  • 当我们当前状态需要用上层状态进行转移时,就从大到小枚举体积
  • 当我们当前状态需要用本层状态进行转移时,就从小到大枚举体积

这就直接扔上来一维版本了,二维都写了这么多遍了,抓住代码思想,代码实现都大同小异。

分组背包终极优化版本代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 105;

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

5. 二维费用背包

8. 二维费用的背包问题

在这里插入图片描述
二维费用背包问题可和 01 背包、完全背包、多重背包、分组背包等背包模型进行简单拼接拓展。

这个每个物品也只被选 1 次,是完全基于 01 背包的二维费用背包问题。

相较于朴素 01 背包状态表示,本题增加了一个重量限制。那么再开一维表示该状态即可。

思路:

  • 状态表示:f[i,j,k] 从前 i 个物品中选择且总体积不大于 j,总重量不超过 k 的选法的最大价值
  • 状态计算:
    • 将整个状态划分成两类:
    • 不选第 i 个物品f[i,j,k] = f[i-1,j,k]
    • 选第 i 个物品f[i,j,k] = f[i-1,j-v[i],k-m[i]]+w[i]
  • 答案:
    • f[N,V,M]

同理与 01 背包从 2 维优化成 1 维,在此可由 3 维优化为 2 维,不需要存储 i,但是体积需要从大到小循环。

代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1005, V = 105, M = 105;

int n, v, m;
int f[V][M];

int main() {
    cin >> n >> v >> m;
    for (int i = 0; i < n; ++i) {
        int vi, mi, wi;
        cin >> vi >> mi >> wi;
        for (int j = v; j >= vi; --j) {
            for (int k = m; k >= mi; --k)
                f[j][k] = max(f[j][k], f[j - vi][k - mi] + wi);
        }
    }
    cout << f[v][m] << endl;
    return 0;
}

6. 混合背包

7. 混合背包问题

在这里插入图片描述
重点: 01背包、完全背包、多重背包

这三个背包问题具有相同的状态表示,和集合属性。f[i][j] 都表示只从前 i 件物品中选,且总体积不超过 j 的所有选法的最大价值。在此简单回顾下状态转移方程即可:

  • 01 背包 f[i][j] = max(f[i-1][j], f[i-1][j - vi]+wi)
  • 完全背包 f[i][j] = max(f[i-1][j], f[i][j-vi]+wi)
  • 多重背包 f[i][j] = max(f[i-1][j], f[i-1][j-vi]+wi,f[i-1][j-2*vi]+2*wi,...)

其实 01 背包就是多重背包各个物品数量限制为 1 的背包问题,所以 01 背包很容易就可以转化为多重背包。

本题需要采用多重背包的二进制优化,具体可以参考本博文多重背包部分

代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1005;

int n, m;
int f[N];

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; ++i)  {
        int v, w, s;
        cin >> v >> w >> s;
        if (s == 0) {                                       // 完全背包
            for (int j = v; j <= m; ++j) 
                f[j] = max(f[j], f[j - v] + w);
        } else {
            if (s == -1) s = 1;                             // 01背包就是多重背包数量为1的情况
            for (int k = 1; k <= s; k *= 2) {               // 多重背包二进制优化
                for (int j = m; j >= k * v; --j) 
                    f[j] = max(f[j], f[j - k * v] + k * w);
                s -= k;
            }
            if (s) {                                        // 处理剩下的物品个数
                for (int j = m; j >= s * v; --j) 
                    f[j] = max(f[j], f[j - s * v] + s * w);
            }
        }
    }
    cout << f[m] << endl;
    return 0;
}

7. 有依赖的背包问题

10. 有依赖的背包问题

在这里插入图片描述在这里插入图片描述

在这里插入图片描述

简单版: [分组背包] 金明的预算方案(分组背包+二进制枚举+有依赖背包+思维)

树形 dp[树形dp] 没有上司的舞会(模板题+树形dp)

金明的预算方案,这个依赖关系就是主件和附件,且附件最多也就两个,树结构高度为 2。而这个依赖关系就是一颗树,更加复杂。是一个树形 dp 题目,可以参考没有上司的舞会。关于树的题目,都是采用数组模拟邻接表来实现存储的。

有依赖背包问题和单调队列优化多重背包问题是背包九讲中最难的两个问题。


若选当前节点的子树中某一个,那么当前节点就必须先被选了,所以就得递归的考虑该问题,树形 dp

思路:

  • 状态表示:f[u][j] 从以 u 为根的子树中选,且总体积不超过 j 的方案的最大价值。这是和线性前 i 个物品不同之处,一个是线性、一个是树形
  • 状态计算:
    • u 为根节点的子树中选,那么 u 肯定是要选的。但是,u 的子节点还是很多的,假设有 p 个,那么就有 2^p 种情况,是指数级别的。我们在金明中,之所以能够 2 进制枚举,也得益于 p 很少,在此肯定是不行的
    • 考虑按体积枚举所有的子节点选择情况,这里需要声明,假设 u 节点有 3 个子节点,那么这 3 个子节点递归考虑的话是独立的。我们需要按体积分别划分节点 1、节点 2、节点 3。此时划分的就需要留出 u 的体积,故体积不是从 m 开始,而是从 m-v[u] 开始,枚举体积的话就有 0~m-v[u] 种选法,再这些选法中取价值最大值即可。故,这样枚举就将情况从 2^p 降到 0~m-v[u]
    • 那么,一个根节点 u,包含 3 个子树,这些子树中的所有状态按体积划分为 0~m-v[u] 种情况,将这些情况看成状态,那么每个子树都有这么多情况,且这些情况只能被选出来最大的一个。所以,子树就是物品组,这些情况就是物品,将其转化为一个分组背包问题
    • 故,递归考虑每个子树的过程中,就是一个分组背包问题。在每个子树内部采用分组背包问题做一遍就行了

还是蛮抽象的…,代码做了详细的注释。几点需要注意:

  • 这里 f 开成 2 维,但是体积仍要从大到小逆序枚举,因为这里还是需要上层的状态,和 01 背包类似,不逆序体积就滚动数组。反正不能当前层更新当前层
  • 注意将根节点的价值加上
  • 注意将不合法情况,即体积小于根节点的情况清空
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 105;

int n, m;
int v[N], w[N];
int h[N], e[N], ne[N], idx;
int f[N][N];   // 虽然开了二维,不过第一维是根节点数,对于分组背包部分来说还是一维,需要逆序体积

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void dfs(int u) {
    for (int i = h[u]; ~i; i = ne[i]) { // 循环物品组,遍历u号点的所有子树,~i表示i!=-1,因为~(-1) = 0
        int son = e[i];                 // 子节点编号
        dfs(e[i]);                      // 递归所有子树,得到所有子节点的f值
        
        // 分组背包
        // 这里对于树中的每个节点来说,就是一个分组背包问题。每个子节点是一组,
        // 每个子节点的不同体积和每个体积所对应的最大价值,就是这个物品组中的选出的物品
        for (int j = m - v[u]; j >= 0; --j)     // 循环体积,根节点一定被先选,留出u的体积。这里是逆序体积,需要上层状态
            for (int k = 0; k <= j; ++k)        // 循环决策,根据体积分割集合,故为0~j
                f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);    // f[son][k]代表的是当前子节点k决策的价值
    }
    
    for (int i = m; i >= v[u]; --i) f[u][i] = f[u][i - v[u]] + w[u];    // 得加上根节点的价值
    for (int i = 0; i < v[u]; ++i) f[u][i] = 0;                         // 若根节点放不下,则依赖关系不满足,该得到的价值失效
}

int main() {
    cin >> n >> m;
    memset(h, -1, sizeof h); 
    int root;
    for (int i = 1; i <= n; ++i) {  // 读取n个物品
        int p;                      // p为第i个物品的依赖物品编号,可以理解为父节点
        cin >> v[i] >> w[i] >> p;
        if (p == -1) root = i;      // 存根节点
        else add(p, i);             // p指向i的边,父节点指向i这个子节点
    }
    
    dfs(root);
    
    cout << f[root][m] << endl;
    
    return 0;
}
  • 6
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

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

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

打赏作者

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

抵扣说明:

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

余额充值