Loading

【笔记】欧拉图

pZJ8kM8.jpg

以上是图论经典问题:科尼斯堡七桥问题,由欧拉于 1736 年提出,旨在探讨图上的一笔画问题。

欧拉路径、欧拉回路

欧拉路径:图中经过所有边恰好一次的路径(即一笔画)。若该路径的起点与终点相同,则称其为一条欧拉回路。

判断

判断一个连通图是否存在欧拉路径:

有向图欧拉路径:图中恰存在 \(1\) 点,使其出度比入度多 \(1\)(作为起点 \(S\)),且恰存在 \(1\) 点,入度比出度多 \(1\)(作为终点 \(T\)),其余节点入度 \(=\) 出度。

有向图欧拉回路:所有点的入度 \(=\) 出度。此时 \(S\)\(T\) 可为任意点。

无向图欧拉路径:图中恰存在 \(2\) 点度为奇数,其余点度均为偶数,则两度为奇数点可作为 \(S\)\(T\)

无向图欧拉回路:所有点度均为偶数。\(S\)\(T\) 可为任意点。

存在欧拉回路时,一定存在欧拉路径。

存在欧拉回路的图叫欧拉图,仅存在欧拉路径的图叫半欧拉图。

证明不在此给出。

Luogu P1636 Einstein学画画

