圖論雜項細節梳理&模板(虛樹,圓方樹,仙人掌,歐拉路徑,還有。。。)

orzYCB

虛樹

%自爲風月馬前卒巨佬%
用於優化一類樹形DP問題。
當狀態轉移只和樹中的某些關鍵點有關的時候,我們把這些點和它們兩兩之間的LCA弄出來,以點的祖孫關係連成一棵新的樹,這就是虛樹。
容易證明,如果關鍵點數量爲\(m\),則虛樹點數不超過\(2m\)

虛樹的構建

dfs原樹,對點進行dfn標號,並將關鍵點按dfn從小到大排序。
搞個棧,棧內的點滿足:都在從棧頂的點到原樹的根的一條鏈上。
現在我們準備加入一個點\(x\)
直接加可能破壞一條鏈的性質,於是把棧頂的元素彈掉直到可以加入爲止。求個LCA討論一波,具體參考代碼。
彈棧的時候就可以連好虛樹邊了。

int p=0;//st[0]代表一個dfn爲0的0號空點,方便處理
sort(a+1,a+m+1,cmp);//按dfn排序
for(int i=1;i<=m;st[++p]=a[i++]){
	int y=lca(a[i],st[p]);
	while(p&&dfn[st[p-1]]>=dfn[y])
	    add(st[p-1],st[p]),--p;
	if(y!=st[p])add(y,st[p]),st[p]=y;//注意判斷
}
while(p>1)add(st[p-1],st[p]),--p;//st[1]應爲虛樹根

當然,可能有些題的虛樹在關鍵點之間也有限制?寫出來都不一樣。
比如洛谷P2495 [SDOI2011]消耗戰
有一個固定的\(1\)號點,再就是隻能保留沒有祖孫關係(\(1\)號點除外)的關鍵點。寫法也有好幾處不一樣

int p=0;st[0]=1;
sort(h+1,h+k+1,cmp);
for(R i=1;i<=k;++i){
    if(!p){st[++p]=h[i];continue;}
    R x=h[i],y=lca(x,st[p]);
    if(y==st[p])continue;
    while(p&&l[st[p-1]]>=l[y])add(st[p-1],st[p]),--p;
    if(y!=st[p])add(y,st[p]),st[p]=y;
    st[++p]=x;
}
while(p)add(st[p-1],st[p]),--p;

所以看來虛樹這個東西關鍵不在於背板子,而在於靈活運用。

洛谷P3233 [HNOI2014]世界樹

每個詢問建虛樹,兩遍dfs確定每個虛樹上的點被哪裏管理(第一遍從下往上更新,第二遍從上往下)
對於兩個虛樹點中間的部分,倍增找出臨界點,兩邊的size分開貢獻。
找臨界點是個極其噁心的討論就對了。
倍增代碼短常數大,表示基本沒有看到別的小於2.5k的代碼。。。

