前排提醒,本文转载自 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] << "
"[i == m];
}
进击的Bill_Yang桑也在博客提出这个技巧,通过依赖关系优化树上背包问题
另外附上一道模板题
「bzoj4182」Shopping - 点分治+多重背包
题目链接:Here