根据欧拉路径的判断,两奇点 \(+\) 若干偶点可以组成一条欧拉路径。因为题目没有规定一边只能画一次,所以同一个连通块内,奇点两两间都可以连成一条欧拉路径,只需要将奇点个数 \(\div 2\) 便可得到该连通块需要几笔画完。但要特判连通块上均为偶点的情况,此时可一笔画完。最后把所有连通块答案累加。

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
const int M=1e5+10;
int n,m,cnt,ans;
int deg[N];
int h[N],tot;
bool vis[N];
struct Node{
    int to,nxt;
}e[2*M];
void Add(int u,int v){
    tot++;
    e[tot].to=v;
    e[tot].nxt=h[u];
    h[u]=tot;
}
void dfs(int u){
    if(deg[u]&1) cnt++;
    vis[u]=1;
    for(int i=h[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(vis[v]) continue;
        dfs(v);
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int u,v;
        cin>>u>>v;
        deg[u]++,deg[v]++;
        Add(u,v),Add(v,u);
    }
    for(int i=1;i<=n;i++){
        if(!vis[i]){
            cnt=0;
            dfs(i);
            if(!deg[i]) continue;
            if(cnt) ans+=cnt/2;
            else ans++;
        }
	}
    cout<<ans;
    return 0;
}

UVA10129 单词 Play on Words

对于一个字符串,相当于一条连接首位字符的有向边。

首先需要判连通性,将有向边视为无向边,用并查集判断弱连通性。

然后按有向图欧拉路径存在条件判断,记录入度出度。

#include<bits/stdc++.h>
using namespace std;
const int N=50;
int T,n;
int ind[N],outd[N];
int fa[N];
int Find(int x){
    if(fa[x]==x) return x;
    else return fa[x]=Find(fa[x]);
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>T;
    while(T--){
        cin>>n;
        memset(ind,0,sizeof(ind));
        memset(outd,0,sizeof(outd));
        for(int i=0;i<26;i++) fa[i]=i;
        for(int i=1;i<=n;i++){
            string s;
            cin>>s;
            int l=s[0]-'a',r=s[s.size()-1]-'a';
            ind[r]++,outd[l]++;
            l=Find(l),r=Find(r);
            if(l!=r) fa[l]=r;
        }
        bool flag=0;
        int p=-1;
        for(int i=0;i<26;i++){
            if(!ind[i]&&!outd[i]) continue;
            if(Find(i)!=p&&(p!=-1)){
                flag=1;
                break;
            }
            p=fa[i];
        }
        if(flag){
            cout<<"The door cannot be opened.\n";
            continue;
        }
        int cnts=0,cnte=0;
        for(int i=0;i<26;i++){
            if(ind[i]==outd[i]) continue;
            if(outd[i]-ind[i]==1) cnts++;
            else if(ind[i]-outd[i]==1) cnte++;
            else{
                flag=1;
                break;
            } 
        }
        if(!flag&&((!cnts&&!cnte)||(cnts==1&&cnte==1))) cout<<"Ordering is possible.\n";
        else cout<<"The door cannot be opened.\n";
    }
    return 0;
}

Luogu P1333 瑞瑞的木棍

无向图版本。只需为字符串开一个 map,其余相同。

注意边数最大 \(2\times 10^5\),因此点数最大 \(5\times 10^5\)

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
int T,n;
int d[N];
int fa[N],tot;
map<string,int> stk;
int Find(int x){
    if(fa[x]==x) return x;
    else return fa[x]=Find(fa[x]);
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    for(int i=1;i<=N-10;i++) fa[i]=i;
    string s,t;
    while(cin>>s>>t){
        int l,r;
        if(!stk[s]) stk[s]=++tot;
        if(!stk[t]) stk[t]=++tot;
        l=stk[s],r=stk[t];
        d[l]++,d[r]++;
        l=Find(l),r=Find(r);
        if(l!=r) fa[l]=r;
    }
    bool flag=0;
    int p=-1;
    for(int i=1;i<=tot;i++){
        if(Find(i)!=p&&(p!=-1)){
            flag=1;
            break;
        }
        p=fa[i];
    }
    if(flag){
        cout<<"Impossible";
        return 0;
    } 
    int cnt=0;
    for(int i=1;i<=tot;i++){
        if(d[i]%2) cnt++;
    } 
    if((!cnt)||(cnt==2)) cout<<"Possible";
    else cout<<"Impossible";
    return 0;
}

寻找

寻找一条欧拉路径:

  1. 确定起点 \(S\)
  2. \(S\) 开始 DFS。

DFS 伪代码:

void dfs(int u){
    枚举 u 的出边
        若该边未访问
            标记为已访问
            dfs(边指向的另一个点)
    u 入栈
}

最后倒序输出栈内所有节点。

倒序输出的正确性:

  • 若是欧拉回路,则欧拉路径不唯一,在遍历过程中,走到哪个点,哪个点就可以接在已走过的点后面,直到所有边都被走过,最终路径一定合法。
  • 若是欧拉路径,则会遇到所有出边都被标记过而无路可走的情况。第一个遇到这种情况的一定会是欧拉路径终点,剩下的点都不会被走到而入栈,所以倒序输出仍然合法。

Luogu P7771【模板】欧拉路径

一个点可以被重复搜索,但边不能。

所以我们可以用一个数组 \(\Delta u\) 记录上次访问到的 \(u\) 的出边号,在向下搜索时,不会重复访问出边。

由于要求要按最小字典序输出,所以要对出边从小到大排序,因此使用 vector 存储。

#include<iostream>
#include<vector>
#include<algorithm>
#include<cstdio>
using namespace std;
const int maxn=1e5+10;
int n,m,s=1;
int ind[maxn],outd[maxn],r[maxn],cnt,d[maxn];
vector<int>g[maxn];
void dfs(int u){
	for(int i=d[u];i<g[u].size();i=d[u]){
		d[u]=i+1;
		dfs(g[u][i]);
	} 
	r[++cnt]=u;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		g[u].push_back(v);
		outd[u]++,ind[v]++; 
	}
	for(int i=1;i<=n;i++) sort(g[i].begin(),g[i].end());
	int flag=0,cnt1,cnt2;
	for(int i=1;i<=n;i++){
		if(ind[i]!=outd[i]){
			flag=1;
			if(outd[i]-ind[i]==1){
				cnt1++;
				s=i;
			}else if(ind[i]-outd[i]==1) cnt2++;
			else{
				printf("No");
				return 0;
			}
		}
	}
	if(flag&&!(cnt1==cnt2&&cnt2==1)){
		printf("No");
		return 0;
	}
	dfs(s);
	for(int i=cnt;i>=1;i--) printf("%d ",r[i]);
	return 0;
}

Luogu P1341 无序字母对

转化一下,即以字母为节点,无序字母对间建无向边,询问是否有欧拉路径存在。同样求字典序最小,但此题最多 \(26\) 个字母,可以使用邻结矩阵存储。

