《歐拉圖相關的生成與計數問題探究》學習筆記


基本概念

定義: 度(自環統計計兩次),入度,出度。奇頂點,偶頂點,孤立點。連通,連通圖,弱連通圖。橋。路徑,回路。歐拉回路(每條邊經過恰一次),歐拉路徑。歐拉圖(存在歐拉回路),半歐拉圖(存在歐拉路徑)。簡單圖。

歐拉圖的判定

無向歐拉圖

定理: 一張無向圖為歐拉圖當且僅當該圖連通且無奇頂點。

定理: 一張無向圖為半歐拉圖當且僅當該圖連通且奇頂點數恰為兩個。此時兩個奇頂點為歐拉路徑的起點和終點。

定理: 若一張無向連通圖有 \(2k\) 個奇頂點,則可以用 \(k\) 條路經將該圖的每條邊經過一次,且至少要用 \(k\) 條路徑。

有向歐拉圖

定理: 一張有向圖為歐拉圖當且僅當該圖弱連通且所有點的入度等於出度。

定理: 一張有向圖為半歐拉圖當且僅當該圖弱連通且恰有一個點 \(u\) 入度比出度小 \(1\),一個點 \(v\) 入度比出度大 \(1\)。此時 \(u\)\(v\) 分別為歐拉路徑的起點和終點。

定理: 若一張有向弱連通圖中所有節點的入度與出度之差的絕對值之和為 \(2k\),則可以用 \(k\) 條路經將該圖的每條邊經過一次,且至少要用 \(k\) 條路徑。

歐拉回路的生成

問題: 給定無向歐拉圖 \(G\),求出 \(G\) 的一條歐拉回路。

Fleury 算法

維護當前經過 \(k\) 條邊的歐拉路徑 \(p_k\),並且保證任意時刻未訪問的邊構成的子圖 \(G_k=G\setminus p_k\) 除去孤立點后連通。具體地,可以每次暴力枚舉待擴展的邊,刪掉它然后 bfs/dfs 判斷連通性。時間復雜度 \(O(m^2)\)

同時,該算法可以通過回溯來求一張無向歐拉圖(半歐拉圖)的所有歐拉回路(路徑)。

Hierholzer 算法

隨意選取起始點進行 dfs,沿着任意未訪問的邊走到相鄰節點,直至無路可走,此時必然會回到起點形成回路。若仍有邊為訪問過,則退棧時找到有未訪問邊的節點,以它開始求出另一回路並與之前回路拼接。如此反復直到所有邊都被訪問。時間復雜度 \(O(m)\)

同時,該算法可以用於求解有向圖的情況、求解歐拉路徑、求解對字典序有要求的問題、求最少路徑數將圖的所有邊經過一次。

歐拉圖相關的性質

定理: 對於任意無向連通圖,一定存在回路使得每條邊經過恰好兩次。進一步地,存在回路使得每條邊的兩個方向各經過一次。

證明: 將該圖的每一條邊變成兩條重邊,能夠得到無向歐拉圖;將該圖的每一條邊變成兩條方向相反的有向邊,能夠得到有向歐拉圖。

例題: (CF 788 B)

定理: 一張無向圖有圈分解當且僅當該圖無奇頂點。

例題: (BZOJ 3706)

定理: 對於不存在歐拉回路的圖,若最少用 \(a\) 條路徑將圖中的每條邊經過一次,最少在圖中中加入 \(b\) 條邊使其成為歐拉圖,那么 \(a=b\)

例題: (CF 209 C)

歐拉圖的生成問題

De Bruijin 序列

問題: 求出一個 \(2^n\) 位的環形 0/1 串,滿足所有 \(2^n\) 個長為 \(n\) 的子串恰為所有 \(2^n\)\(n\) 位 0/1 串。

解法: 構造一個 \(2^{n-1}\) 個點,\(2^n\) 次方條邊的有向圖。其中每個點代表一個 \(n-1\) 位 0/1 串,每條邊代表一個 \(n\) 位 0/1 串。其中有向邊 \(x_1x_2\ldots x_n\) 連接 \((x_1,x_2\ldots x_{n-1},x_2x_3\ldots x_n)\)。於是原問題等價於求此圖上的歐拉回路。由於此圖的每個節點都恰有兩條出邊和兩條入邊,所以解一定存在。

