浅谈背包问题

问题背景

背包问题是运筹学领域经典的组合优化问题,核心目标是在给定资源约束(如背包容量)下,从一组物品中选择一个子集,使得选中物品的总价值最大化(或总消耗最小化),同时满足资源约束条件。

一、01 背包

(一)、题目概述

给定 \(n\) 个物品和一个容量为 \(m\) 的背包。第 \(i\) 个物品体积 \(v_i\),价值 \(w_i\),每个物品只有一个,求总容量不超过 \(m\) 的最大价值。

(二)、暴力枚举子集

由于每个物体只有两种可能的状态(取与不取),那么对于 \(n\) 个物品,我们可以用 \(2^n\) 表示所有可能的状态。不妨用 0 表示不取,1 表示取,代码如下所示:

#include <iostream>
using namespace std;
const int N = 1010;
int n, m, w[N], v[N];
int ans;

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; i ++) cin >> v[i] >> w[i];
    for (int i = 0; i < 1 << n; i ++) {
        int x = 0, y = 0;
        for (int j = 0; j < n; j ++) {
            if (i & (1 << j)) {
                x += v[j];
                y += w[j];
            }
        }
        if (x <= m && y > ans) ans = y;
    }
    cout << ans;
    return 0;
}  
  • 该枚举方式的时间复杂度为 \(O(2^n)\),无法在合理时间内完成计算。

(三)、DP 优化

1. 思路

假设按照物品编号从左到右依次进行决策。对于第 \(n\) 个物品在容量约束为 \(m\) 下的决策的最大价值需依赖于前 \(n-1\) 个物品的决策在容量约束为 \(m\)\(m-v_n\) (选第 \(n\) 个物品或者不选)下的最优解。

考虑第 \(n\) 个物品的两种决策方式:

  • 不选第 \(n\) 件物品,此时最大价值为考虑前 \(n-1\) 件物品选或不选在容量约束为 \(m\) 下的最大价值。
  • 选第 \(n\) 件物品,此时的最大价值为前 \(n-1\) 件物品在容量约束为 \(m-v_n\) 下的最大价值。

因此,我们可以依次考虑 \(n-1,n-2,...,1\) 阶段,不难发现,它们是相同且规模更小的问题。以此类推,我们可以将该问题划分为 \(n\) 阶段的线性决策,且此时,\(0\) 作为边界,其在「选」与「不选」两种状态下的最优解为 \(0\),是确定且可直接人工给出的,因此该边界条件能够作为递推的起点,使得所有更大规模的问题都可以通过状态转移方程逐层回推得到最优解。

由于从左到右枚举,在枚举到 \(i\) 阶段前,我们是无法知道 \(i-1\) 阶段需要的容量约束是多少,即 \(i-1\) 阶段的容量约束 \(j-v_i\) 可能是\([0,j]\)的任意值,于是我们需要求出阶段 \(i-1\) 在所有容量约束下的最大价值。

定义状态 \(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, \ j\ge v_i\)

初始条件:\(f_{0,j}=0, \ \forall j\in [0,m]\)

综合得到状态转移方程

\[f_{i,j}=\max(f_{i-1,j},f_{i-1,j-v_i}+w_i) \]

2. 滚动数组优化

仔细观察上述状态转移方程,不难看出,第 \(i\) 阶段的状态 \(f_{i,j}\) 仅仅依赖于 \(i-1\) 阶段状态 \(f_{i-1,j}\)\(f_{i-1,j-v_i}\),于是我们可以仅保留前一个阶段的状态,从而计算当前状态。但由于从小到大枚举 \(j\),在计算当前阶段状态时会出现覆盖前一个阶段的状态的情况,因此考虑从大到小枚举,可将状态压缩为

\[f_j=\max(f_j,f_{j-v_i}+w_i) \]

3. 代码实现

#include <iostream>
using namespace std;
constexpr 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];
  return 0;
} 

