浅谈一类点分治优化 DP
wjyppm1403 · · 算法·理论
可能更好的阅读体验
1. 引入与介绍
对于一类树上连通块或路径问题,合并子树(可以看作卷积)的复杂度很高,但是插入一个点和自己与自己合并(可以看作点积)的复杂度可以用点分治来进行优化。
以一道例题引入:Ridiculous Netizens - HDU 6643
首先你会想到用树形背包来做,但是问题在于如果你直接设置为
- 你无法保证你选取的方案子树是连通的。
- 在不考虑乘积的情况下,你的合并是
O(m\sqrt{m}) 的,因为你要枚举约数。
先保证状态是
转化思路,我们考虑每个点作为连通块内部的贡献,我们先考虑强制这个连通块经过根,可以发现在这种情况下,如果一个点在连通块中,那么它的父亲必须也在。
自此,由于选取一个点就必须选它的父亲,所以要去选一个子树就必须选这个子树的根。我们可以把原命题转化为一个单点加入问题。
设
如果认为自顶向下的 DP 比较难想,当然我们可以在 DFS 序上进行 DP,这样选择一个结点就是转移到 DFS 上的下一个,而跳过一个结点就相当于跳过整棵子树,快速跳转即可。设
时间复杂度都是
然后考虑第二个情况,我们不考虑乘积暴力合并是
说了这么多的求解,终于到优化了。由于上面的统计连通块包含根节点的情况复杂度会到达
复个小盘,点分治优化 DP 的关键就是在于减小子树规模,降低 DP 的复杂度。一般来说,点分治优化 DP 要求满足以下的条件:
- 问题是根独立的,即问题的答案不依赖选定的根。
- 问题可以通过合并子树的答案得到。
- 合并子树复杂度大,但是处理单个节点的贡献可以快速计算。
要对于每个连通块求一个什么东西,我们先考虑强制这个连通块经过根,可以发现在这种情况下,如果一个点在连通块中,那么它的父亲必须也在。所以我们先从根自上向下进行 DP,对于每个点进行选/不选的决策,如果不选那就新开一份 dp 去做;如果选那就操作维护父亲对 dp 的贡献,然后 DFS 儿子;最后把儿子的 DP 值的答案合并上来,我们就得到了这个点子树内的答案。当然最后要强制选重心。
以下直接 DFS 实现版本,若 DFN 可以参考 crashed 的题解。
#include<bits/stdc++.h>
#define int long long
using namespace std;
constexpr int MN=2e3+15,MM=1e6+15,INF=1e18,MOD=1e9+7;
int n,m,a[MN],w[MN],st[MM],ctot,ans,f[MN][MN];
vector<int> adj[MN];
namespace Tree{
int dep[MN],maxp[MN],siz[MN],dfn[MN],rt,sum;
bool vis[MN];
void dfs1(int u,int pre){ // getrt
siz[u]=1;
maxp[u]=0;
for(auto v:adj[u]){
if(v==pre||vis[v]) continue;
dfs1(v,u);
siz[u]+=siz[v];
maxp[u]=max(maxp[u],siz[v]);
}
maxp[u]=max(maxp[u],sum-siz[u]);
if(maxp[u]<maxp[rt]) rt=u;
}
void dfs2(int u,int pre){
for(int i=1;i<=ctot;i++) f[u][i]=0;
for(int i=1;i<=ctot;i++){
if(w[i]>=a[u]){
(f[u][st[w[i]/a[u]]]+=f[pre][i])%=MOD;
}
}
for(auto v:adj[u]){
if(vis[v]||v==pre) continue;
dfs2(v,u);
for(int i=1;i<=ctot;i++){
(f[u][i]+=f[v][i])%=MOD;
}
}
}
void calc(int u){
f[0][ctot]=1;
dfs2(u,0);
for(int i=1;i<=ctot;i++){
(ans+=f[u][i])%=MOD;
}
f[u][ctot]=0;
}
void solve(int u){
vis[u]=1;
calc(u);
for(auto v:adj[u]){
if(vis[v]) continue;
sum=siz[v];
maxp[rt=0]=INF;
dfs1(v,0);
solve(rt);
}
}
}using namespace Tree;
void init(){
rt=ans=0;
sum=0;
for(int i=1;i<=n;i++){
adj[i].clear();
vis[i]=0;
a[i]=0;
dfn[i]=maxp[i]=dep[i]=siz[i]=dfn[i]=0;
}
for(int i=1;i<=ctot;i++){
st[w[i]]=0;
w[i]=0;
}
ctot=0;
}
void prework(){
for(int i=m,ls=0;i>=1;i--){
int x=m/i;
w[st[x]=x!=ls?++ctot:ctot]=x;
ls=x;
}
for(int i=0;i<=n;i++){
for(int j=0;j<=ctot;j++){
f[i][j]=0;
}
}
}
void solve(){
cin>>n>>m;
init();
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
adj[u].push_back(v);
adj[v].push_back(u);
}
prework();
maxp[rt=0]=sum=n;
dfs1(1,0);
solve(rt);
cout<<ans<<'\n';
}
signed main(){
int T;
cin>>T;
while(T--){
solve();
}
return 0;
}
2. 例题
HDU 5909
有点眼熟啊,不是吗,只不过换成了异或。
但是我知道,这不位运算吗?我会 FWT!这个背包显然你一开始就说是卷积,我用脚都会,时间复杂度
但显然我不是要讲这个做法,考虑到这个玩意直接做背包很难,但是单独加入一个点的复杂度近乎
CodeChef SUBWAY
注意有中文题面,首先这是一颗有重边的树,可以看成每条边选择一个颜色,让相邻边不同的最多。
然后考虑分析性质,发现一个树有很多重边甚是卑鄙,考虑简化问题。我们发现如果一条边有三种颜色,那么这条边一定可以选出一种颜色使得于另外两边颜色都不相同,所以可以看成只有一种以前没有出现过的颜色。所以每条边我们至多保留两个颜色即可。
其次,我们求的是同色路径,如果我们直接自底向上进行合并的话会因为要求联通而不行,我们还是和上面一样的方法,枚举根
同时本题显然存在倍增 DP 做法,状态同上但是加入了倍增必须的
3.参考
求赞 QwQ。
- Querainy 的点分治优化 dp 学习笔记;
- HDU6643 题解 C202044zxy。