树形背包上下界优化复杂度分析
上下界优化树形背包
分析
常见的问题是树上连通块相关。因为连通块就是选子要选父,除了根以外,这是可以 dp 的。
通常我们需要记录关于连通块的和之类的,就要上背包了。
直接设 dp[u][i]
表示以 u 为根且权值和恰好/不大于是 i 的连通块的信息。
void dfs(int u,int fa){
// initialize dp[u][val[u]]/dp[u][val[u]~m]
for(int v:e[u]){
if(v==fa)continue;
dfs(v,u);
for(int i=m;i>=0;--i)
for(int j=0;j<=min(m,i);++j)
// dp[u][i] <- dp[u][i-j],dp[v][j]
}
}
这个过程关键是背包理解。
单纯来看,就是分子树为组,分组背包即可。
但是容易发现,这样子是时间复杂度很高,是 \(O(nm^2)\)。
我们使用上下界优化,即保证调用的 dp 位置是有意义的。
void dfs(int u,int fa){
// initialize dp[u][val[u]]/dp[u][val[u]~m]
for(int v:e[u]){
if(v==fa)continue;
dfs(v,u);
// get L,R
for(int i=R;i>=L;--i){
// get l,r
for(int j=l;j<=r;++j)
// dp[u][i] <- dp[u][i-j],dp[v][j]
}
// maintain value of next L,R
}
}
具体来说,若背包是 \([0,m]\) 的,且一颗子树内产生的体积是 \([0,\text{size}_u]\),是这样的
void dfs(int u,int fa){
// initialize dp[u][val[u]]/dp[u][val[u]~m]
for(int v:e[u]){
if(v==fa)continue;
dfs(v,u);
int _sz=sz[u]+sz[v];
int L=0,R=min(m,_sz);
for(int i=R;i>=L;--i){
int l=0,r=min(m,sz[v]);
for(int j=l;j<=r;++j)
// dp[u][i] <- dp[u][i-j],dp[v][j]
}
sz[u]=_sz;
}
}
实际上我们必须让 dp[u][i-j]
有意义,不这么做可能会导致数组越界,在多次树型背包时可能会访问到之前没有清空的位置导致问题。
void dfs(int u,int fa){
// initialize dp[u][val[u]]/dp[u][val[u]~m]
for(int v:e[u]){
if(v==fa)continue;
dfs(v,u);
int _sz=sz[u]+sz[v];
int L=0,R=min(m,_sz);
for(int i=R;i>=L;--i){
int l=max(0,i-sz[u]),r=min(m,sz[v],i);
for(int j=l;j<=r;++j)
// dp[u][i] <- dp[u][i-j],dp[v][j]
}
sz[u]=_sz;
}
}
这样优化了多少呢?在上述那个问题中,复杂度进化成了 \(O(nm)\)。
考虑证明这个复杂度。
证明
深层来看,背包的过程其实是将子树两两合并起来,合并背包。
当没有 m 的限制时,以 u 为根的两棵子树之间的点对的 LCA 就是点 u,合并子树的代价是两棵子树的点对数,并且每个点对只会在 LCA 合并一次。时间复杂度为 \(O(n^2)\)。容易感性理解,理性理解见 ouuan的blog。
当考虑 m 的限制时,可能 n 很大,m 很小,但 nm 是可以接受的,我们猜复杂度是 \(O(nm)\) 的。具体证明需要考虑两颗子树合并时代价最坏是 \(\le\min(m,\text{size}_v)\cdot \min(m,\text{size}_w)\),考虑分类讨论去掉 \(\min\)。
- 当 \(m\le \text{size}_v\) 且 \(m\le \text{size}_w\) 时,代价是 \(m^2\),考虑最下面的大于 m 的子树的根,发现所有第一类合并都是在这些根的虚树的虚点上的,合并次数取决与最下层点数。而最下层每个子树大小至少是 m 且互不重复,故至多合并 \(\dfrac{n}{m}\) 次,复杂度是 \(O(\dfrac{n}{m}\cdot m^2)=O(nm)\)。
- 当 \(m\le \text{size}_v\) 且 \(m\ge \text{size}_w\) 时,代价是 \(m\cdot\text{size}_w\),发现子树 w 只会合并一次,因为合并后就大于 m 了,也就是说所有的 w 互不包含,则 \(\sum \text{size}_w=O(n)\),复杂度是 \(\sum m\cdot\text{size}_w=m\sum\text{size}_w=O(nm)\)。
- 当 \(m\ge \text{size}_v\) 且 \(m\ge \text{size}_w\) 时,代价是 \(\text{size}_v\cdot\text{size}_w\),其实这些点是第一类中最下面的大于 m 的子树中的节点,考虑更精准的估计,即考虑枚举时其实是对子树前缀和取的 \(\min\),它们之间合并复杂度最坏是 \(O(m^2)\),而这样的子树只有 \(\dfrac{n}{m}\) 个,复杂度是 \(O(\dfrac{n}{m}\cdot m^2)=O(nm)\)。
至此,就证明了体积为 1 ,背包限制是 m 时时间复杂度是 \(O(nm)\)。
那是不是体积为任意数时复杂度都不会坍缩呢?
很可惜不是,随机数据也能在体积任意时把复杂度卡到 \(O(n^2m)\)。
在 loj 树形背包 的数据范围下
import random
rand=random.randint
n=1000
w=60000
print(n,w)
for i in range(n):
print(rand(0,i),end=' ')
print()
for i in range(n):
print(rand(1,200),end=' ')
print()
for i in range(n):
print(rand(0,5000),end=' ')
print()
运算量最坏大概是 1.2e10 级别的,直接随数据大概跑 5s 左右。
那这样的复杂度时啥?我们套用上面的分析。令 \(\text{sum}_u\) 表示子树和,\(V\) 是所有权值和。
两颗子树合并时代价最坏是 \(\le\min(m,\text{sum}_v)\cdot \min(m,\text{sum}_w)\)。
- 当 \(m\le \text{sum}_v\) 且 \(m\le \text{sum}_w\) 时,代价是 \(m^2\),考虑最下面的大于 m 的子树的根,发现所有第一类合并都是在这些根的虚树的虚点上的,合并次数取决与最下层点数。而最下层每个子树权值至少是 m 且互不重复,故至多合并 \(\dfrac{V}{m}\) 次,复杂度是 \(O(\dfrac{V}{m}\cdot m^2)=O(Vm)\)。
- 当 \(m\le \text{sum}_v\) 且 \(m\ge \text{sum}_w\) 时,代价是 \(m\cdot\text{sum}_w\),发现子树 w 只会合并一次,因为合并后就大于 m 了,也就是说所有的 w 互不包含,则 \(\sum \text{sum}_w=O(V)\),复杂度是 \(\sum m\cdot\text{sum}_w=m\sum\text{sum}_w=O(Vm)\)。
- 当 \(m\ge \text{sum}_v\) 且 \(m\ge \text{sum}_w\) 时,代价是 \(\text{sum}_v\cdot\text{sum}_w\),其实这些点是第一类中最下面的大于 m 的子树中的节点,考虑更精准的估计,即考虑枚举时其实是对子树前缀和取的 \(\min\),它们之间合并复杂度最坏是 \(O(m^2)\),而这样的子树只有 \(\dfrac{V}{m}\) 个,复杂度是 \(O(\dfrac{V}{m}\cdot m^2)=O(Vm)\)。
至此,就证明了体积为任意,背包限制是 m 时时间复杂度是 \(O(Vm)\),但其实实际效果可能只有理论复杂度的 \(\dfrac{1}{20}\)。
我们甚至可以讨论权值为负数时的情况,其实代价最坏就是子树内权值的绝对值之和,不就与上面一样了么,时间复杂度 \(O(Vm)\),\(V\) 是绝对值之和。