#include<bits/stdc++.h>
#define R register int
#define G if(++ip==ie)if(fread(ip=buf,1,SZ,stdin))
using namespace std;
const int SZ=1<<19,N=3e5+9,M=2*N;
char buf[SZ],*ie=buf+SZ,*ip=ie-1;
inline int in(){
	G;while(*ip<'-')G;
	R x=*ip&15;G;
	while(*ip>'-'){x*=10;x+=*ip&15;G;}
	return x;
}
int p,he[N],ne[M],to[M],l[N],sr[N],d[N],o[N],fa[N][20];
void dfs(R x,R f){
	l[x]=++p;sr[x]=1;d[x]=d[f]+1;fa[x][0]=f;
	for(R&i=o[x];(fa[x][i+1]=fa[fa[x][i]][i]);++i);
	for(R i=he[x];i;i=ne[i])
		if(to[i]!=f)dfs(to[i],x),sr[x]+=sr[to[i]];
}
int lca(R x,R y){
	if(d[x]<d[y])swap(x,y);
	for(R i=o[x];~i;--i)
		if(d[fa[x][i]]>=d[y])x=fa[x][i];
	if(x==y)return x;
	for(R i=o[x];~i;--i)
		if(fa[x][i]!=fa[y][i])x=fa[x][i],y=fa[y][i];
	return fa[x][0];
}
namespace VT{
	int h[N],a[N],st[N],he[N],ne[N],tp[N],mn[N],id[N],si[N],ans[N],ok[N];
	inline bool cmp(R x,R y){
		return l[x]<l[y];
	}
	inline void add(R x,R y){
		ne[y]=he[x];he[x]=tp[y]=y;
		for(R i=0,k=d[y]-d[x]-1;k;k>>=1,++i)
			if(k&1)tp[y]=fa[tp[y]][i];
	}
	inline void chkmn(R x,R y){
		R t=mn[y]+abs(d[y]-d[x]);
		if(mn[x]>t)mn[x]=t,id[x]=id[y];
		else if(mn[x]==t&&h[id[x]]>h[id[y]])id[x]=id[y];
	}
	void calc(R x,R y){
		R z=y,p=d[x]-mn[x]+d[y]+mn[y];
		if(p&1)p=(p+1)>>1;
		else p=(p>>1)+(h[id[x]]<h[id[y]]||mn[x]+d[x]==mn[y]+d[y]);
		for(R i=0,k=d[y]-p;k;k>>=1,++i)
			if(k&1)z=fa[z][i];
		ans[id[y]]+=sr[z]-si[y];
		ans[id[x]]+=sr[tp[y]]-sr[z];
		he[y]=si[y]=0;
	}
	void dfsup(R x){
		if(!ok[x])mn[x]=M;
		for(R y=he[x];y;y=ne[y])
			dfsup(y),chkmn(x,y),si[x]+=sr[tp[y]];
	}
	void dfsdn(R x){
		for(R y=he[x];y;y=ne[y])
			chkmn(y,x),dfsdn(y),calc(x,y);
	}
	void work(){
		R m=in(),p=0;
		for(R i=1;i<=m;++i){
			R x=h[i]=a[i]=in();
			mn[x]=0,id[x]=i,ok[x]=1;
		}
		sort(a+1,a+m+1,cmp);
		for(R i=1;i<=m;st[++p]=a[i++]){
			R y=lca(a[i],st[p]);
			while(p&&l[st[p-1]]>=l[y])add(st[p-1],st[p]),--p;
			if(y!=st[p])add(y,st[p]),st[p]=y;
		}
		while(p)add(st[p-1],st[p]),--p;
		dfsup(0);dfsdn(0);he[0]=0;
		for(R i=1;i<=m;++i)printf("%d ",ans[i]),ok[h[i]]=ans[i]=0;puts("");
	}
}
int main(){
	R n=in();to[he[0]=1]=1;
	for(R i=1,p=1;i<n;++i){
		R x=in(),y=in();
		ne[++p]=he[x];to[he[x]=p]=y;
		ne[++p]=he[y];to[he[y]=p]=x;
	}
	dfs(0,0);
	for(R q=in();q;--q)VT::work();
	return 0;
}

仙人掌

orzyyb
orzyl

DFS樹

就是Tarjan算法用的那種結構,邊分成樹邊和返祖邊。
放到仙人掌上就會有一個性質:返祖邊覆蓋的樹邊區間是沒有交錯重疊的。
那麼,我們不用寫Tarjan也可以很方便的知道那些點在一個環裏。
於是已經可以解決一點點問題了。

BZOJ4316 小C的獨立集

求仙人掌最大獨立集
yyb說額外記一維表示環底下那個點的狀態
蒟蒻覺得,先把環上其它子樹都做完,放到環上,再單獨取環底下那個點的兩個狀態分別在環上跑DP,也是挺吼的。
暫時BZOJ rank1

洛谷P2478 [SDOI2010]城市規劃

仙人掌上選若干個點滿足兩兩之間最短路\(>=3\),最大化點權和。和帶權最大獨立集很像的。
每個點的狀態有三個:自己選,兒子選,自己和兒子都不選。轉移隨便yy就好了,細節有一些,但應該還是不難。
環上DP應該要考慮最下面兩個點,依據環底部點對環頂部點的影響(也就是環底部點離最近已選點的距離)分成三類。
然而這個題是個假題。。。
https://www.luogu.org/discuss/lists?forumname=P2478
https://www.lydsy.com/JudgeOnline/wttl/wttl.php?pid=1952
所以下面的代碼蒟蒻也不能保證正確性
(蒟蒻的寫法應該是可以適用於仙人掌而不侷限於題面說的點至多在一個環上

#include<bits/stdc++.h>
#define R register int
#define G if(++ip==ie)if(fread(ip=buf,1,SZ,stdin))
using namespace std;
const int SZ=1<<19,N=1e6+9,M=2*N,INF=-2e9;
char buf[SZ],*ie=buf+SZ,*ip=ie-1;
inline int in(){
	G;while(*ip<'-')G;
	R x=*ip&15;G;
	while(*ip>'-'){x*=10;x+=*ip&15;G;}
	return x;
}
int he[N],ne[M],to[M],fa[N],d[N];
struct Dat{
	int f0,f1,f2;
	inline void operator+=(const Dat&a){
		f0=max(f0+max(a.f0,a.f1),f1+a.f2);
		f1+=max(a.f0,a.f1);
		f2+=a.f1;
	}
}g[N];
void dp(R x,R f){
	Dat now,lst,res=(Dat){0,0,0};R y=fa[x];
	for(R op=0;op<3;++op){//x到已選點的最短路是op
		switch(op){
		case 0:now=(Dat){g[y].f1+g[x].f2,INF,INF};break;
		case 1:now=(Dat){g[y].f0+g[x].f0,g[y].f1+g[x].f0,g[y].f2+g[x].f1};break;
		case 2:now=(Dat){g[y].f0+g[x].f1,g[y].f1+g[x].f1,INF};
		}
		for(R y=fa[x];y!=f;y=fa[y])
			lst=now,(now=g[fa[y]])+=lst;
		switch(op){
		case 2:res.f2=max(res.f2,now.f2);
		case 1:res.f0=max(res.f0,now.f0);res.f1=max(res.f1,now.f1);break;
		case 0:res.f0=max(res.f0,now.f1);
		}
	}
	g[f]=res;
}
int dfs(R x,R f){
	fa[x]=f;d[x]=d[f]+1;
	R top=0;//環頂端
	for(R i=he[x];i;i=ne[i]){
		if(to[i]==f)continue;
		if(d[to[i]]){
			if(d[to[i]]>d[x])dp(to[i],x),top^=x;
			else top^=to[i];
		}
		else top^=dfs(to[i],x);
	}
	if(!top)g[f]+=g[x];
	return top;
}
int main(){
	R n=in(),m=in(),ans=0;
	for(R i=1;i<=n;++i)g[i].f2=in();
	for(R i=1,p=0;i<=m;++i){
		R x=in(),y=in();
		ne[++p]=he[x];to[he[x]=p]=y;
		ne[++p]=he[y];to[he[y]=p]=x;
	}
	for(R i=1;i<=n;++i)
		if(!d[i])dfs(i,0),ans+=max(g[i].f0,max(g[i].f1,g[i].f2));
	printf("%d\n",ans);
	return 0;
}

洛谷P4244 [SHOI2008]仙人掌圖 II

求仙人掌直徑。
不在環上的記一下最大值和次大值轉移即可。
對於環還是單獨來一遍DP,破環爲鏈,貢獻答案的限制爲兩點距離不超過環長的一半,顯然單調隊列優化。

圓方樹

專門用來優化仙人掌上的一些問題。
圓方樹的點分爲圓點和方點,圓點與原來仙人掌中的點一一對應,方點與仙人掌的每個環一一對應。
圓方樹的邊也有兩種:

  1. 仙人掌中不在環上的邊,在圓方樹中保留。
  2. 每個方點向其對應的仙人掌環上的每一個點連一條邊。

可以想象成,把仙人掌的所有環上的邊抹去,環中央建一個方點向四周放射狀連邊,就形成了圓方樹。

另參考YL的總結,圓方樹的另一種寫法是,不在環上的邊中間也強行插入一個方點。
或者說,把不在環上的邊視爲兩條重邊形成的環。
這樣的圓方樹會有一些更好的性質,比如任意路徑上的圓點和方點相間。

BZOJ2125 最短路 or 洛谷P5236 【模板】靜態仙人掌(圓方樹)

仙人掌最短路,多組詢問,不帶修改。
因爲環上兩點的最短路可以直接算,所以建出圓方樹:

  • 不在環上的邊不動;
  • 方點的父親是其對應環的頂端節點,邊權爲0。
  • 在環上但不是頂端的節點的父親是方點,邊權爲其到頂端節點的最短路。

每個詢問在圓方樹上求LCA:

  • LCA是圓點,兩點距離就是答案。
  • LCA是方點,兩點距離除掉頂上那兩條邊的邊權,再加上環上最短路。
#include<bits/stdc++.h>
#define LL long long
#define R register int
#define G if(++ip==ie)if(fread(ip=buf,1,SZ,stdin))
using namespace std;
const int SZ=1<<19,N=3e4+9;
char buf[SZ],*ie=buf+SZ,*ip=ie-1;
inline int in(){
	G;while(*ip<'-')G;
	R x=*ip&15;G;
	while(*ip>'-'){x*=10;x+=*ip&15;G;}
	return x;
}
int n,he[N],ne[N],to[N],w[N],d[N],fa[N];
inline int calc(R x,R y,R len){//環上最短路
	R r=abs(d[x]-d[y]);
	return min(r,len-r);
}
namespace RST{
	int he[N],ne[N],to[N],w[N],d[N],l[N],o[N],fa[N][15];
	inline void add(R x,R y,R z){
		ne[y]=he[x];he[x]=y;fa[y][0]=x;w[y]=z;
	}
	void dfs(R x){
		for(R&i=o[x];(fa[x][i+1]=fa[fa[x][i]][i]);++i);
		for(R y=he[x];y;y=ne[y])
			d[y]=d[x]+1,w[y]+=w[x],dfs(y);
	}
	int qry(R x,R y){
		if(d[x]<d[y])swap(x,y);
		R r=w[x]+w[y];
		for(R i=o[x];~i;--i)
			if(d[fa[x][i]]>=d[y])x=fa[x][i];
		if(x==y)return r-2*w[x];
		for(R i=o[x];~i;--i)
			if(fa[x][i]!=fa[y][i])x=fa[x][i],y=fa[y][i];
		return l[fa[x][0]]?r+calc(x,y,l[fa[x][0]])-w[x]-w[y]:r-2*w[fa[x][0]];
	}
}
void build(R x,R f,R len){
	RST::l[++n]=len;
	for(;x!=f;x=fa[x])
		RST::add(n,x,calc(x,f,len));
	RST::add(x,n,0);
}
int dfs(R x){
	R top=0;
	for(R y,i=he[x];i;i=ne[i]){
		if((y=to[i])==fa[x])continue;
		if(d[y]){
			if(d[y]>d[x])build(y,x,d[y]-d[x]+w[i]),top^=x;
			else top^=y;
		}
		else fa[y]=x,d[y]=d[x]+w[i],top^=dfs(y);
	}
	if(!top&&x!=1)RST::add(fa[x],x,d[x]-d[fa[x]]);
	return top;
}
int main(){
	n=in();R m=in(),q=in();
	for(R p=0,i=1;i<=m;++i){
		R x=in(),y=in();w[p+1]=w[p+2]=in();
		ne[++p]=he[x];to[he[x]=p]=y;
		ne[++p]=he[y];to[he[y]=p]=x;
	}
	d[1]=RST::d[1]=1;//防止一些邊界情況
	dfs(1);RST::dfs(1);
	while(q--)
		printf("%d\n",RST::qry(in(),in()));
	return 0;
}

廣義圓方樹

將仙人掌的環對應一般圖的點雙,圓方樹也就變成了廣義圓方樹。
寫Tarjan求割點,把點雙裏的點壓進棧裏,一起連邊後一起彈出來。
核心構建代碼

void dfs(R x){
	low[x]=dfn[x]=++df;st[++p]=x;
	for(R y,i=he[x];i;i=ne[i])
		if(dfn[y=to[i]])cmn(low[x],dfn[y]);
		else{
			dfs(y),cmn(low[x],low[y]);
			if(low[y]==dfn[x]){
				RST::add(x,++RST::n);R z;
				do RST::add(RST::n,z=st[p--]);while(z!=y);
			}
		}
}

洛谷P4320 道路相遇 (板子題)

歐拉路徑

遍歷整張圖,不重不漏地經過每一條邊的路徑。如果起點終點相同則稱作歐拉回路。
判斷歐拉路、歐拉回路是否存在的充要條件:

無向圖歐拉回路:所有點度數爲偶數
有向圖歐拉回路:所有點入度等於出度
無向圖歐拉路:至多兩點度數爲奇數
有向圖歐拉路:至多一點入度等於出度+1,一點入度等於出度-1,其它所有點入度等於出度

構造方法:dfs,每次訪問一條未訪問的邊並打上訪問標記,回溯時將邊加入答案數組,最後將數組倒序輸出。
UOJ117 歐拉回路

#include<bits/stdc++.h>
#define LL long long
#define R register int
#define G if(++ip==ie)if(fread(ip=buf,1,SZ,stdin))
using namespace std;
const int SZ=1<<19,N=1e5+9,M=4*N;
char buf[SZ],*ie=buf+SZ,*ip=ie-1;
inline int in(){
	G;while(*ip<'-')G;
	R x=*ip&15;G;
	while(*ip>'-'){x*=10;x+=*ip&15;G;}
	return x;
}
int t,n,m,p,he[N],ne[M],to[M],d[N],ans[M];
bool vis[M];
void dfs1(R x){
	for(R i=he[x];i;i=he[x]){
		he[x]=ne[he[x]];
		if(!vis[i]){
			vis[i^1]=1;
			dfs1(to[i]),ans[++p]=(i>>1)*(i&1?-1:1);
		}
	}
}
void dfs2(R x){
	for(R i=he[x];i;i=he[x]){
		he[x]=ne[he[x]];
		dfs2(to[i]),ans[++p]=i;
	}
}
int main(){
	t=in(),n=in(),m=in();
	for(R i=1,p=t&1;i<=m;++i){
		R x=in(),y=in();
		ne[++p]=he[x],to[he[x]=p]=y,++d[y];
		if(t&1)ne[++p]=he[y],to[he[y]=p]=x,++d[x];
		else --d[x];
	}
	for(R i=1;i<=n;++i)
		if(t&1?d[i]&1:d[i])return puts("NO"),0;
	for(R i=1;i<=n;++i)
		if((t&1?dfs1:dfs2)(i),p)break;
	if(p<m)return puts("NO"),0;
	puts("YES");
	for(R i=p;i;--i)printf("%d ",ans[i]);
	puts("");
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章