同時,該序列可以擴展到 \(k\) 進制。

例題: (POJ 1780)

混合圖歐拉回路

問題: 給定包含有向邊和無向邊的弱連通圖,求出該圖的一條歐拉回路或判斷無解。

解法: 將所有無向邊定向之后就容易解決,但要滿足定向之后所有點的入度等於出度。考慮先隨意定向,然后通過網絡流進行調整、構造。

例題: (Luogu 3511)

中國郵遞員問題

問題: 給定有向帶權連通圖,求出一條總邊權和最小的回路,使得經過每條邊至少一次。

解法: 原問題等價於復制一些邊若干次,使得所有點入度等於出度,最小化總邊權。可以轉化成費用流問題進行求解。

問題: 給定無向帶權連通圖,求出一條總邊權和最小的回路,使得經過每條邊至少一次。

解法: 原問題等價於復制一些邊若干次,使得所有點度數為偶,最小化總邊權。容易發現每次選定兩個奇頂點,復制它們最短路上的邊是使它們變為偶頂點的最佳方案。於是對所有奇頂點進行一般圖最小權完美匹配即可。

歐拉圖相關的計數

歐拉圖計數

問題:\(n\) 個節點無奇頂點的有標號簡單無向圖個數。

解法: 考慮 \(n\) 號點的連邊,與 \(n-1\) 個節點的有編號任意簡單無向圖進行雙射。答案為 \(2^{\binom{n-1}{2}}\)

問題:\(n\) 個節點的有標號簡單連通歐拉圖個數。

解法: 容斥(枚舉 \(n\) 號點所在連通塊的大小)/生成函數(ln/求逆)。

歐拉子圖計數

問題: 給定 \(n\) 個節點 \(m\) 條邊的無向連通圖,求該圖有多少無奇頂點的支撐子圖。

解法: 考慮一棵生成樹。每條非樹邊都有選或不選兩種選擇,所有非樹邊都確定后樹邊只有一種可能的選擇方案。答案為 \(2^{m-n+1}\)

問題: 給定 \(n\) 個節點 \(m\) 條邊的無向圖,求該圖有多少無奇頂點的支撐子圖。

解法: 對每個連通塊單獨計算,再累乘。

歐拉回路計數

問題: 給定 \(n\) 個節點 \(m\) 條邊的有向歐拉圖,求從 \(1\) 號點開始的歐拉路徑數。

解法: 考慮一種構造方案。找到一棵以 \(1\) 號點為根的內向樹,對於一個點所有不在樹上的出邊指定順序。此方案與從 \(1\) 號點開始的歐拉路徑構成雙射:

  • 此方案 \(\rightarrow\) 歐拉路徑:考慮 Fleury 算法的過程。對於每個點,先按照指定順序訪問不在樹上的出邊,再訪問樹邊。容易證明方案合法。
  • 歐拉路徑 \(\rightarrow\) 此方案:將除了一號點之外的所有點在路徑中訪問的最后一條出邊設為樹邊,其余邊按訪問次序決定順序。容易證明選擇的樹邊無環。

答案為 \(T_1d_1!\prod_{i=2}^n(d_i-1)!\)。其中 \(d_i\)\(i\) 號點的出度,\(T_i\) 為以 \(i\) 號點為根的內向樹個數(可以用矩陣樹定理求出)。

問題: 給定 \(n\) 個節點 \(m\) 條邊的有向歐拉圖,求歐拉回路數。 (BEST 定理)

解法: 考慮用同樣的方法求出。為了去重,欽定 \(1\) 號點的一條出邊為第一條訪問邊。答案為 \(T_1\prod_{i=1}^n(d_i-1)!\)

問題: 給定 \(n\) 個節點 \(m\) 條邊的有向半歐拉圖,求歐拉路徑數。

解法: 將該圖添加一條有向邊變成歐拉圖,新圖的歐拉回路和原圖的歐拉路徑構成雙射,用 BEST 定理計算即可。

例題

CF 788 B

簡要題意

給定 \(n\) 個點,\(m\) 條邊的無向連通圖。求出有多少條路徑滿足經過其中 \(m-2\) 條邊各兩次,剩下兩條邊各一次。兩條路徑不同當且僅當存在一條邊在兩條路徑中的經過次數不同。