4. 复杂度分析

  • 时间复杂度:\(O(nm)\)
  • 空间复杂度:\(O(m)\)

二、完全背包

(一)、题目概述

给定 \(n\) 个物品和一个容量为 \(m\) 的背包。第 \(i\) 个物品的体积为 \(v_i\)、价值为 \(w_i\)。每个物品可以被选择无限次,求在总容量不超过 \(m\) 的前提下可获得的最大价值。

(二)、DP 思路

沿用 01 背包的思路,定义:\(f_{i,j}\) 表示考虑前 \(i\) 个物品、且容量约束为 \(j\) 时的最大价值。

由于完全背包中的每个物品可以取无限次,于是可以增加一层循环,枚举满足 \(kv_i\le j\)\(k\) 即可。

下面我们直接讨论优化方式。将 \(f_{i,j}\) 所有可能的依赖状态展开

\[f_{i,j} = \max \big( f_{i-1,j}, f_{i-1,j-v_i}+w_i, f_{i-1,j-2v_i}+2w_i, \dots, f_{i-1,j-kv_i}+kw_i \big) \]

容易观察到,排除 \(k=0\) 的情况后

\[\max \big( f_{i-1,j-v_i}+w_i, + f_{i-1,j-2v_i}+2w_i, \dots, f_{i-1,j-kv_i}+kw_i \big) = \max \big( f_{i-1,j}, f_{i-1,j-v_i}, + f_{i-1,j-2v_i}+w_i, \dots, f_{i-1,j-kv_i + (k-1)w_i} \big) + w_i \]

其余状态可以统一表示为 \(f_{i,j-v_i}+w_i\),于是

\[f_{i,j}=\max(f_{i-1,j},f_{i,j-v_i}+w_i) \]

进一步可以发现,对于当前阶段的状态 \(f_{i,j}\),其仅依赖于前一阶段的状态 \(f_{i-1,j}\) 以及当前阶段的状态 \(f_{i,j-v_i}\)。因此,当 \(j\) 从小到大枚举时,可以进行状态压缩,得到

\[f_j=\max(f_j,f_{j-v_i}+w_i) \]

1. 代码

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

2. 复杂度分析

  • 时间复杂度:\(O(nm)\)
  • 空间复杂度:\(O(m)\)

三、多重背包

(一)、题目概述

给定 \(n\) 个物品和一个容量为 \(m\) 的背包。第 \(i\) 个物品的体积、价值以及数量分别为: \(v_i\)\(w_i\)\(s_i\),求在总容量不超过 \(m\) 的前提下可获得的最大价值。

(二)、DP 思路

\(f_{i,j}\) 表示考虑前 \(i\) 个物品、且在容量约束为 \(j\) 下的最大价值。

由于第 \(i\) 个物品可以选取 \(s_i\) 个,可以类似于为优化的完全背包,在转移时额外增加一层循环枚举选取数量不超过 \(s_i\) 的情况,其时间复杂度为\(O(nms)\),其中 \(s=\max\{s_1,\dots,s_n\}\)。当 \(n,m,s\) 同阶时,可写作:\(O(n^3)\)。下面直接讨论优化方式。

(三)二进制优化

完全背包的优化方式不能直接应用于多重背包。因为完全背包允许无限取用某一物品,其状态转移所依赖的状态大于等于多重背包。下面通过一个简单例子说明这一差异:

  • 考虑 \(n=1,m=9,v_1=3,w_1=5,c_1=2\),可以发现,若为完全背包,最大价值为 15;而在多重背包中,最大价值仅为 10。

换句话说,多重背包在将第 \(i\) 件物品全部选取完后,任然存在剩余容量,从而导致其性质与完全背包本质不同。

引理(二进制拆分原理)

\(S=2^0+2^1+\dots+2^{k-1}\),则集合 \(\{2^0,2^1,\dots,2^{k-1}\}\) 的元素可以通过加法表示区间 \([0,S]\) 内的所有整数。

推论证明

