1. 程式人生 > >動態點分治學習筆記

動態點分治學習筆記

記錄 刪除 n) 最大 tis 序列 不同 sum 所有

學習動態點分治之前要先弄清楚點分治的原理,二者的應用範圍的不同就在於動態的支持在線修改操作,而實現的不同就在於動態點分治要建點分樹。

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 }

動態點分治學習筆記