圓方樹初探

圓方樹

ref1 ref2

圓方樹是處理帶環圖的利器,它可以把原圖轉化成一個樹的形態,所以很多樹的性質都可以在其上加以利用。

圓方樹實際上有兩種。一種是仙人掌上圓方樹,另一種是廣義圓方樹

蒯圖預警:接下來引用的圖片全部來自網絡,除了csacademy的圖,沒有一張不是蒯的(不蒯圖會死.jpg)。感謝各位被動提供照片的神仙。

仙人掌上圓方樹

首先定義仙人掌:任意一條邊只會出現在一個環裏面的無向連通圖。Like this:

a

圓點就是原圖上的點。

在一個仙人掌上,圓方樹的構造方法是:

  • 如果一條邊在仙人掌中不屬於任何一個環,那麼就直接將圓方樹上對應兩圓點相連。
  • 而對於每一個點雙連通分量(也就是環),我們都構建出一個方點,將環上的點都向方點連一條邊。這樣每一個方點對應原圖中的一個環。

Like this:

<img src="https://i.loli.net/2019/12/20/XEiJIlasgpTLrQK.png" alt="a" style="zoom: 67%;" />

可以證明這樣構造出來的新圖一定是一棵樹(引用自WC2017 immortalCO課件):

<img src="https://images.cnblogs.com/cnblogs_com/fruitea/1617073/o_1912191349362019-12-19%2021-48-00%20%E7%9A%84%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE.png" alt="a" style="zoom:67%;" />

仙人掌上圓方樹的構造

直接套用Tarjan找點雙的方法實現。在這裏,兩個點一條邊的情況並不看作點雙,特殊考慮一下子。

inline void Tarjan(int nw,int pa=0){
    dfn[nw]=low[nw]=++dfc;stk[++tp]=nw;
    for(int to:r_E[nw])
	if(to^pa){
	    if(!dfn[to]){
		Tarjan(to,nw);
		low[nw]=min(low[nw],low[to]);
		if(dfn[nw]<=low[to]){
		    if(stk[tp]==to)E[nw].pb(to),E[to].pb(nw),--tp;//only two vertices
		    else{
			++cnt;
			for(int x=0;x^to;--tp){
			    x=stk[tp];
			    E[cnt].pb(x),E[x].pb(cnt);
			}
			E[cnt].pb(nw),E[nw].pb(cnt);
		    }
		}
	    }
	    else low[nw]=min(low[nw],dfn[to]);
	}
}

一些性質

  • 方點不和方點直接連接。

  • 圓方樹是無根樹,不管取哪個點爲根,構造出來的圓方樹形態一樣。

  • 首先定義:以 r 爲根的仙人掌上的點 p 的子仙人掌是從仙人掌中去掉 p 到 r 的簡單 路徑上的所有邊之後,p 所在的連通塊。

    那麼:以 r 爲根的仙人掌中點 p 的子仙人掌就是圓方樹以 r 爲根時點 p 的 子樹中的所有圓點。

廣義圓方樹

廣義圓方樹與仙人掌圓方樹不同之處在於,認爲兩個點一條邊的情況也是點雙。

廣義圓方樹構造 Like this:

a

我們可以發現:現在圓方樹上圓點只和方點相連,方點只和圓點相連

值得注意的是:因爲一般圖不像仙人掌(一條邊只在至多一個簡單環中),所以它有可能是這樣

a

而他的點雙只有這個(模擬一下Tarjan的過程就可以知道)

a

所以構造出的圓方樹長這樣:

a

可以看到,原圖中的(1,3)的邊在圓方樹中已經消失不見。所以除了像仙人掌那種一條邊在至多一個環內的圖外,一般圖構造出的圓方樹邊的信息會有丟失。

廣義圓方樹的構造

一句話的區別。將兩個點一條邊的情況也看作點雙。

inline void Tarjan(int nw,int pa=0){
    dfn[nw]=low[nw]=++dfc;stk[++tp]=nw;
    for(int to:r_E[nw])
	if(to^pa){
	    if(!dfn[to]){
		Tarjan(to,nw);
		low[nw]=min(low[nw],low[to]);
		if(dfn[nw]<=low[to]){
		    ++cnt;
		    for(int x=0;x^to;--tp){
			x=stk[tp];
			E[cnt].pb(x),E[x].pb(cnt);
		    }
		    E[cnt].pb(nw),E[nw].pb(cnt);
		}
	    }
	    else low[nw]=min(low[nw],dfn[to]);
	}
}

例題

「APIO2018」鐵人兩項

Link

Solution

固定$s$和$f$,那麼合法的$c$的數量就是$s$,$f$之間簡單路徑的點集的並集減2(減掉$s$,$f$本身)。

手玩一下可以發現一個結論:兩圓點在圓方樹上的路徑的圓點點集,加上與路徑上的方點相鄰的圓點點集,就等於原圖中兩點所有簡單路徑的點集。

圓方樹有一個技巧:路徑統計時,給點附上恰當的權值。

例如這道題,給方點附上其對應點雙大小,給圓點附上-1。

那麼兩圓點間路徑的點權和就是圓點個數。因爲方點貢獻就是他的點雙大小,而每個割點被重複統計多次,減去就好。

直接算還是不好算,可以考慮每個點對答案貢獻:就是經過他的路徑個數。樹形dp即可。

