在聯勝過後我又開始更博客啦。php
首先在學習圓方樹以前,要先有一些圖論的基礎,要先學會$tarjan$求點雙。學習
首先先定義一下什麼是仙人掌圖。仙人掌圖知足兩個性質:性質一,仙人掌圖是一個無向連通圖。性質二,仙人掌圖中的任意一條邊最多存在於一個環中。借用$bzoj$的圖來解釋會清晰一點。ui
咱們考慮仙人掌圖具備什麼性質,將仙人掌圖的$dfs$樹取出,咱們能夠獲得一課掛滿非樹邊的樹,這些非樹邊必定不相交。對於這個性質,咱們能夠解決一類問題,例如仙人掌的最大獨立集問題。例題:bzoj4316: 小c的獨立集spa
咱們考慮上述的性質,即仙人掌中任意兩條非樹邊不相交。由於上述性質咱們能夠知道,在求最大獨立集的時候,任意兩個環能夠作到互不影響。因此咱們能夠先考慮不在環上的點,在將一個環上全部的點所引出的子樹都考慮結束的時候,考慮整個環。咱們設狀態$f[i][0/1]$,表示對於$i$號點的子樹中,第$i$號點選擇/不選擇的最大獨立集大小是多少。如今咱們考慮轉移,對於一條樹邊,咱們直接按照求樹的最大獨立集那樣轉移就能夠。若不是樹邊,咱們先不轉移。因爲每個點是在處理完全部的子樹內的點以後再被處理,因此咱們發現,若如今要處理的是一個環的頂點(對於一個環的頂點的定義爲這個環中最早遍歷到的點),這個環上其餘的全部點除了由當前環轉移的部分,都已經處理完畢了,這個時候咱們處理整個環。咱們從整個環的最低端往上跳,咱們只須要枚舉當前環的最高點選擇或不選擇就行了,若環的最上方的點選擇,整個環的最下方的點就不能選擇,若環的最上方的點不選擇,則環的最下方的點選擇或不選擇就是隨意的。這樣作的話,就至關於每一次將環單獨拿出來考慮,其他的就變成樹上的動態規劃了。3d
這份代碼咕掉啦
仙人掌的另外一類題目,求直徑,例題:bzoj1023:仙人掌圖code
咱們考慮樹的直徑怎麼求,咱們仍是將樹邊和環分開考慮,對於樹邊,咱們直接像轉移樹的直徑那樣轉移就能夠了,對於環,咱們考慮怎麼更新$f[i]$($f[i]$表示以$i$爲端點,向$i$的子樹中延伸的最長鏈)和答案,$f[i]$彷佛很好考慮,$f[i]=Max\{f[j]+dis(i,j)]\}$。咱們直接維護一下環上全部點的最大值就行了(對於$dis(i,j)$,咱們只須要遍歷一遍環就能夠了)。考慮答案$ans=Max\{f[i]+f[j]+dis(i,j)\}$,對於上邊的東西咱們能夠將$dis(i,j)$拆成$min(abs(dep[i]-dep[j]),cir_sze-abs(dep[i]-dep[j]))$,因此咱們發現咱們能夠維護一個單調隊列來更新答案。可能有人想問,這個不是單調棧就能維護的嗎?咱們考慮直徑的定義,兩點間的最短路徑最長,因此咱們要考慮在環上兩個點之間要取最小距離,因此咱們就要判斷隊首的點和當前的點的距離是否已經超過半個環的長度,若超過是要彈掉隊首的。blog
#include <cstdio> #include <algorithm> using namespace std; #define N 100010 int f[N],fa[N],ans=1,dep[N],low[N],cnt,level[N]; int head[N],nxt[N<<3],to[N<<3],idx,Q[N<<2],Q1[N<<2],H,T,n,m; void add(int a,int b) {nxt[++idx]=head[a],to[idx]=b,head[a]=idx;} void dp(int p,int y) { int t=0; for(int i=y;i!=p;i=fa[i]) Q1[++t]=i; Q1[++t]=p; reverse(&Q1[1],&Q[t+1]); for(int i=1;i<=t;i++) Q1[i+t]=Q1[i]; H=1,T=0; for(int i=1;i<=t+t;i++) { while(H<=T&&i-Q[H]>t/2) ++H; if(H<=T) ans=max(ans,f[Q1[i]]+f[Q1[Q[H]]]+i-Q[H]); while(H<=T&&f[Q1[i]]-i>f[Q1[Q[T]]]-Q[T]) --T; Q[++T]=i; } for(int i=y;i!=p;i=fa[i]) f[p]=max(f[p],f[i]+min(level[i]-level[p],1+level[y]-level[i])); } void dfs(int p,int from) { fa[p]=from,dep[p]=low[p]=++cnt,level[p]=level[from]+1; for(int i=head[p];i;i=nxt[i]) { if(!dep[to[i]]) dfs(to[i],p),low[p]=min(low[p],low[to[i]]); else if(to[i]!=from) low[p]=min(low[p],dep[to[i]]); if(low[to[i]]>dep[p]) ans=max(ans,f[p]+f[to[i]]+1),f[p]=max(f[p],f[to[i]]+1); } for(int i=head[p];i;i=nxt[i]) if(fa[to[i]]!=p&&dep[p]<dep[to[i]]) dp(p,to[i]); } int main() { scanf("%d%d",&n,&m); for(int i=1,k,a,b;i<=m;i++) { scanf("%d%d",&k,&a); for(int j=1;j<k;j++) scanf("%d",&b),add(a,b),add(b,a),a=b; } dfs(1,0),printf("%d\n",ans); }
可能有人要問,這個題目不是圓方樹淺談嗎?爲何要先講仙人掌。由於圓方樹能夠求解一些仙人掌的題目。首先咱們先引用一下$WC2017$的課件:隊列
仙人掌$ G=(V,E)$的圓方樹$ T=(VT,ET)$爲知足如下條件的無向圖:$VT=RT∪ST,RT=V,RT∩ST=∅$,咱們稱$RT $集合爲圓點、$ST$集合爲方點$∀e∈E$,若$ e$不在任何簡單環中,則$ e∈ET$,對於每一個仙人掌中的簡單環$ R$,存在方點$pR∈ST$,而且$ ∀p∈R$知足$ (pR,p)∈ET$,即對每一個環建方點連全部點. ci
看完上邊的講解,我再講講我對於圓方樹的理解。對於一棵仙人掌的圓方樹來講,上面一共有兩種點,一類是圓點,另外一類是方點。原仙人掌中全部的點都映射爲圓方樹中的圓點,圓方樹中方點是咱們後加進去的,表示爲仙人掌中的一個環。對於圓方樹的連邊規則:對於原仙人掌中的一條邊,若這條邊不屬於任何一個環中,這條邊直接將原仙人掌中它的兩個端點所映射的相連,若這條邊屬於環上,咱們不會將這條邊體如今圓方樹中,咱們把原仙人掌的一個環上的全部點都連向這個環所映射的方點。如圖,咱們將一個仙人掌轉化爲圓方樹的效果圖:get
知道了仙人掌的結構,怎麼構建彷佛就很好辦了,咱們首先用$tarjan$縮點,把每個大小超過$1$的環的點都和一個方點相連,對於原來的樹邊,判斷鏈接的兩個點是否是在一個環中,若不是咱們就鏈接在一塊兒。
圓方樹擁有幾個小性質:
性質一:方點和方點不會直接相連,考慮定義,十分顯然。
性質二:不管哪個點爲根,圓方樹的形態不會改變,即圓方樹是無根樹。
性質三:以$r$爲根的仙人掌上$p$的子仙人掌就是圓方樹中以$r$爲根時,$p$子樹中的全部圓點。(子仙人掌:以$r$爲根的仙人掌上的點$p$的子仙人掌是去除掉$p$到$r$的全部簡單路徑後,$p$所在的連通塊)。
例題:bzoj2125:最短路
咱們考慮先建出此仙人掌的圓方樹,咱們考慮對於圓方樹上的邊的邊權,若當前邊鏈接的是兩個圓點,則當前邊就是原仙人掌中的邊權,若當前點鏈接的是圓點和方點,咱們將其邊權定義爲圓點到當前環中深度最小的點的最短路徑長度,這樣咱們維護兩個點的最短距離就能夠直接在圓方樹上求$Lca$,而後求路徑和就行了。可是這樣的求法有必定的問題,咱們考慮當$Lca$是圓點的時候這個求法沒有問題,可是當$Lca$是方點的時候,咱們就至關於都走到了方點所表明的環的最淺的點,可是並不必定就要都走到那裏,因此咱們要用倍增,求到$Lca$的下方的兩個點,在計算答案的時候,就是路徑和加上兩點之間在環上的最短距離。具體看代碼就行了:
#include <cstdio> #include <vector> #include <algorithm> using namespace std; #define N 100010 int head[N],to[N<<1],nxt[N<<1],val[N<<1],idx,fa[20][N],many,n,m,q,tot; int head2[N],to2[N<<1],nxt2[N<<1],idx2,dep[N],low[N],sta[N],top,cnt,level[N],fa2[N]; vector <int> have[N]; bool in[N],vis[N]; long long val2[N<<1],len[N],up_len[20][N],dis[N]; void add(int a,int b,int c) {nxt[++idx]=head[a],to[idx]=b,val[idx]=c,head[a]=idx;} void add2(int a,int b,long long c) {nxt2[++idx2]=head2[a],to2[idx2]=b,val2[idx2]=c,head2[a]=idx2;} void dfs3(int p,int from) { ++many; for(int i=from;i!=p;i=fa2[i]) have[many].push_back(i); have[many].push_back(p); } void tarjan(int p,int from) { fa2[p]=from,dep[p]=low[p]=++cnt,sta[++top]=p,vis[p]=in[p]=true; for(int i=head[p];i;i=nxt[i]) if(to[i]!=from) { if(!vis[to[i]]) tarjan(to[i],p),low[p]=min(low[p],low[to[i]]); else low[p]=min(low[p],dep[to[i]]); if(dep[p]<low[to[i]]) add2(p,to[i],val[i]),add2(to[i],p,val[i]); } for(int i=head[p];i;i=nxt[i]) if(to[i]!=from&&fa2[to[i]]!=p&&dep[to[i]]>dep[p]) dfs3(p,to[i]); } void dfs(int p,int b) { vis[p]=true,add2(p,tot,min(dis[p],len[b]-dis[p])),add2(tot,p,min(dis[p],len[b]-dis[p])); for(int i=head[p];i;i=nxt[i]) if(!vis[to[i]]) dis[to[i]]=dis[p]+val[i],dfs(to[i],b); } void dfs_(int p,int b,int x,int from) { vis[p]=true; for(int i=head[p];i;i=nxt[i]) { if((!vis[to[i]])||(to[i]!=from&&to[i]==x)) len[b]+=val[i]; if(!vis[to[i]]) dfs_(to[i],b,x,p); } } void dfs2(int p,int from) { fa[0][p]=from,level[p]=level[from]+1; for(int i=head2[p];i;i=nxt2[i]) if(to2[i]!=from) up_len[0][to2[i]]=val2[i],dfs2(to2[i],p); } long long find(int a,int b) { long long ans=0; if(level[a]>level[b]) swap(a,b); for(int i=19;~i;i--) if(level[b]-level[a]>=(1<<i)) ans+=up_len[i][b],b=fa[i][b]; if(a==b) return ans; for(int i=19;~i;i--) if(fa[i][a]!=fa[i][b]) ans+=up_len[i][a]+up_len[i][b],a=fa[i][a],b=fa[i][b]; return fa[0][a]<=n?ans+up_len[0][a]+up_len[0][b]: ans+min(abs(dis[a]-dis[b]),len[fa[0][a]]-abs(dis[a]-dis[b])); } int main() { scanf("%d%d%d",&n,&m,&q),tot=n; for(int i=1,a,b,c;i<=m;i++) scanf("%d%d%d",&a,&b,&c),add(a,b,c),add(b,a,c); tarjan(1,0); for(int i=1,rt;i<=many;i++) if(have[i].size()>1) { ++tot,rt=0; for(int j=0;j<(int)have[i].size();j++) vis[have[i][j]]=0,rt=(!rt)||(dep[have[i][j]]<dep[rt])?have[i][j]:rt; dfs_(rt,tot,rt,0); for(int j=0;j<(int)have[i].size();j++) vis[have[i][j]]=0; dfs(rt,tot); } dfs2(1,0); for(int i=1;i<=19;i++) for(int j=1;j<=tot;j++) fa[i][j]=fa[i-1][fa[i-1][j]],up_len[i][j]=up_len[i-1][j]+up_len[i-1][fa[i-1][j]]; for(int i=1,a,b;i<=q;i++) scanf("%d%d",&a,&b),printf("%lld\n",find(a,b)); }
圓方樹不必定就只能求仙人掌上的問題,咱們實際上能夠將其擴展。一個無向連通圖的圓方樹上的圓點表示的是原圖上的點,方點表示的是原圖上的每個點雙,連邊規則不變。可是考慮兩個相鄰的點也能構成一個點雙,因此廣義圓方樹上不存在圓點和圓點連邊的狀況。找到一張圖,輔助理解一下。
建圖彷佛仍是像仙人掌的那樣,咱們用$tarjan $求每個點雙,並連邊。給出一道例題:Codeforces487E:Tourists
對於這道題目,咱們先建出這個無向連通圖的圓方樹。咱們考慮一個性質,就是一個點雙中,不論從哪一個點進入點雙,從哪一個點出點雙,咱們都必定能找到一條簡單路徑通過整個點雙的最小點。對於這個性質,咱們能夠定義方點的點權就爲他所表明的點雙中全部的點的最小點權,由於方點表明的點雙中的全部點所映射的圓點都會和方點連邊,因此方點的權值就是他所鏈接的全部圓點的權值最小值,固然圓點的權值就是原來點的權值。咱們考慮查詢,實際上就是查詢樹上路徑點權最小值,直接用樹剖加線段樹就能實現。考慮修改,對於咱們上述定義的方點點權來看,咱們每一次修改一個圓點的權值,就要跟着修改和當前圓點鏈接的全部方點的權值,時間複雜度顯然能夠卡爲$O(n^2)$,因此咱們考慮從新定義方點的權值。因爲圓方樹是一棵無根樹,因此咱們知道每個點在固定根的狀況下只會有一個父親,這樣咱們將方點的定義更改成,方點的全部兒子的權值最小值。這樣咱們修改就能作到一個$log$的了,由於咱們更改就只會更改當前圓點,和其父親。對於查詢,當路徑上的方點不是$Lca$的時候,顯然方點的父親圓點也被計算到了路徑當中,因此不影響答案,可是當方點爲$Lca$的時候,咱們就要在求完路徑最小值的時候,將方點的父親圓點算進去。
#include <set> #include <cstdio> #include <vector> #include <algorithm> using namespace std; #define N 200010 #define inf 1000000000 vector <int> bel[N]; multiset <int> s[N]; multiset <int> ::iterator it; int head[N],to[N<<1],nxt[N<<1],idx,head2[N],to2[N<<1],nxt2[N<<1],idx2,n,m,q,ans; char kind[10]; int mn[N<<2],ord[N],level[N],fa[N],top[N],size[N],son[N],num[N],low[N],dep[N],sta[N],Top,tot,cnt; bool vis[N]; void add(int a,int b) {nxt[++idx]=head[a],to[idx]=b,head[a]=idx;} void add2(int a,int b) {nxt2[++idx2]=head2[a],to2[idx2]=b,head2[a]=idx2;} void tarjan(int p,int from) { low[p]=dep[p]=++cnt,vis[p]=true; for(int i=head[p];i;i=nxt[i]) if(to[i]!=from) { if(!vis[to[i]]) { sta[++Top]=to[i],tarjan(to[i],p),low[p]=min(low[p],low[to[i]]); if(low[to[i]]>=dep[p]) { ++tot; while(sta[Top+1]!=to[i]) bel[sta[Top]].push_back(tot),Top--; bel[p].push_back(tot); } } else low[p]=min(low[p],dep[to[i]]); } } void rebuild() {for(int i=1;i<=n;i++) for(int j=0;j<(int)bel[i].size();j++) add2(i,bel[i][j]),add2(bel[i][j],i);} void dfs(int p,int from) { size[p]=1,fa[p]=from,level[p]=level[from]+1; for(int i=head2[p];i;i=nxt2[i]) if(to2[i]!=from) dfs(to2[i],p),size[p]+=size[to2[i]],son[p]=size[son[p]]<size[to2[i]]?to2[i]:son[p]; } void dfs2(int p,int from) { top[p]=from,ord[p]=++cnt; if(son[p]) dfs2(son[p],from); for(int i=head2[p];i;i=nxt2[i]) if(to2[i]!=son[p]&&to2[i]!=fa[p]) dfs2(to2[i],to2[i]); } void change(int p,int l,int r,int x,int y) { if(l==r) return mn[p]=y,void(); int mid=(l+r)>>1; if(x<=mid) change(p<<1,l,mid,x,y); else change(p<<1|1,mid+1,r,x,y); mn[p]=min(mn[p<<1],mn[p<<1|1]); } int find(int p,int l,int r,int x,int y) { if(x<=l&&r<=y) return mn[p]; int mid=(l+r)>>1,tmp=inf; if(x<=mid) tmp=min(tmp,find(p<<1,l,mid,x,y)); if(y>mid) tmp=min(tmp,find(p<<1|1,mid+1,r,x,y)); return tmp; } void init() { for(int i=1;i<=tot<<2;i++) mn[i]=inf; for(int i=n+1;i<=tot;i++) for(int j=head2[i];j;j=nxt2[j]) if(level[i]<level[to2[j]]) s[i-n].insert(num[to2[j]]); for(int i=1;i<=n;i++) change(1,1,tot,ord[i],num[i]); for(int i=n+1;i<=tot;i++) change(1,1,tot,ord[i],*s[i-n].begin()); } int main() { scanf("%d%d%d",&n,&m,&q),tot=n; for(int i=1;i<=n;i++) scanf("%d",&num[i]); for(int i=1,a,b;i<=m;i++) scanf("%d%d",&a,&b),add(a,b),add(b,a); tarjan(1,0),rebuild(),cnt=0,dfs(1,0),dfs2(1,1),init(); for(int i=1,a,b;i<=q;i++) { scanf("%s%d%d",kind,&a,&b); if(kind[0]=='C') { change(1,1,tot,ord[a],b); if(!fa[a]) {num[a]=b;continue;} it=s[fa[a]-n].lower_bound(num[a]),s[fa[a]-n].erase(it),num[a]=b; s[fa[a]-n].insert(b),change(1,1,tot,ord[fa[a]],*s[fa[a]-n].begin()); } else { ans=inf; while(top[a]!=top[b]) { if(level[top[a]]>level[top[b]]) swap(a,b); ans=min(ans,find(1,1,tot,ord[top[b]],ord[b])),b=fa[top[b]]; } if(level[a]>level[b]) swap(a,b); ans=min(ans,find(1,1,tot,ord[a],ord[b])); if(a>n) ans=min(ans,num[fa[a]]); printf("%d\n",ans); } } }