进一步证明:若 \(S=2^0+2^1+\dots+2^{k-1}+x, \ 1\le x<2^{k}\),则集合 \(\{2^0,2^1,\dots,2^{k-1},x\}\) 可以组合出 \([0,S]\) 内的所有整数。

设集合 \(Q=\{2^0,2^1,\dots,2^{k-1}\}\),并令 \(S'=2^0+2^1+\dots+2^{k-1}\)。由上述引理可知,区间 \([0,S']\) 可以由集合 \(Q\) 表示。于是只需证明区间 \([S',S]\) 可以由集合 \(P=Q\cup\{x\}\)表示。

对于任意 \(S'+n,1\le n\le x\),有

\[S'+n=(S'+n-x)+x \]

又因为 \(n\le x\Rightarrow S'+n-x\le S'\),所以 \(S'+n-x\) 可由集合 \(Q\) 表示,进而 \(S'+n\) 可由集合 \(P\) 表示,故命题成立。

回到多重背包问题,对于第 \(i\) 个物品的数量 \(s_i\),若可以将其拆分为若干份,使这些份数的能够表示区间 \([0,s_i]\) 内的任意整数。具体而言,将 \(s_i\) 按 2 的整数幂进行拆分。设 \(k\) 为满足 \(s_i\ge2^0+2^1+\dots+2^{k-1}\) 的最大整数,分类讨论 \(s_i\) 的两种情况:

  • \(s_i=2^0+2^1+\dots+2^{k-1}\),由二进制拆分定理,其可被转换为 01 背包问题
  • \(s_i=2^0+2^1+\dots+2^{k-1}+x,1\le x<2^{k}\),由上述推论,同样可以转换为 01 背包问题。

通过上述分析,多重背包问题可以通过二进制拆分完全转化为 01 背包问题,下面给出若干示例以帮助理解拆分过程:

  • \(6=1+2+3\)
  • \(8=1+2+4+1\)
  • \(18=1+2+4+3\)
  • \(31=1+2+4+8+16\)

1. 代码实现

#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int v[N], w[N];
int f[N];
int main() {
    int n, m, cnt = 0;
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) {
        int a, b, s, k = 1;
        cin >> a >> b >> s;
        while (s >= k) {
            v[++ cnt] = k * a;
            w[cnt] = k * b;
            s -= k;
            k *= 2;
        }
        if (s) {
            v[++ cnt] = s * a;
            w[cnt] = s * b;
        }
    }
    n = cnt;
    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];
    return 0;
}

2. 复杂度分析

  • 时间复杂度:\(O(n\max\{m, \log{s}\})\),其中\(s\)\({s_1,s_2,...,s_n}\)的最大值。
  • 空间复杂度:\(O(m+log{s})\)

(三)、单调队列优化

仔细观察状态转移表达式

\[f_{i,j} = \max\big( f_{i-1,j}, f_{i-1,j-v}+w, f_{i-1,j-2v}+2w, \dots, f_{i-1,j-sv}+sw \big) \]

其中第 \(i\) 件物品的体积为 \(v\),价值为 \(w\),数量上限为 \(s\)

不难发现,\(f_{i,j}\) 仅可能由满足 \(j\equiv k\pmod v,\ k\le j\)\(f_{i-1,k}\) 转移而来。换句话说,对于当前重量 \(j\) 而言,其只依赖于 \(i-1\) 阶段中 \(\bmod v\) 同余的更小的体积 \(k\)

举个例子:设 \(v=2,j=9\),其依赖的状态分别 \(f_{i-1,9},f_{i-1,7},f_{i-1,5},f_{i-1,3},f_{i-1,1}\),均满足\(k\equiv9\pmod2\)

因此,可以对所有可能的容量 \(j\) 按照模 \(v\) 分组,枚举余数 \(r=j\bmod v, \ r\in\{0,1,\dots,v-1\}\),对于每个余数 \(r\),枚举以 \(r\) 为起点,\(v\) 为步长的容量序列