題解

將該圖的每一條邊變成兩條重邊,原問題轉化為刪除兩條邊使得奇頂點個數 \(\leq 2\)。分類討論然后計數即可。

code

#include<cstdio>
#include<algorithm>
#define ll long long
#define N 1000005

int n,m;

int hd[N],_hd;
struct edge{
	int v,nxt;
}e[N<<1];
inline void addedge(int u,int v){
	e[++_hd]=(edge){v,hd[u]};
	hd[u]=_hd;
}

bool vis[N];
inline void dfs(int u){
	vis[u]=1;
	for(int i=hd[u];i;i=e[i].nxt)
		if(!vis[e[i].v])
			dfs(e[i].v);
}

int deg[N],cnt;

inline ll C2(int x){
	return 1ll*x*(x-1)/2;
}

ll ans;

int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		vis[i]=1;
	for(int i=1;i<=m;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		if(u==v)
			cnt++;
		else{
			deg[u]++;
			deg[v]++;
		}
		addedge(u,v);
		addedge(v,u);
		vis[u]=vis[v]=0;
	}
	for(int i=1;i<=n;i++)
		if(!vis[i]){
			dfs(i);
			break;
		}
	for(int i=1;i<=n;i++)
		if(!vis[i]){
			puts("0");
			return 0;
		}
	ans=C2(cnt)+1ll*cnt*(m-cnt);
	for(int i=1;i<=n;i++)
		ans+=C2(deg[i]);
	printf("%lld\n",ans);
}

BZOJ 3706

簡要題意

給定 \(n\) 個點,\(m\) 條邊的無向圖,邊有黑白兩種顏色。你每次操作可以選擇一條回路將其反色。求出將所有邊變成白色的最小操作次數或判斷無解。會進行若干次對一條邊反色,每次反色后求出答案。

題解

要滿足條件當且僅當所有黑邊經過奇數次,所有白邊經過偶數次。於是可以將圖的每一條白邊變成兩條重邊,有解當且僅當新圖可以圈分解。最小操作數為包含黑邊的連通塊數量。

code

#include<cstdio>
#include<algorithm>
#define N 1000005

int n,m,q;
struct Edge{
	int u,v,col;
}E[N];

int deg[N];

int f[N];
inline int fnd(int x){
	return f[x]?f[x]=fnd(f[x]):x;
}

int cnt,sz[N];

int ans;

int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		int u,v,col;
		scanf("%d%d%d",&u,&v,&col);
		E[i]=(Edge){u,v,col};
		int fu=fnd(u),fv=fnd(v);
		if(fu!=fv)
			f[fu]=fv;
		if(col){
			deg[u]^=1;
			deg[v]^=1;
		}
	}
	for(int i=1;i<=n;i++)
		cnt+=deg[i];
	for(int i=1;i<=m;i++)
		sz[fnd(E[i].u)]+=E[i].col;
	for(int i=1;i<=n;i++)
		if(!f[i]&&sz[i])
			ans++;
	scanf("%d",&q);
	while(q--){
		int opt;
		scanf("%d",&opt);
		if(opt==1){
			int i;
			scanf("%d",&i);
			i++;
			int u=E[i].u,v=E[i].v,col=E[i].col;
			E[i].col^=1;
			int fu=fnd(u);
			ans-=sz[fu]>0;
			sz[fu]+=E[i].col-col;
			ans+=sz[fu]>0;
			cnt-=deg[u]+deg[v];
			deg[u]^=1,deg[v]^=1;
			cnt+=deg[u]+deg[v];
		}
		else{
			if(cnt)
				puts("-1");
			else
				printf("%d\n",ans);
		}
	}
}

CF 209 C

簡要題意

給定 \(n\) 個點,\(m\) 條邊的無向圖。求最少添加多少條無向邊后,圖中存在從號點 1 出發的歐拉回路。

題解

轉化為將圖中每條邊經過一次的最少路徑數,對每個連通塊分別考慮即可。注意特判 1 號點為孤立點的情況。

code

#include<cstdio>
#include<algorithm>
#define N 1000005

int n,m;

int hd[N],_hd;
struct edge{
	int v,nxt;
}e[N<<1];
inline void addedge(int u,int v){
	e[++_hd]=(edge){v,hd[u]};
	hd[u]=_hd;
}

