hihoCoder #1063 缩地
题意
给定一棵带边权及点权的有根树 \(T(V,E)\) ( \(|V| \le 100\) , 边权 \(w\colon E \to \mathbb{N}^*\) , \(w \le 10^4\) , 点权 \(v \colon V \to \{0,1,2\}\) ). 要求回答 \(q\) ( \(q \le 10^5\) ) 组询问, 询问格式: 给定 $d \in \mathbb{N} $ ( \(d \le 10^6\) ), 问从根节点出发累计移动距离不超过 \(d\) , 经过的节点权值之和的最大值 (重复经过的节点只算一次).
树形背包
将问题向树形背包转化.
为了描述方便, 令 \(N=|V|\) , 将树的节点从 \(1\) 到 \(N\) 编号, 且令根节点编号为 \(1\) .
DP状态:
\(dp[i][j]\) : 在子树 \(i\) 中移动, 距离不超过 \(j\) 所能获得的最大收益.
复杂度 \(O(Nd^2)\) (?) , 不能容忍.
注意到点权不超过2, 从而节点权值和为 \(O(N)\) .
考虑新DP状态:
\(dp[i][j]\) : 在子树 \(i\) 中移动, 收益 恰好 为 \(j\) 所需的最小移动距离.
复杂度为 \(O(N^2)\) .
状态细化
上述两种DP状态是从一般背包问题出发得出的, 在本题中并不能直接转移, 需要细化 (增维); 两种DP状态的转移方式类似, 以第二种DP状态为例:
\(dp[0][i][j]\) : 在子树 \(i\) 中移动, 收益恰好为 \(j\) 且最后回到节点 \(i\) 所需的最小移动距离.
\(dp[1][i][j]\) : 在子树 \(i\) 中移动, 收益恰好为 \(j\) , 不论最后在哪个节点, 所需的最小移动距离.
转移方程
将子树看做 泛化物品 合并即可.
泛化物品
(见 崔添翼《背包九讲》)
泛化物品可一般地表示为二元组
\((n, g)\) , \(n \in \mathbb{N}\) , \(g \colon \{0, 1, 2, \dots, n\} \to \mathbb{Z}\) , \(g\) 是收益函数.
采用 启发式合并 的方法合并两泛化物品 \((n_1, g_1)\) , \((n_2, g_2)\) 的复杂度为 \(O(n_1n_2)\) .
Implementation
#include <bits/stdc++.h>
using namespace std;
const int N=105;
vector<pair<int,int>> g[N];
const int M=205;
int dp[2][N][M], v[N], tmp[M];
int dfs(int u, int f){
int sum=v[u];
// boundary condition
dp[0][u][v[u]]=dp[1][u][::v[u]]=0;
for(auto e: g[u]){
int v=e.first, w=e.second;
if(v!=f){
int sum_v=dfs(v, u);
sum+=sum_v;
for(int i=sum; i>=::v[u]+::v[v]; --i)
for(int j=::v[v]; j<=min(i-::v[u], sum_v); j++){
dp[0][u][i]=min(dp[0][u][i], dp[0][u][i-j]+dp[0][v][j]+2*w);
dp[1][u][i]=min(dp[1][u][i], dp[1][u][i-j]+dp[0][v][j]+2*w);
dp[1][u][i]=min(dp[1][u][i], dp[0][u][i-j]+dp[1][v][j]+w);
}
}
}
return sum;
}
int main(){
int n;
scanf("%d", &n);
for(int i=1; i<=n; i++)
scanf("%d", v+i);
memset(dp, 0x3f, sizeof(dp));
//boundary conditon
// for(int i=1; i<=n; i++) // This is not the boundary!
// dp[0][i][0]=0;
for(int i=1; i<n; i++){
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
g[u].push_back({v, w});
g[v].push_back({u, w});
}
int sum=dfs(1, 1);
int q;
scanf("%d", &q);
for(; q--; ){
int d;
scanf("%d", &d);
for(int i=sum; i>=0; i--)
if(dp[1][1][i]<=d){
printf("%d\n", i);
break;
}
}
return 0;
}
Reference
本文参考了解题报告1和解题报告2.
对比这两篇解题报告可更好地理解:
- DP的基本原理与概念
- 泛化物品的合并
其他
关于树形背包问题的复杂度, 我感觉应该能给出一个更紧的复杂度, 仍需研究.
2015年国家集训队张恒捷的论文《DP的一些优化技巧》$\S$2.7 启发式合并DP数组 中讨论了这个问题.
将一个长为 \(x\) 的数组看做一个权重为 \(x\) 的点, 所有点权重之和为 \(m\) . 合并权重为 \(x\) 与 \(y\) 的点需要花费 \(O(T(x,y))\) 的复杂度, 得到一个权重为 \(x+y\) 的点.
\(T(x,y) =xy\)
这种情况多数在树上问题出现. 无论按什么顺序合并, 总复杂度都是 \(O(m^2)\) 而非 \(O(m^3)\) . 关于这一点, 我们可以把合成的点向合成它的点连边, 整个结构就是一棵树, \(T(x,y)\) 相当于枚举左右子树内所有点的匹 (分?) 配方案. 这时只要注意到每一对点只会在他们 LCA 处枚举到就行了.
所以该情况的复杂度为 \(O(m^2)\) .
不难分析出这道题的复杂度为 \(O(N^2)\) .
需要特别注意的是, 这个题目不是一个典型的背包问题, 而是一个树上的合并问题. 在背包问题中, 合并两泛化物品受制于背包容量 \(C\) , 复杂度不超过 \(O(C^2)\) . 树形背包的复杂度还需要研究.