\[r,r+v,r+2v,\dots,r+kv \]

其中 \(k\) 为满足 \(r+kv\le m\) 的最大整数。

固定 \(r\),设当前容量为 \(j=r+kv\),根据上述分析,其应从 \(i-1\) 阶段的 \([j-sv,j-(s-1)v, \dots,j-v,j]\) 中选择使得 \(f_{i,j}\) 最大的容量,这属于分组窗口最大值问题,可以用单调队列优化。

需要声明的是,这里队列存储的不是最大容量,而是使得后续状态最优的容量。

定义队列 \(q\) 维护 \(i-1\) 阶段在模 \(v\) 意义下的大小为 \(s + 1\) 的窗口 \([j-sv,j]\) 中,使得 \(f_{i,j}\) 最优的状态对应的容量,其中 \(h, t\) 分别表示 \(q\) 的头尾索引。

考虑如何维护队列最大值。设后续容量为 \(x\),则当

\[f_{i-1,j}+\frac{x-k}{v}\cdot w \ge f_{i-1,q_t}+\frac{x-q_t}{v} \cdot w \]

时,则说明 \(f_{i-1,j}\) 的作为 \(f_{i,x}\) 的转移状态比 \(f_{i-1,q_{t}}\) 更优,且更靠后,于是移除队尾。但由于 \(x\) 是未知量,于是将上述不等式的 \(x\) 右移,得到

\[f_{i,j}\ge f_{i,q_t}+\frac{k-q_t}{v}\cdot w \]

,用该不等式作为维护队列单调性的判断条件。

然后将 \(j\) 加入队列,并判断是否 \(q_{t}\) 在窗口 \([j - sv,j]\) 内部,即若 \(q_{h}<j-sv\),则将队头出队。

接着用 \(q_{h}\) 更新 \(f_{i,j}\),即

\[f_{i,j}=\max\left( f_{i,j}, f_{i-1,q_h}+\frac{j-q_h}{v}\cdot w \right) \]

参考代码

#include <bits/stdc++.h>
using namespace std;
const int N = 1010, M = 2000;
int f[N][M], q[M];

int main() {
    int n, m;
    cin >> n >> m;
    
    for (int i = 1; i <= n; i ++) {
        int v, w, s;
        cin >> v >> w >> s;
        for (int r = 0; r < v; r ++) {
            int h = 0, t = -1;
            for (int j = r; j <= m; j += v) {
                while (h <= t && f[i - 1][j] >= f[i - 1][q[t]] + (j - q[t]) / v * w) {
                    t --;
                }
                q[++ t] = j;
                if (h <= t && q[h] < j - s * v) h ++;
                f[i][j] = max(f[i][j], f[i - 1][q[h]] + (j - q[h]) /  v * w);
            }
        }
    }
    cout << f[n][m];
    return 0;
}

空间优化

不难发现,与 01 背包类似,\(i\) 阶段仅仅依赖于 \(i-1\) 阶段,但不同的是,由于多重背包是在枚举 \(i\) 阶段的在模 \(v\) 意义下的容量 \(j\) 的同时通过队列维护 \(i-1\) 阶段的在模 \(v\) 意义下的容量 \([j-sv,j]\),若倒序枚举,则无法用队列维护这个 \(k\)

于是我们考虑用一个数组 \(g\) 维护上一个阶段的容量,于是

\[f_j = \max\left( g_j, g_{q_h}+\frac{j-q_h}{v}\cdot w \right) \]

但需要注意的是,\(f_{j}\) 在未被更新前,本身表示的就是原型 \(f_{i-1,j}\) 的状态,所以队列维护的窗口应该变为 \([j-sv,j-v]\)。而这只需要将维护队列最大值与更新状态进行互换即可。

参考代码

#include <bits/stdc++.h>
using namespace std;
const int N = 20010;
int f[N], g[N], q[N];

