动态点分治学习笔记
学习动态点分治之前要先弄清楚点分治的原理,二者的应用范围的不同就在于动态的支持在线修改操作,而实现的不同就在于动态点分治要建点分树。
OI中有很多树上统计问题,这类问题往往都有一个比较容易实现的暴力做法,而用高级数据结构维护信息有显得过于复杂,有没有一种“优美的暴力”,能既保证思维的简单性,又有更高效的时间复杂度保证呢?这就是点分治的思想。
点分治的实现过程是:每次找到当前树的重心,然后以这个重心为根统计这个树的信息,然后对重心的每个孩子分别递归,同样用将重心作为根的方法统计子树的信息(这里可能要除去不合法的重复影响)。为什么复杂度有保证呢?我们先来看重心的性质。
以一棵树的重心作为根,则根的最大子树的size不超过整个树size的一半。考虑证明,因为重心的定义是使以它作为根的树的最大子树size最小,那么如果重心某一个子树size超过整个树的一半,则一定能找到另一个节点使这个节点作为根树的最大子树size比重心更小,矛盾。
那么我们可以得到,每个重心都有自己的管辖范围(事实上因为是分治,所以所有点都是某一块(或仅仅是它自己)的重心),而管辖一个点的重心最多有$O(\log n)$个。所以如果每个点被管辖自己的重心处理一次,那么总次数是$O(n \log n)$个的。
基于这个思想,我们得到了点分治的实现方法。
关于如何找重心,直接DP即可,注意传入S为这棵树的size,以及f[rt=0]=inf。
下面是一道裸题:POJ1741
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #define rep(i,l,r) for (int i=l; i<=r; i++) 5 #define For(i,x) for (int i=h[x],k; i; i=nxt[i]) 6 typedef long long ll; 7 using namespace std; 8 9 const int N=20100,inf=1000000000; 10 int ans,n,cnt,tot,S,k,u,v,w,rt; 11 int sz[N],vis[N],d[N],val[N],h[N],nxt[N],to[N],a[N],f[N]; 12 13 void add(int u,int v,int w) 14 { to[++cnt]=v; val[cnt]=w; nxt[cnt]=h[u]; h[u]=cnt; } 15 16 void find(int x,int fa){ 17 sz[x]=1; f[x]=0; 18 For(i,x) if ((k=to[i])!=fa && !vis[k]){ 19 find(k,x); sz[x]+=sz[k]; f[x]=max(f[x],sz[k]); 20 } 21 f[x]=max(f[x],S-sz[x]); 22 if (f[x]<f[rt]) rt=x; 23 } 24 25 void deep(int x,int fa){ 26 a[++tot]=d[x]; 27 For(i,x) if ((k=to[i])!=fa && !vis[k]) d[k]=d[x]+val[i],deep(k,x); 28 } 29 30 int cal(int x,int v){ 31 d[x]=v; tot=0; deep(x,0); 32 sort(a+1,a+tot+1); 33 int l=1,r=tot,sum=0; 34 while (l<r) 35 if (a[l]+a[r]>k) r--; else sum+=r-l,l++; 36 return sum; 37 } 38 39 void solve(int x){ 40 ans+=cal(x,0); vis[x]=1; 41 For(i,x) if (!vis[k=to[i]]) 42 ans-=cal(k,val[i]),S=sz[k],f[rt=0]=inf,find(k,x),solve(rt); 43 } 44 45 int main(){ 46 while (scanf("%d%d",&n,&k),n+k){ 47 ans=cnt=0; memset(vis,0,sizeof(vis)); memset(h,0,sizeof(h)); 48 rep(i,1,n-1) scanf("%d%d%d",&u,&v,&w),add(u,v,w),add(v,u,w); 49 S=n; f[rt=0]=inf; solve(1); printf("%d\n",ans); 50 } 51 return 0; 52 }
接着我们来看动态点分治。首先介绍点分树的概念,对于一个重心,将它与所有子树的重心连边(也就是按照分治的根的顺序连边),就得到了点分树。我们可以发现,每个重心记录的是自己管辖范围的所有点的信息,实际上也就是点分树上以这个重心为根的子树的信息。而如果修改某个点的值,它影响到的也就是点分树上这个点到根的路径上的所有点的信息。
根据上面对于点分治复杂度的分析可知,点分树的层数是$O(\log n)$层的。这就保证了修改的复杂度。
“树上的动态点分治相当于序列上的线段树"
我们再看一道模板题:BZOJ1095
这题如果不带修改就是简单的点分治或者直接DP的题目,现在带了修改,显然就是需要建立点分树。
建立点分树有一个需要注意的地方,一定要分清点在点分树上的父亲和在原树上的父亲。有的时候我们需要把点分树给建出来,有时则不需要。
还有动态点分治经常要用到两点间LCA,这个不要用树剖或者倍增LCA,因为单次询问是$O(\log n)$的。求出dfs序的深度序列,然后用RMQ求区间最小值就可以使单次询问复杂度降到$O(1)$。
还有,一般动态点分治都会与数据结构(STL)结合,一般每个节点用两个数据结构需要记录两个信息:以它为根的子树对它的父亲的影响,和所有以它的儿子为根的子树对它的影响,显然后者可以直接使用前者的信息(这里的父亲儿子都是指在点分树上)。
具体到这一题上,我们将找重心需要的所有参数全部传到find函数内部去而不是作为全局变量以免递归时出现冲突。
另外,下面这份代码在BZOJ上TLE了,因为multiset的常数过大。较为高效的实现是使用priority_queue,通过建立一个“垃圾堆”实现删除功能(具体实现见hzwer博客)
1 #include<set> 2 #include<cstdio> 3 #include<algorithm> 4 #pragma GCC optimize(3) 5 #define rep(i,l,r) for (register int i=l; i<=r; i++) 6 #define For(i,x) for (register int i=h[x],k; i; i=nxt[i]) 7 using namespace std; 8 9 const int N=200100,inf=1000000000; 10 char s[10]; 11 int n,m,u,v,x,cnt,tot,pos[N],mv[N],lg[N<<1],a[N<<2],b[N],to[N<<1],nxt[N<<1],fa[N],h[N],sz[N],f[N],d[N],st[N][20],vis[N]; 12 multiset<int>A[N],B[N],C; 13 multiset<int>::iterator it; 14 15 void add(int u,int v){ to[++cnt]=v; nxt[cnt]=h[u]; h[u]=cnt; } 16 void ins(multiset<int>a){ if (a.size()>=2) it=--a.end(),C.insert(*it+*(--it)); } 17 void del(multiset<int>a){ if (a.size()>=2) it=--a.end(),C.erase(C.find(*it+*(--it))); } 18 19 void find(int x,int fa,int S,int &rt){ 20 sz[x]=1; f[x]=0; 21 For(i,x) if ((k=to[i])!=fa && !vis[k]) 22 find(k,x,S,rt),sz[x]+=sz[k],f[x]=max(f[x],sz[k]); 23 f[x]=max(f[x],S-sz[x]); 24 if (f[x]<=f[rt]) rt=x; 25 } 26 27 void dfs(int x,int fa){ 28 pos[x]=++tot; a[tot]=d[x]; 29 For(i,x) if (fa!=(k=to[i])) d[k]=d[x]+1,dfs(k,x),a[++tot]=d[x]; 30 } 31 32 void getst(){ 33 rep(i,1,tot) st[i][0]=a[i]; 34 for (int j=1; j<=18; j++) 35 rep(i,1,tot) st[i][j]=min(st[i][j-1],st[i+(1<<(j-1))][j-1]); 36 } 37 38 int que(int l,int r){ 39 int t=lg[r-l+1]; 40 return min(st[l][t],st[r-(1<<t)+1][t]); 41 } 42 43 int dis(int x,int y){ int a=pos[x],b=pos[y]; if (a>b) swap(a,b); return d[x]+d[y]-2*que(a,b); } 44 45 void get(int x,int fa,int dep,multiset<int>&s){ 46 s.insert(dep); 47 For(i,x) if (!vis[k=to[i]] && k!=fa) get(k,x,dep+1,s); 48 } 49 50 int work(int x){ 51 int rt=0; f[0]=inf; find(x,0,sz[x],rt); vis[rt]=1; 52 B[rt].insert(0); 53 For(i,rt) if (!vis[k=to[i]]){ 54 multiset<int> s; get(k,0,1,s); 55 int p=work(k); fa[p]=rt; A[p]=s; 56 B[rt].insert(*(--A[p].end())); 57 } 58 ins(B[rt]); return rt; 59 } 60 61 void mdf(int x,bool f){ 62 del(B[x]); if (f) B[x].erase(B[x].find(0)); else B[x].insert(0); ins(B[x]); 63 for (int i=x; fa[i]; i=fa[i]){ 64 int y=fa[i]; del(B[y]); 65 if (A[i].size()) B[y].erase(B[y].find(*(--A[i].end()))); 66 if (f) A[i].erase(A[i].find(dis(y,x))); else A[i].insert(dis(y,x)); 67 if (A[i].size()) B[y].insert(*(--A[i].end())); 68 ins(B[y]); 69 } 70 } 71 72 int main(){ 73 freopen("bzoj1095.in","r",stdin); 74 freopen("bzoj1095.out","w",stdout); 75 scanf("%d",&n); 76 rep(i,1,n-1) scanf("%d%d",&u,&v),add(u,v),add(v,u); 77 lg[1]=0; rep(i,2,N+100) lg[i]=lg[i>>1]+1; 78 dfs(1,0); getst(); tot=n; work(1); scanf("%d",&m); 79 rep(i,1,m){ 80 scanf("%s",s); 81 if (s[0]=='G') if (tot<=1) printf("%d\n",tot-1); else printf("%d\n",*(--C.end())); 82 else scanf("%d",&x),tot+=(b[x])?1:-1,b[x]^=1,mdf(x,b[x]); 83 } 84 return 0; 85 }