[模板]树上背包|[CTSC1997] 选课 题解
[CTSC1997] 选课 题解
(符号约定:\(n\) 课程总数,\(m\) 选择的课程门数)
树上背包典题,值得深入分析。
图论建模
可以把题目中的选课关系抽象成一棵有根树森林:每个节点代表一门课程,节点的父亲代表这门课程的先修课。为了避免对多个连通块操作带来的不便,可以新增一个虚拟的 \(0\) 号节点作为原森林中所有根节点的父节点,点权为 \(0\)。由于我们一定会选择 \(0\) 号节点,所以要使 \(m \leftarrow m + 1\)。于是,我们的目标就是:在树上选择一个包含根节点且大小为 \(m\) 的连通块,使得连通块中的点权之和最大。
dp 的初步实现
设 \(f_{u, i}\) 表示在 \(T(u)\) 中选择 \(i\) 个节点的最大收益。我们想从 \(u\) 的子节点转移过来。由于一定会选择 \(i\) 节点,所以要在 \(u\) 的子节点的子树中选择 \(i-1\) 个节点。
令 \(u\) 的子节点的个数 \(|son(u)| = l\),子节点的集合 \(son(u) = \{v_1, v_2, \cdots, v_l\}\)。假设我们从 \(u\) 的第 \(j\) 个子节点的子树中选择 \(a_j\) 个节点,那么转移方程即为:
实质上可以把求 \(f_{u, i}\) 的过程看作一个分组背包问题:有 \(l\) 类物品,每类物品有若干个。第 \(a\) 的第 \(b\) 个物品的价值就是 \(f_{v_a, b}\)。要从这 \(l\) 类物品中的每一类中选择一个物品,使得总价值最大。
显然,无法直接使用上面的转移方程,因为枚举满足 \(\sum a_j = i-1\) 的数值组合 \((a_1, a_2, \cdots, a_l)\) 是困难的。
另辟蹊径,设 \(f_{u, c, i}\) 表示在 \(u\) 的前 \(c\) 棵子树中选择 \(i\) 个节点的最大收益。这样就可以顺次枚举 \(u\) 的子节点,每次求当前子节点的答案时都可以由前面的结果合并过来。设 \(blo\) 表示加入第 \(c\) 个子节点之前 \(u\) 和它的子树构成的连通块的大小(实际上是 \(1 + \sum_{k=1}^{c-1} size_{k}\)),那么转移方程为:
翻译:枚举 \(j\),从新的子树 \(T(v_c)\) 中选 \(j\) 个点,从原先的连通块(\(u\) 与前 \(c-1\) 个子树形成的)中选 \(i-j\) 个点,此时的收益为 \(f_{u, c-1, i-j} + f_{v, |son(v)|, j}\)。
这里枚举的边界很重要。
- \(0 \le j \le \min(size_v, i)\):在 \(T(v_c)\) 中,最少选 \(0\) 个点,最多不能超过 \(size_v\) 和 \(i\)。
- \(0 \le i-j \le blo\):在加入第 \(c\) 个子节点之前 \(u\) 和它的子树构成的连通块中,最少选 \(0\) 个点,最多选 \(blo\) 个点。
综上,可以得出 \(j\) 的上界和下界:
(我就是因为边界问题没搞明白,调了一道树上背包一周多……)
初始化:\(f_{u, 0, 1} = val(u); \forall 1 \le c \le |son(u)|,f_{u, c, 0} = 0\)
代码:
void dfs(int u)
{
sz[u] = 1, f[u][0][1] = val[u];
int &cnt = son[u];
for(int v : G[u])
{
cnt++;
dfs(v);
sz[u] += sz[v];
for(int i = 1; i <= min(m, sz[u]); i++)
{
for(int j = max(0, i + sz[v] - sz[u]); j <= min(i-1, sz[v]); j++)
f[u][cnt][i] = max(f[u][cnt][i], f[v][son[v]][j] + f[u][cnt-1][i-j]);
}
}
}
代码中的 sz[u] - sz[v]
就相当于上文中的 \(blo\)。
滚动数组优化
因为 \(f_{u, c, [\cdot]}\) 只会从 \(f_{u, c-1, [\cdot]}\) 转移过来,所以可以把第二个维度滚掉。空间复杂度从 \(O(n^2m)\) 降为 \(O(nm)\)。
代码:
void dfs(int u)
{
sz[u] = 1, f[u][0][1] = val[u];
int now = 0, lst;
for(int v : G[u])
{
now ^= 1, lst = now ^ 1, son[u]++;
dfs(v);
sz[u] += sz[v];
for(int i = 1; i <= min(m, sz[u]); i++)
{
f[u][now][i] = 0; // 这里不清零或许也是对的
for(int j = max(0, i + sz[v] - sz[u]); j <= min(i-1, sz[v]); j++)
f[u][now][i] = max(f[u][now][i], f[v][son[v] & 1][j] + f[u][lst][i-j]);
}
}
}
滚动数组优化 II
这次我们可以真正彻底地去掉第二维。
因为 \(i, j \ge 0\),所以 \(j, i-j \le i\)。因此可以倒序枚举。
虽然这样做并没有空间复杂度上的优化,但我们确实减少了空间占用。
代码:
void dfs(int u)
{
sz[u] = 1, f[u][1] = val[u];
for(int v : G[u])
{
dfs(v);
sz[u] += sz[v];
for(int i = min(m, sz[u]); i >= 1 ; i--)
{
for(int j = max(0, i + sz[v] - sz[u]); j <= min(i-1, sz[v]); j++)
f[u][i] = max(f[u][i], f[v][j] + f[u][i-j]);
}
}
}
时间复杂度
时间复杂度是 \(O(nm)\),但我不会证明。
咕咕咕