int deg[N],cnt;

bool vis[N];
inline void dfs(int u){
	vis[u]=1;
	cnt+=deg[u]&1;
	for(int i=hd[u];i;i=e[i].nxt)
		if(!vis[e[i].v])
			dfs(e[i].v);
}

int co,ce;

int ans;

int main(){
	scanf("%d%d",&n,&m);
	if(!m){
		puts("0");
		return 0;
	}
	for(int i=1;i<=n;i++)
		vis[i]=1;
	for(int i=1;i<=m;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		deg[u]++;
		deg[v]++;
		addedge(u,v);
		addedge(v,u);
		vis[u]=vis[v]=0;
	}
	for(int i=1;i<=n;i++)
		if(!vis[i]){
			cnt=0;
			dfs(i);
			if(!cnt){
				ans++;
				ce++;
			}
			else{
				ans+=cnt/2;
				co++;
			}
		}
	if(ce==1&&co==0&&deg[1])
		ans=0;
	if(!deg[1])
		ans++;
	printf("%d\n",ans);
}

POJ 1780

簡要題意

給定 \(n\),求出一個字典序最小的長度為 \(10^n+n-1\)\(10\) 進制串,滿足所有 \(10^n\) 個長為 \(n\) 的子串恰為所有 \(10^n\)\(n\)\(10\) 進制串。

題解

仿照 0/1 串的 De Bruijin 序列的解法,建出 \(n\)\(10\) 進制串對應的圖,找到字典序最小的歐拉路徑即可。

code

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<stack>
#define pii std::pair<int,int>
#define mp std::make_pair
#define fir first
#define sec second
#define M 1000005

int n,m;

int vis[M][10];

std::stack<pii> st;

std::vector<int> ans;

int main(){
	while(1){
		scanf("%d",&n);
		if(!n)
			break;
		m=1;
		for(int i=1;i<=n;i++)
			m*=10;
		ans.clear();
		memset(vis,0,sizeof(vis));
		vis[0][0]=1;
		st.push(mp(0,0));
		while(st.size()){
			int u=st.top().fir,c=st.top().sec;
			int flg=0;
			for(int i=0;i<10;i++)
				if(!vis[u][i]){
					vis[u][i]=1;
					st.push(mp((u+i*m/10)/10,i));
					flg=1;
					break;
				}
			if(!flg){
				ans.push_back(c);
				st.pop();
			}
		}
		std::reverse(ans.begin(),ans.end());
		for(int i=1;i<n;i++)
			putchar('0');
		for(int i=0;i<ans.size();i++)
			putchar('0'+ans[i]);
		puts("");
	}
}

Luogu 3511

簡要題意

給定 \(n\) 個點,\(m\) 條邊的圖,每條邊正着走和反着走有不同的邊權。求一條最大邊權最小的從 \(1\) 號點出發的歐拉回路。

題解

二分答案后轉化為混合圖歐拉回路問題。

code

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<queue>
#define inf 0x3f3f3f3f
#define N 1005
#define M 2005

namespace MF{
	int n,s,t;
	
	int hd[N],_hd;
	struct edge{
		int v,f,nxt;
	}e[(M+N)<<1];
	inline void addedge(int u,int v,int f){
		e[++_hd]=(edge){v,f,hd[u]};
		hd[u]=_hd;
		e[++_hd]=(edge){u,0,hd[v]};
		hd[v]=_hd;
	}
	
	inline void init(int n_,int s_,int t_){
		for(int i=1;i<=n;i++)
			hd[i]=0;
		_hd=1;
		n=n_,s=s_,t=t_;
	}
	
	std::queue<int> q;
	int cur[N],dis[N];
	inline bool bfs(){
		for(int i=1;i<=n;i++)
			cur[i]=hd[i];
		for(int i=1;i<=n;i++)
			dis[i]=inf;
		dis[s]=0;
		q.push(s);
		while(q.size()){
			int u=q.front();
			q.pop();
			for(int i=hd[u];i;i=e[i].nxt){
				int v=e[i].v,f=e[i].f;
				if(f&&dis[v]>dis[u]+1){
					dis[v]=dis[u]+1;
					q.push(v);
				}
			}
		}
		return dis[t]<inf;
	}
	inline int dfs(int u,int lmt){
		if(u==t||!lmt)
			return lmt;
		int res=0;
		for(int i=cur[u];i;i=e[i].nxt){
			cur[u]=i;
			int v=e[i].v,f=e[i].f;
			if(dis[v]!=dis[u]+1)
				continue;
			f=dfs(v,std::min(lmt,f));
			e[i].f-=f,e[i^1].f+=f;
			lmt-=f,res+=f;
			if(!lmt)
				break;
		}
		return res;
	}
	inline int sol(){
		int res=0;
		while(bfs())
			res+=dfs(s,inf);
		return res;
	}
}