#include<iostream>
#include<cstdio>
using namespace std;
int n,minx=60,maxx;
int g[120][120],d[60],r[1010],cnt;
void dfs(int u){
	for(int i=minx;i<=maxx;i++){
		if(g[u][i]){
			g[u][i]=0;g[i][u]=0;
			dfs(i);
		} 
	}
	r[++cnt]=u;
}
int main(){
	scanf("%d",&n); 
	for(int i=1;i<=n;i++){
		char a,b;
		int u,v;
		cin>>a>>b;
		if(a<'a') u=a-'A'+1;
		else u=a-'a'+27;
		if(b<'a') v=b-'A'+1;
		else v=b-'a'+27;
		g[u][v]=g[v][u]=1;
		minx=min(minx,min(u,v));
		maxx=max(maxx,max(u,v));
		d[u]++;d[v]++;
	}
	int s,cnto=0;
	bool flag1=0,flag2=0;
	for(int i=minx;i<=maxx;i++){
		if(d[i]%2){
			flag1=1;
			if(!flag2){
				s=i;
				flag2=1;
			} 
			cnto++;
		} 
	}
	if(flag1){
		if(cnto==2) dfs(s);
		else{
			printf("No Solution");
			return 0;
		} 
	} 
	else dfs(minx);
	if(cnt!=n+1) printf("No Solution");
	for(int i=cnt;i>=1;i--){
		char c;
		if(r[i]<27) c='A'+r[i]-1;
		else c='a'+r[i]-27;
		printf("%c",c);
	}  
	return 0;
}

Luogu P2731 [USACO3.3] 骑马修栅栏 Riding the Fences

无向图版本。

唯一的不同是要防止边被重复走,走到一条边时要将其删除。

#include<iostream>
#include<cstdio>
using namespace std;
const int maxn=510;
int m,minu=520,maxu;
int d[maxn],r[maxn],s,cnt;
int g[maxn][maxn];
void dfs(int u){
	for(int i=minu;i<=maxu;i++){
		if(g[u][i]){
			g[u][i]--,g[i][u]--;
			dfs(i);
		}
	}
	r[++cnt]=u;
}
int main(){
	scanf("%d",&m);
	for(int i=1;i<=m;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		g[u][v]++,g[v][u]++;
		d[u]++,d[v]++; 
		minu=min(minu,min(u,v));
		maxu=max(maxu,max(u,v));
	}
	int flag=0;
	for(int i=minu;i<=maxu;i++){
		if(d[i]%2){
			flag=1;
			s=i;
			break;
		}
	}
	if(flag) dfs(s);
	else dfs(minu);
	for(int i=cnt;i>=1;i--) printf("%d\n",r[i]);
	return 0;
}

Luogu P1127 词链

在有向图上输出欧拉路径上的边。

字符串的处理与 UVA10129 类似,为字符串首和尾字符间建边。对于字典序限制,只需要为所有字符串和每个点出边排序,DFS 时按顺序遍历。

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
const int M=50;
int n,s=-1;
int ind[M],outd[M];
int fa[M],d[M];
int st[N],top;
string wd[N];
struct Node{
    int to,id;
};
vector<Node> g[N];
int Find(int x){
    if(fa[x]==x) return x;
    else return fa[x]=Find(fa[x]);
}
bool check(){
    int p=-1;
    for(int i=0;i<26;i++){
        if(!ind[i]&&!outd[i]) continue;
        if(Find(i)!=p&&(p!=-1)) return 0;
        p=fa[i];
    }
    int cnts=0,cnte=0;
    for(int i=0;i<26;i++){
        if(ind[i]==outd[i]) continue;
        if(outd[i]-ind[i]==1){
            cnts++;
            if(s==-1) s=i; 
        } 
        else if(ind[i]-outd[i]==1) cnte++;
        else return 0;
    }
    return (!cnts&&!cnte)||(cnts==1&&cnte==1);
}
void dfs(int u){
    for(int i=d[u];i<g[u].size();i=d[u]){
        d[u]=i+1;
        dfs(g[u][i].to);
        st[++top]=g[u][i].id;
    }
}
bool cmp(Node x,Node y){
    return x.id<y.id;
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    for(int i=0;i<26;i++) fa[i]=i;
    for(int i=1;i<=n;i++) cin>>wd[i];
    sort(wd+1,wd+1+n);
    for(int i=1;i<=n;i++){
        int u=wd[i][0]-'a',v=wd[i][wd[i].size()-1]-'a';
        g[u].push_back((Node){v,i});
        ind[v]++,outd[u]++;
        u=Find(u),v=Find(v);
        if(u!=v) fa[u]=v;
    }
    if(!check()) cout<<"***";
    else{
        if(s==-1){
            for(int i=0;i<26;i++){
                if(ind[i]||outd[i]){
                    s=i;
                    break;
                }
            }
        }
        for(int i=0;i<26;i++) sort(g[i].begin(),g[i].end(),cmp);
        dfs(s);
        while(top){
            cout<<wd[st[top]];
            top--;
            if(top) cout<<'.';
        }
    }
    return 0;
}

应用 - 图论建模

Codeforces 547D Mike and Fish

题目中要求同一列红点和蓝点数量之差不超过 \(1\)。动用人类智慧,想到以横纵坐标为点,原题的点为边,原题点颜色为边的方向。于是题目就转化为:给定无向图,确定每条边的方向,使得每个点的入度、出度差不超过 \(1\)

不妨先考虑弱化,若每个点都为偶数度,那么在原图上构造有向图欧拉回路,所有点的入度就都会等于出度。然后考虑有奇点怎么办。不难想到,我们要把奇点的入度、出度差构造为 \(1\)。我们把奇点两两配对连边,使其全部变为偶点,此时可以构造出欧拉回路,所有的点入度等于出度,那么原先的奇点要么多一条出边,要么多一条入边,入度和出度的差就为 \(1\)

构造欧拉回路的话,维护每个点是否已经过,为走过的边标记一个方向。走完所有节点后,要回到起点,由于题目保证有解,剩下的边就都是反向,用于走回起点。

注意,这种构造方法需要走完一条边删除一条边,才能保证时间复杂度为 \(O(m)\)\(m\) 为边数)。具体实现很容易,只需要在遍历时同步更改每个节点在邻接表中的头指针。

时间复杂度 \(O(n)\)

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int n;
int cnt[N],deg[4*N];;
int h[4*N],tot=1; 
bool vise[4*N],visn[4*N],ans[4*N];
struct Node{
    int to,nxt;
}e[4*N];
void Add(int u,int v){
    tot++;
    e[tot].to=v;
    e[tot].nxt=h[u];
    h[u]=tot;
}
void dfs(int u){
    visn[u]=1;
    for(int& i=h[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(vise[i]) continue;
        ans[i]=vise[i]=vise[i^1]=1;
        dfs(v);
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++){
        int x,y;
        cin>>x>>y;
        y+=N;
        cnt[i]=tot+2;
        deg[x]++,deg[y]++;
        Add(x,y),Add(y,x);
    }
    int lstu=0;
    for(int i=1;i<=2*N;i++){
        if(deg[i]%2){
            if(!lstu) lstu=i;
            else{
                Add(i,lstu),Add(lstu,i);
                lstu=0;
            }
        }
    }
    for(int i=1;i<=2*N;i++){
        if(!visn[i]) dfs(i);
    }
    for(int i=1;i<=n;i++){
        if(ans[cnt[i]]) cout<<'b';
        else cout<<'r';
    }
    return 0;
}

Codeforces 527E/528C Data Center Drama

注意到题目要求构造有向图使每个点的出入度都是偶数,即图在定向前每个点度都是偶数,即存在无向图欧拉回路。所以像上个题一样把奇点两两相连。

但存在欧拉回路的图并不都满足定向后每个点的出入度都是偶数。因为一条边会贡献一个出度和一个入度,所以若每个点出入度都是偶数,总边数也必须是偶数。所以若处理完上一步后总边数是奇数,就要随便找一个点连一个自环。

因为所有新增的边都是必要的,所以这样构造边一定是最少的。

然后考虑怎么定向。我们要在访问每个节点的时候为他的入度或出度 \(+2\),那么他的上下相邻两边要么都是出边,要么都是入边。于是我们在搜索时隔一条边换一个方向,即以 \(a→b←c→d←...→a\) 的方式构造。

时间复杂度 \(O(m)\)

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m,idx;
int h[N],tot=1;
int deg[N];
bool vis[8*N];
struct Node{
    int to,nxt;
}e[8*N];
void Add(int u,int v){
    tot++;
    e[tot].to=v;
    e[tot].nxt=h[u];
    h[u]=tot;
}
void dfs(int u){
    for(int& i=h[u];i;i=e[i].nxt){
        if(vis[i]) continue;
        vis[i]=vis[i^1]=1;
        int v=e[i].to;
        dfs(v);
        if(idx&1) cout<<v<<' '<<u<<'\n';
        else cout<<u<<' '<<v<<'\n';
        idx++;
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int u,v;
        cin>>u>>v;
        deg[u]++,deg[v]++;
        Add(u,v),Add(v,u);
    }
    int las=-1;
    for(int i=1;i<=n;i++){
        if(deg[i]&1){
            if(las==-1) las=i;
            else Add(las,i),Add(i,las),las=-1,m++;
        }
    }
    if(m&1) Add(1,1),m++;
    cout<<m<<'\n';
    dfs(1);
    return 0;
}
posted @ 2026-01-07 23:19  Seqfrel  阅读(89)  评论(0)    收藏  举报