zoukankan      html  css  js  c++  java
  • 《欧拉图相关的生成与计数问题探究》学习笔记

    基本概念

    定义: 度(自环统计计两次),入度,出度。奇顶点,偶顶点,孤立点。连通,连通图,弱连通图。桥。路径,回路。欧拉回路(每条边经过恰一次),欧拉路径。欧拉图(存在欧拉回路),半欧拉图(存在欧拉路径)。简单图。

    欧拉图的判定

    无向欧拉图

    定理: 一张无向图为欧拉图当且仅当该图连通且无奇顶点。

    定理: 一张无向图为半欧拉图当且仅当该图连通且奇顶点数恰为两个。此时两个奇顶点为欧拉路径的起点和终点。

    定理: 若一张无向连通图有 (2k) 个奇顶点,则可以用 (k) 条路经将该图的每条边经过一次,且至少要用 (k) 条路径。

    有向欧拉图

    定理: 一张有向图为欧拉图当且仅当该图弱连通且所有点的入度等于出度。

    定理: 一张有向图为半欧拉图当且仅当该图弱连通且恰有一个点 (u) 入度比出度小 (1),一个点 (v) 入度比出度大 (1)。此时 (u)(v) 分别为欧拉路径的起点和终点。

    定理: 若一张有向弱连通图中所有节点的入度与出度之差的绝对值之和为 (2k),则可以用 (k) 条路经将该图的每条边经过一次,且至少要用 (k) 条路径。

    欧拉回路的生成

    问题: 给定无向欧拉图 (G),求出 (G) 的一条欧拉回路。

    Fleury 算法

    维护当前经过 (k) 条边的欧拉路径 (p_k),并且保证任意时刻未访问的边构成的子图 (G_k=Gsetminus 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_2ldots x_n) 连接 ((x_1,x_2ldots x_{n-1},x_2x_3ldots x_n))。于是原问题等价于求此图上的欧拉回路。由于此图的每个节点都恰有两条出边和两条入边,所以解一定存在。

    同时,该序列可以扩展到 (k) 进制。

    例题: (POJ 1780)

    混合图欧拉回路

    问题: 给定包含有向边和无向边的弱连通图,求出该图的一条欧拉回路或判断无解。

    解法: 将所有无向边定向之后就容易解决,但要满足定向之后所有点的入度等于出度。考虑先随意定向,然后通过网络流进行调整、构造。

    例题: (Luogu 3511)

    中国邮递员问题

    问题: 给定有向带权连通图,求出一条总边权和最小的回路,使得经过每条边至少一次。

    解法: 原问题等价于复制一些边若干次,使得所有点入度等于出度,最小化总边权。可以转化成费用流问题进行求解。

    问题: 给定无向带权连通图,求出一条总边权和最小的回路,使得经过每条边至少一次。

    解法: 原问题等价于复制一些边若干次,使得所有点度数为偶,最小化总边权。容易发现每次选定两个奇顶点,复制它们最短路上的边是使它们变为偶顶点的最佳方案。于是对所有奇顶点进行一般图最小权完美匹配即可。

    欧拉图相关的计数

    欧拉图计数

    问题:(n) 个节点无奇顶点的有标号简单无向图个数。

    解法: 考虑 (n) 号点的连边,与 (n-1) 个节点的有编号任意简单无向图进行双射。答案为 (2^{inom{n-1}{2}})

    问题:(n) 个节点的有标号简单连通欧拉图个数。

    解法: 容斥(枚举 (n) 号点所在连通块的大小)/生成函数(ln/求逆)。

    欧拉子图计数

    问题: 给定 (n) 个节点 (m) 条边的无向连通图,求该图有多少无奇顶点的支撑子图。

    解法: 考虑一棵生成树。每条非树边都有选或不选两种选择,所有非树边都确定后树边只有一种可能的选择方案。答案为 (2^{m-n+1})

    问题: 给定 (n) 个节点 (m) 条边的无向图,求该图有多少无奇顶点的支撑子图。

    解法: 对每个连通块单独计算,再累乘。

    欧拉回路计数

    问题: 给定 (n) 个节点 (m) 条边的有向欧拉图,求从 (1) 号点开始的欧拉路径数。

    解法: 考虑一种构造方案。找到一棵以 (1) 号点为根的内向树,对于一个点所有不在树上的出边指定顺序。此方案与从 (1) 号点开始的欧拉路径构成双射:

    • 此方案 ( ightarrow) 欧拉路径:考虑 Fleury 算法的过程。对于每个点,先按照指定顺序访问不在树上的出边,再访问树边。容易证明方案合法。
    • 欧拉路径 ( ightarrow) 此方案:将除了一号点之外的所有点在路径中访问的最后一条出边设为树边,其余边按访问次序决定顺序。容易证明选择的树边无环。

    答案为 (T_1d_1!prod_{i=2}^n(d_i-1)!)。其中 (d_i)(i) 号点的出度,(T_i) 为以 (i) 号点为根的内向树个数(可以用矩阵树定理求出)。

    问题: 给定 (n) 个节点 (m) 条边的有向欧拉图,求欧拉回路数。 (BEST 定理)

    解法: 考虑用同样的方法求出。为了去重,钦定 (1) 号点的一条出边为第一条访问边。答案为 (T_1prod_{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
    ",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
    ",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
    ",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
    ",ans);
    		chk(ans,1);
    	}
    }
    
  • 相关阅读:
    PHP递归方法实现前序、中序、后序遍历二叉树
    php循环方法实现先序、中序、后序遍历二叉树
    Mac charles 抓取https请求,安装证书后还是显示unknown
    PHP工厂模式
    PHP策略模式2
    PHP单例模式
    PHP 面试知识点整理归纳
    十大迷你iPhone天气应用
    来自极客标签10款最新设计素材-系列十三
    帮助快速生成页面固定显示元素的jQuery插件
  • 原文地址:https://www.cnblogs.com/Y25t/p/15344552.html
Copyright © 2011-2022 走看看