int n,m;
struct Edge{
	int u,v,a,b;
}E[M];

int cnt;

namespace DSU{
	int f[N],sz[N];
	inline int fnd(int x){
		return f[x]?f[x]=fnd(f[x]):x;
		}
	inline void mrg(int u,int v){
		int fu=fnd(u),fv=fnd(v);
		if(fu==fv)
			return;
		if(sz[fu]<sz[fv]){
			f[fu]=fv;
			sz[fv]+=sz[fu];
		}
		else{
			f[fv]=fu;
			sz[fu]+=sz[fv];
		}
	}
	inline void init(){
		memset(f,0,sizeof(f));
		for(int i=1;i<=n;i++)
			sz[i]=1;
	}
}

int hd[N],_hd;
struct edge{
	int v,nxt;
}e[M];
inline void addedge(int u,int v){
	e[++_hd]=(edge){v,hd[u]};
	hd[u]=_hd;
}

bool vis[M];
std::vector<int> p;
inline void dfs(int u,int c){
	for(int i=hd[u];i;i=e[i].nxt)
		if(!vis[i]){
			vis[i]=1;
			dfs(e[i].v,i);
		}
	if(c)
		p.push_back(c);
}

int deg[N],id[M];
inline bool chk(int x,int tp){
	memset(deg,0,sizeof(deg));
	MF::init(n+2,n+1,n+2);
	for(int i=1;i<=m;i++){
		int u=E[i].u,v=E[i].v,a=E[i].a,b=E[i].b;
		if(a<=x&&b<=x){
			deg[u]--,deg[v]++;
			MF::addedge(v,u,1);
			if(tp)
				id[i]=MF::_hd;
		}
		else if(a<=x)
			deg[u]--,deg[v]++;
		else if(b<=x)
			deg[u]++,deg[v]--;
		else
			return 0;
	}
	int sum=0;
	for(int i=1;i<=n;i++){
		if(deg[i]&1)
			return 0;
		if(deg[i]>0){
			MF::addedge(MF::s,i,deg[i]/2);
			sum+=deg[i]/2;
		}
		else
			MF::addedge(i,MF::t,-deg[i]/2);
	}
	if(MF::sol()!=sum)
		return 0;
	if(!tp)
		return 1;
	for(int i=1;i<=m;i++){
		int u=E[i].u,v=E[i].v,a=E[i].a,b=E[i].b;
		if(a<=x&&b<=x){
			if(MF::e[id[i]].f)
				addedge(v,u);
			else
				addedge(u,v);
		}
		else if(a<=x)
			addedge(u,v);
		else if(b<=x)
			addedge(v,u);
	}
	dfs(1,0);
	std::reverse(p.begin(),p.end());
	for(auto i:p)
		printf("%d ",i);
	puts("");
	return 1;
}

int main(){
	scanf("%d%d",&n,&m);
	DSU::init();
	for(int i=1;i<=m;i++){
		scanf("%d%d%d%d",&E[i].u,&E[i].v,&E[i].a,&E[i].b);
		DSU::mrg(E[i].u,E[i].v);
	}
	for(int i=2;i<=n;i++)
		if(!DSU::f[i]&&DSU::sz[i]==1)
			cnt++;
	if(DSU::sz[DSU::fnd(1)]!=n-cnt){
		puts("NIE");
		return 0;
	}
	int l=1,r=1000,ans=-1;
	while(l<=r){
		int mid=(l+r)>>1;
		if(chk(mid,0)){
			ans=mid;
			r=mid-1;
		}
		else
			l=mid+1;
	}
	if(ans==-1)
		puts("NIE");
	else{
		printf("%d\n",ans);
		chk(ans,1);
	}
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM