【动态规划】树上背包,依赖背包的小技巧
前排提醒,本文转载自 Sshwy's Notes
转载仅供学习使用。
可能大家都知道树上背包合并 \(O(n^3)\) 对子树大小取min可以优化到 \(O(n^2)\) 。但是对于树上依赖背包问题,背包合并的复杂度仍不能接受。考虑形式化的问题:
一棵 \(n\) 个结点有根树,每个结点 \(i\) 有 \(s_i\) 个价格为 \(c_i\) ,价值为 \(v_i\) 的物品。除了根结点,要购买某个结点的物品必须在它的父节点购买至少一件。求总费用为x时的最大化价值。\(x,v_i \le m\) 。
一个朴素的 DP 是 $f(i,j) $ 表示在结点 \(i\) 的子树中购买费用不超过 \(j\) 的物品的最大价值。转移时 \(O(m^2)\) 进行背包合并,总复杂度 \(O(nm^2)\)。
我们考虑换一种 DP 的顺序。朴素 DP 是以递归子结构的形式进行 DP。考虑按照 DFS 序 DP。
我们以后序遍历(先按序遍历子节点,再遍历根结点)的方式求出结点的 DFS 序。则对于结点 \(u\) ,设其 DFS 序为 \(D_u\),记它的子树大小为 \(S_u\)。同时我们记 DFS 序为i的结点为 \(V(i)\)。
通俗地说,我们设 \(f(i,j)\) 表示在 DFS 序小于等于 \(i\) 的结点构成的连通块中选物品,总费用不超过 \(j\) 时的最大价值。转移的时候枚举 \(V(i)\) 上选不选物品,从 \(f(i−1),f(i−S_{V(i)})\) 转移。
如果上述状态定义不能理解,我们接下来做一些严谨的描述。首先给出后序遍历 DFS 序的一些性质:
- 如果 \(u\) 不是叶子结点,则 \(V(D_u−1)\) 是 \(u\) 的儿子(儿子序列中的最后一个儿子)
- 对于树中的任意结点 \(u\) ,如果 \(D_u \ge S_u\) ,则 \(V(D_u−S_u)\) 是离 \(u\) 最近的,存在前兄弟结点的祖先结点(也可能是 \(u\) 自己)的前兄弟结点。说的很绕口,可以自己画图理解一下。
知道这个以后,我们定义 \(P(i)=i−S_{V(i)}\) ,即 DFS 序为 \(i\) 的结点由性质 \(2\) 导出的结点;如果\(D_u<S_u\) 则令 \(P(u)=0\) 。
因此更严谨地说,我们设 \(f(i,j)\) 表示在子树 \(V(i), V(P(i)), V(P(P(i))), V(P(P(P(i)))), \cdots\) 构成的森林中按依赖关系选物品,总费用不超过 \(j\) 的最大价值。
转移的时候,如果 \(V(i)\) 上选物品才能从 \(f(i−1)\) 转移(要选子节点必须选父节点上的东西)。此外在任何时候都可以从 \(f(P(i))\) 转移。
对于上述多重依赖背包问题,我们首先可以从 \(f(P(i))\) 转移到 \(f(i)\)(直接复制)。然后我们强制选一个物品,可以从 \(f(i−1)\) 转移过来。然后对于剩下的 \(s_{V(i)}−1\) 个物品,使用单调队列优化多重背包或者二进制分组来更新 \(f(i)\) 即可。复杂度 \(\mathcal{O}(nm)\) 或 \(\mathcal{O}(nm\ log_2m)\)。
const int N = 5000;
struct QAQ {int nxt, t;} e[N << 1];
int h[N], le;
int n, m;
int w[N], c[N], s[N];
int totdfn, sz[N], f[N][N];
void add(int f, int t) {e[++le] = (QAQ) {h[f], t}, h[f] = le;}
int dfs(int u) {
sz[u] = 1;
for (int i = h[u], v; v = e[i].t, i; i = e[i].nxt)
sz[u] += dfs(v);
int i = ++totdfn;
int lim = s[u];
f[i][0] = 0;
// ?f[i,j] <- f[i-sz[u],j] , f[i-1,j]
for (int j = m; j >= 0; j--) {
if (j >= c[u]) f[i][j] = max(f[i][j], f[i - 1][j - c[u]] + w[u]); // 至少选一个物品,才能从 i-1 转移
f[i][j] = max(f[i][j], f[i - sz[u]][j]);
}
--lim;
for (int k = 1; k <= lim; lim -= k, k <<= 1) { // 在之前 i-1 和 i-sz 转移的基础上,加入多个物品(二进制)
for (int j = m; j >= k * c[u]; j--) {
f[i][j] = max(f[i][j], f[i][j - k * c[u]] + k * w[u]);
}
}
if (lim) { // 剩下一个二进制余项
for (int j = m; j >= lim * c[u]; j--) {
f[i][j] = max(f[i][j], f[i][j - lim * c[u]] + lim * w[u]);
}
}
return sz[u];
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n >> m;
for (int i = 2, x; i <= n; ++i) {
cin >> x;
add(x, i);
}
for (int i = 1; i <= n; ++i)
cin >> w[i] >> c[i] >> s[i];
dfs(1);
for (int i = 1; i <= m; ++i) cout << f[totdfn][i] << " \n"[i == m];
}
进击的Bill_Yang桑也在博客提出这个技巧,通过依赖关系优化树上背包问题
另外附上一道模板题
「bzoj4182」Shopping - 点分治+多重背包
题目链接:Here