int main() {
    int n, m;
    cin >> n >> m;
    
    for (int i = 1; i <= n; i ++) {
        int v, w, s;
        cin >> v >> w >> s;
        memcpy(g, f, sizeof(g));
        for (int r = 0; r < v; r ++) {
            int h = 0, t = -1;
            for (int j = r; j <= m; j += v) {
                if (h <= t && q[h] < j - s * v) h ++;
                if (h <= t) f[j] = max(f[j], g[q[h]] + (j - q[h]) / v * w);
                while (h <= t && g[j] >= g[q[t]] + (j - q[t]) / v * w) t --;
                q[++ t] = j;
            }
        }
    }
    cout << f[m];
    return 0;
}

混合背包

题目概述

给定\(n\)个物品和一个容量为\(m\)的背包,以及第\(i\)个物品的体积\(v_i\)、价值\(w_i\)和物品数量\(s_i\)。当\(s_i=-1\)时,表示该物品只有\(1\)个;当\(s_i=0\)时,表示该物品数量无限;当\(s_i>0\)时,表示该物品有\(s_i\)个。求在总容量不超过\(m\)的前提下,可获得的最大价值。

思路

在前面几种背包问题的基础上,该问题并无额外难度。只需根据\(s_i\)的不同取值,分别按照对应的背包模型进行状态转移即可,因此这里不再展开详细讨论。

代码实现

#include <bits/stdc++.h>
using namespace std;

const int N = 1110;
int f[N], q[N], g[N];
int main() {
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) {
        int v, w, s;
        cin >> v >> w >> s;
        if (!s) {
            for (int j = v; j <= m; j ++) {
                f[j] = max(f[j], f[j - v] + w);
            }
        } else if (s == -1) {
            for (int j = m; j >= v; -- j) {
                f[j] = max(f[j], f[j - v] + w);
            }
        } else {
            memcpy(g, f, sizeof(f));
            for (int r = 0; r < v; r ++) {
                int h = 0, t = -1;
                for (int j = r; j <= m; j += v) {
                    if (h <= t && q[h] < j - s * v) h ++;
                    if (h <= t) f[j] = max(g[j], g[q[h]] + (j - q[h]) / v * w);
                    while (h <= t && g[j] >= g[q[t]] + (j - q[t]) / v * w) t --;
                    q[++ t] = j;    
                }
            }
        }
    }
    cout << f[m];
    return 0;
}
  • 该问题中的多重背包部分同样可以采用二进制优化,这里留给读者自行实现。

复杂度分析

  • 时间复杂度:\(O(nm)\)
  • 空间复杂度:\(O(n)\)

二维费用背包

题目概述

给定\(n\)个物品和一个容量为\(m\)、承受重量为\(t\)的背包,以及第\(i\)个物品的体积\(v_i\)、重量\(u_i\)和价值\(w_i\),求在总容量不超过\(m\)且总重量不超过\(t\)的前提下,可获得的最大价值。

思路

对比01背包问题,该问题额外引入了一个承受重量\(t\)的约束,但整体分析思路保持不变。考虑第\(i\)个物品选或不选,其决策依赖于第\(i-1\)个物品的状态,因此在每个阶段中,需要枚举前一阶段在所有可能的容量与重量组合下的状态。

直接考虑滚动数组优化,定义\(f_{j,k}\)表示容量约束为\(j\)、重量约束为\(k\)时的最大价值,其状态转移方程为

\[f_{j,k}=\max(f_{j,k},f_{j-v_i,k-u_i}+w_i) \]

代码实现

#include <bits/stdc++.h>
using namespace std;

const int N = 110;
int f[N][N];
int main() {
    int n, m, t;
    cin >> n >> m >> t;
    for (int i = 1; i <= n; i ++) {
        int v, u, w;
        cin >> v >> u >> w;
        for (int j = m; j >= v; -- j) {
            for (int k = t; k >= u; -- k) {
                f[j][k] = max(f[j][k], f[j - v][k - u] + w);
            }
        }
    }
    cout << f[m][t];
    return 0;
}

复杂度分析

  • 时间复杂度:\(O(nm)\)
  • 空间复杂度:\(O(m)\)

分组背包

题目概述

给定\(n\)组物品和一个容量为\(m\)的背包,以及第\(i\)组物品个数\(s_i\)。对于每个\(s_i\),给定第\(j\)个物品体积\(v_{i,j}\)及价值\(w_{i,j}\),求总容量不超过\(m\)的选择方案的最大价值。

思路

由于每组物品最多只能选择一个物品,可以在01背包的基础上增加一层循环枚举每组物品。该思路较为直接,因此不做过多赘述。接下来主要讨论分组背包滚动数组优化的细节。

代码实现中,枚举顺序为\(i,j,k\),分别表示阶段、容量和物品。其中\(j\)从大到小枚举,且\(j,k\)的枚举顺序不可交换,否则会导致当前阶段的状态覆盖上一阶段的状态。需要注意的是,由于\(k\)\(j\)内循环,所以\(j\)需要枚举到0(而不是类似01背包问题的\(v_i\)),因为在分组背包中,物品体积\(v_{i,k}\)在内层循环\(k\)中确定。

代码实现

#include <bits/stdc++.h>
using namespace std;
constexpr int N = 110;
int f[N], v[N], w[N];

int main() {
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) {
        int s;
        cin >> s;
        for (int j = 1; j <= s; j ++) cin >> v[j] >> w[j];
        for (int j = m; j; -- j) {
            for (int k = 1; k <= s; k ++) {
                if (j >= v[k]) {
                    f[j] = max(f[j], f[j - v[k]] + w[k]);
                }
            }
        }
    }
    cout << f[m];
  return 0;
} 

复杂度分析

  • 时间复杂度:\(O(nm \max\{s\})\),其中\(s\)\(\{s_1,s_2,...,s_n\}\)的最大值。
  • 空间复杂度:\(O(m)\)

有依赖的背包(树上背包)

题目概述

给定\(n\)组物品和一个容量为\(m\)的背包,以及第\(i\)个物品的体积\(v_i\)、价值\(w_i\)和其父节点编号\(p_i\)。物品之间具有依赖关系,且依赖关系组成一棵树的形状,如果选择一个物品,则必须选择它的父节点。求总容量不超过\(m\)的选择方案的最大价值。

思路

由于选择某个物品必须同时选择其父节点,我们不妨从根节点\(r\)开始进行决策。设根节点的体积为\(v_r\)。当背包容量\(m < v_r\)时,根节点无法被选择,问题无解;当\(m \ge v_r\)时,根节点必须选择,此时剩余可分配给其子树的容量为\(m - v_r\)

设根节点\(r\)共有\(d\)个直接子节点,其对应的子树根节点分别为

\[s_1, s_2, \dots, s_d . \]

对每一棵子树,需要决定是否参与选择以及分配多少容量。记分配给第\(i\)棵子树的容量为\(k_i\),则这些容量满足

\[\sum_{i=1}^{d} k_i \le m - v_r \]

不同子树之间的选择相互独立,因此问题转化为:在容量约束下,如何在各个子树之间分配容量以使得总价值最大。

为了刻画这一过程,我们依次考虑各个子树。不妨用\(s\)表示任意一个子树的根节点,设分配给该子树的容量为\(k\),其体积和价值分别为\(v\)\(w\)。此时有两种选择:

  • 若为该子树分配容量\(k \ge v\),则必须选择节点\(s\),并在剩余容量\(k - v\)的约束下,取得其所有子树中的最大价值;
  • 否则,无法选择节点\(s\),且该子树中的所有节点均不能被选择,可直接跳过该子树。

