【总结】树上背包
一般形式
令 \(f[x,t]\) 以 \(x\) 的子树中选 \(t\) 们课的最大价值。
\[f[x,t]=\max_{\sum c_i=t-1} \{ f[i,c_i] \} + score[x]
\]
对于 \(\max_{\sum c_i=t-1} \{ f[i,c_i] \}\) 相当于一个分组背包模型,对于每个儿子的决策相当于一个物品组。
直接做的复杂度为 \(O(nm^2)\) 。
void dfs(int x){
vis[x] = true;
for (int i = head[x]; i;i = fail[i]) {
int v = edge[i];
if (vis[v]) continue;
dfs(v);
for (int t = m; t > 0; t--)
for (int j = 1; j <= t; j++)
f[x][t] = std::max(f[x][t],f[x][t - j] + f[v][j]);
}
if (x != 0)
for (int i = m; i >= 1; i--)
f[x][i] = f[x][i - 1] + a[x];
}
树上背包的上下界优化
复杂度 \(O(nm)\) 。
void dfs(int x){
vis[x] = true; size[x] = 1;
for (int i = head[x]; i;i = fail[i]) {
int v = edge[i];
if (vis[v]) continue;
dfs(v);
for (int t = std::min(m,size[x] + size[v]); t > 0; t--)
for (int j = std::max(1,t - size[x]); j <= t && j <= size[v]; j++)
f[x][t] = std::max(f[x][t],f[x][t - j] + f[v][j]);
size[x] += size[v]; //细节:在最后更新size保证 t - size[x] 的 bound
}
if (x != 0)
for (int i = m; i >= 1; i--)
f[x][i] = f[x][i - 1] + a[x];
}
技巧:体积价值互换后二分
直接按每个点的点权为价值做 \(\text{DP}\) 不好做。
体积和价值互换后,其依然能保证单调性,即随着体积的增大价值也一定增大。
那么就可以将体积价值互换,每次查询二分。
具体地:考虑 \(\text{DP}\) 出选 \(m\) 个点的最小花费,随着花费随着点数的增加单调递增。
那么就可以设计一个树上背包 \(f[u,v,0/1/2]\) 表示以 \(u\) 为根的子树中选 \(v\) 个点不选根节点且都摸鱼,选根节点且都摸鱼,选根节点且除了根节点以外其他点都摸鱼的最小花费。
上下界优化后复杂度为 \(O(nm)\) 。
总体思想即:原来的价值变体积,原来的价值变花费,DP 之后二分最大体积。
状态设计与代码实现的细节
- 对于树形背包根节点的分类讨论,有两种处理手法,一种是先考虑不选的情况在子树内做分组背包之后加入根节点,另一种就是设计状态时加一维表示根节点是否选择,而对于加一维的方法需要主义不同状态间的转移顺序。