Code

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline int read(){//be careful for long long!
    register int x=0,f=1;register char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')f=0;ch=getchar();}
    while(isdigit(ch)){x=x*10+(ch^'0');ch=getchar();}
    return f?x:-x;
}

const int N=1e5+10;
int n,m,stk[N],tp,dfn[N],low[N],dfc,cnt,val[N<<1],siz[N<<1],num;
vector<int> G[N],E[N<<1];
ll ans;

inline void Tarjan(int nw){
    dfn[nw]=low[nw]=++dfc;stk[++tp]=nw;++num;
    for(int to:G[nw]){
	if(!dfn[to]){
	    Tarjan(to);
	    low[nw]=min(low[nw],low[to]);
	    if(dfn[nw]==low[to]){
		++cnt;
		for(int x=0;x!=to;--tp){
		    x=stk[tp];++val[cnt];
		    E[cnt].push_back(x),E[x].push_back(cnt);
		}
		++val[cnt];
		E[cnt].push_back(nw),E[nw].push_back(cnt);
	    }
	}
	else low[nw]=min(low[nw],dfn[to]);
    }
}
inline void Dfs(int nw,int fa=0){
    siz[nw]=(nw<=n);
    for(int to:E[nw])
	if(to^fa){
	    Dfs(to,nw);
	    ans+=2ll*val[nw]*siz[nw]*siz[to];
	    siz[nw]+=siz[to];
	}
    ans+=2ll*val[nw]*siz[nw]*(num-siz[nw]);
}

int main(){
    n=read(),m=read();for(int i=1;i<=n;++i)val[i]=-1;
    for(int i=1;i<=m;++i){
	int u=read(),v=read();
	G[u].push_back(v),G[v].push_back(u);
    }
    cnt=n;
    for(int i=1;i<n;++i)
	if(!dfn[i]){
	    tp=num=0,Tarjan(i);
	    Dfs(i);
	}
    printf("%lld\n",ans);
    return 0;
}

CF1276B Two Fairs

Link

Solution

把圓方樹建出來,然後就顯然了。直接將a,b兩側的子樹內節點數相乘即可。

考慮兩種情況:

  • a,b沒有祖孫關係。這一種ans就是$(siz[a]-1)\times (siz[b]-1)$。
  • a,b有祖孫關係。不失一般性地假設a是b的祖先。這是a一側的子樹不再是$siz[a]-1$,應該是a子樹以外的部分。設b往上走一直走到a兒子處的方點爲s,ans就是$(n-siz[s]-1)*(siz[b]-1)$。注意是a往下的第一個方點而不是圓點,這是我一開始沒考慮清楚的地方。方點代表的點雙上,不是a的點,都可以不經過a就到達b側子樹內。他們是不應計算進答案的。

tips: $siz[]$指的是子樹內圓點個數。

Code

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline int read(){//be careful for long long!
    register int x=0,f=1;register char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')f=0;ch=getchar();}
    while(isdigit(ch)){x=x*10+(ch^'0');ch=getchar();}
    return f?x:-x;
}

const int N=2e5+10,M=5e5+10;
int n,m,a,b,stk[N],tp,cnt,dfn[N],low[N],dfc,siz[N<<1],vis[N<<1],fa[N<<1];
vector<int> r_E[N],E[N<<1];
#define pb(x) push_back(x)

inline void Tarjan(int nw,int pa=0){
    dfn[nw]=low[nw]=++dfc;stk[++tp]=nw;
    for(int to:r_E[nw])
	if(to^pa){
	    if(!dfn[to]){
		Tarjan(to);
		low[nw]=min(low[nw],low[to]);
		if(dfn[nw]<=low[to]){
		    ++cnt;
		    for(int x=0;x^to;--tp){
			x=stk[tp];
			E[cnt].pb(x),E[x].pb(cnt);
		    }
		    E[cnt].pb(nw),E[nw].pb(cnt);
		}
	    }
	    else low[nw]=min(low[nw],dfn[to]);
	}
}

inline void Dfs(int nw,int pa,int type){
    vis[nw]=type;siz[nw]=(nw<=n);fa[nw]=pa;
    if(nw==a)type=1;else if(nw==b)type=2;
    for(int to:E[nw])
	if(to^pa){
	    Dfs(to,nw,type);
	    siz[nw]+=siz[to];
	}
}

int main(){
    int T=read();
    for(int t=1;t<=T;++t){
	cnt=n=read(),m=read();a=read(),b=read();
	for(int i=1;i<=n;++i)r_E[i].clear();
	for(int i=1,tim=(n<<1);i<=tim;++i)E[i].clear();
	for(int i=1;i<=m;++i){
	    int u=read(),v=read();
	    r_E[u].pb(v),r_E[v].pb(u);
	}
	memset(dfn,0,sizeof(int)*(n+1));
	dfc=tp=0;Tarjan(1);
	Dfs(1,0,0);
	if(vis[a]==2){
	    int s=a;while(fa[s]!=b)s=fa[s];
	    printf("%lld\n",1ll*(siz[a]-1)*(n-siz[s]-1));
	}
	else if(vis[b]==1){
	    int s=b;while(fa[s]!=a)s=fa[s];
	    printf("%lld\n",1ll*(siz[b]-1)*(n-siz[s]-1));
	}
	else printf("%lld\n",1ll*(siz[a]-1)*(siz[b]-1));
    }
    return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章