由于分配给子树的容量\(k\)并不确定,其取值范围为\([0,m-v_r]\)。因此,需要预先求出以\(s\)为根的子树在不同容量约束下所能取得的最大价值。由此可见,该问题具有明显的最优子结构,各子问题的形式完全相同但规模更小;同时,由于物品之间存在一个构成树形结构的父子依赖关系,可以通过递归处理。

树结构无需显示处理边界问题,因为采用图的存储方式,没有空指针,那么当递归到叶节点,会自动返回。

当根节点\(r\)依次枚举其子树\(s_1,s_2,\dots,s_d\)时,需要同时考虑每棵子树是否被选择以及为其分配的具体容量。这一过程等价于分组背包问题:将每一棵子树视为一个物品组,不同的容量分配方案对应组内的不同物品。

综上,为形式化描述这一过程,定义状态\(f_{u,i,j}\)表示:在已选择节点\(u\)的前提下,仅考虑前\(i\)颗子树,并在容量约束为\(j\)的情况下所能获得的最大价值,其状态转移方程为

\[f_{u,i,j}=\max\{f_{u,i-1,j}, f_{u,i-1,j-k}+f_{s,d,k}) \]

在实际情况中,可以通过分组背包的压缩方式,将状态转移方程转换压缩为

\[f_{u,j}=\max(f_{u,j}, f_{u,j-k}+f_{s,k}) \]

但需要注意枚举顺序。

代码实现

#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int v[N], w[N], f[N][N];
vector<int> g[N];
int n, m, r;

void dfs(int u, int lim) {
    for (int i = v[u]; i <= lim; i ++) f[u][i] = w[u];

    for (int s : g[u]) {
        dfs(s, lim - v[u]);
        for (int j = lim; j >= v[u]; -- j) {
            for (int k = 0; k <= j - v[u]; k ++) {
                f[u][j] = max(f[u][j], f[u][j - k] + f[s][k]);
            }
        }
    }
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) {
        int p;
        cin >> v[i] >> w[i] >> p;
        if (p != -1) g[p].push_back(i); 
        else r = i;
    }

    dfs(r, m);
    cout << f[r][m];
    return 0;
}

背包求方案数

题目概述

给定\(n\)个物品和一个容量为\(m\)的背包,以及第\(i\)个物品体积\(v_i\),价值\(w_i\),每个物品只有一个,求总容量不超过\(m\)的选择方案的最大价值所对应的方案数,由于数量可能很大,需要对$ 10^9+7$取模。

思路

由于已知\(f_{n,m}\)表示最大价值,一个显然的事实是,所有能转移到\(f_{n,m}\)的状态表示\(f_{n,m}\)的方案数,于是记录在转移过程中所有可以到达\(f_{n,m}\)的状态即可。

定义\(c_{i,j}\)表示\(f_{i,j}\)所对应的方案数,设第\(i\)阶段的物品容量为\(v_i\),价值为\(w_i\),则\(c_{i,j}\)所对应的转移方程

\[c_{i,j}=\begin{cases} c_{i-1,j}, \quad f_{i,j}< f_{i-1,j-v_{i}}+w_i \\ c_{i-1,j}+c_{i-1,j-v_i}, \quad f_{i,j}=f_{i-1,j-v_{i}+w_i} \end{cases} \]

定义初始条件:\(c_{0,j}=1, \ \forall j\in [0,m]\),表示一个都不选的方案数。

代码实现

#include <bits/stdc++.h>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int f[N], c[N];

int main() {
    int n, m;
    cin >> n >> m;
    for (int i = 0; i <= m; i ++) c[i] = 1;
    for (int i = 1; i <= n; i ++) {
        int v, w;
        cin >> v >> w;
        for (int j = m; j >= v; -- j) {
            int x = f[j - v] + w;
            if (x > f[j]) {
                f[j] = x;
                c[j] = c[j - v];
            } else if (x == f[j]) {
                c[j] = (c[j] + c[j - v]) % mod;
            }
        }
    }   
    cout << c[m];
    return 0;
} 
  • 时空复杂度同01背包,不做过多讨论。

背包求具体方案

给定\(n\)个物品和一个容量为\(m\)的背包,以及第\(i\)个物品体积\(v_i\),价值\(w_i\),每个物品只有一个,求总容量不超过\(m\)的选择方案的最大价值所对应的具体方案,要求字典序最小。

思路

在经典01背包中,状态转移方程为

\[f_{i,j}=\max\{f_{i-1,j},f_{i-1,j-v_i}+w_i\} \]

但若需要恢复具体选取方案,则必须保留完整的阶段信息,以便在状态表中回溯决策过程。

为此,我们重新定义状态:令\(f_{i,j}\)表示考虑物品从\([n,i]\),且容量约束为\(j\)的前提下,所能获得的最大价值。对应的转移方程为

\[f_{i,j}=\max\{f_{i+1,j},f_{i+1,j-v_i}+w_i\} \]

于是,当回溯\(f_{i,j}\)时,有三种情况:

  • \(f_{i,j}=f_{i+1,j}\):则表示第不选\(i\)个物品;
  • \(f_{i,j}=f_{i+1,j-v_i}+w_i\):则表必须选第\(i\)个物品;
  • \(f_{i,j}=f_{i+1,j}=f_{i+1,j-v_i}+w_i\):则既可以选也可以不选,但选择了该物品字典序更小。

因为从大到小进行回溯决策,且该决策是在全局最优的情况下进行的,且每一步均保持最优性,因此最终构造出的方案是全局最优解。

代码实现

#include <bits/stdc++.h>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int f[N][N], v[N], w[N];

int main() {
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) cin >> v[i] >> w[i];
    for (int i = n; i; -- 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][j], f[i + 1][j - v[i]] + w[i]);
            }
        }
    }    
    int j = m;
    for (int i = 1; i <= n; i ++) {
        if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i]) {
            cout << i << ' ';
            j -= v[i];
        }
    }
    return 0;
} 

复杂度分析

  • 时间复杂度:同01背包。
  • 空间复杂度:\(O(nm)\),由于无法用滚动数组。

其他

恰好装满的背包(01背包)

对背包问题进行一个扩展,介绍另外一种状态定义方式。

在前面,我们知道在对第\(i\)个阶段在\(j\)容量下的决策的最大价值依赖前\(i-1\)阶段决策后容量约束为\(j-v_i\)的最大价值。

事实上,当\(n\)阶段决策后,最优解一定在某个确定的\([0,m]\)的容量中,假设其为\(j\),考虑第\(n\)个物品的两种决策方式:

  • 若没有拿\(n\)阶段的物品,则最大价值等于前\(n-1\)阶段决策后容量恰好为\(j\)的价值;
  • 若拿了\(n\)阶段的物品,设第\(n\)阶段的容量为\(v\),则最大价值等于前\(n-1\)阶段决策后容量恰好为\(j-v\)的价值。

于是定义状态\(f_{i,j}\)表示考虑前\(i\)个物品,且总容量恰好为\(j\)的最大价值。由于\(1\)起始阶段,设其容量为\(v_1\),需要定义初始状态\(f_{1,0},f_{1,v_1}\)。不过注意到,这两个状态都可以由\(f_{0,0}\)转移,于是定义初始条件:\(f_{0,0}=0,f_{i,j}=-\infty, \ \forall i,j\),状态转移方式不变。

滚动数组优化方式以及时空复杂度和前面相同,不在赘述。

代码实现

#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int v[N], w[N];
int f[N];
int main() {
    int n, m;
    cin >> n >> m;
    memset(f, -0x3f, sizeof(f));
    f[0] = 0;
    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]);
        }
    }
    int ans = 0;
    for (int i = 0; i <= m; i ++) ans = max(ans, f[i]);
    cout << ans;
    return 0;
}
  • 其他背包问题也可以使用该状态定义方式计算。
posted @ 2025-12-20 16:29  uvwijk  阅读(20)  评论(0)    收藏  举报