【笔记】欧拉图
以上是图论经典问题:科尼斯堡七桥问题,由欧拉于 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;
}
构造
构造欧拉路径,输出点:
- 确定起点 \(S\)。
- 从 \(S\) 开始 DFS。
- 走一条边删一条边,直到无路可走,入栈。
- 倒序输出栈内节点。
DFS 伪代码:
void dfs(int u){
枚举 u 的出边
若该边未访问
标记为已访问
dfs(边指向的另一个点)
u 入栈
}
对于欧拉回路,根据拼环的思想,每个点在搜索过程中必将回到自身(也可用度数证明:到达一个新点 \(v\),因为每个点度数都是偶数,减去走到 \(v\) 的那条边,剩余点度为奇数,因此必定存在出边),此时上一条出边已经被删除,会去走他的下一条出边。不断重复这个过程,直到此点的出边已经被走完,并压入栈中。
对于欧拉路径,终点度为奇数,会先找不到出边(奇数 \(-1\) 可能等于 \(0\)),必然会最先无路可走,所以会最先入栈。
Luogu P7771【模板】欧拉路径
有向图,输出点。
所以我们可以用一个数组 \(\Delta u\) 记录上次访问到的 \(u\) 的出边号,相当于删除边。
由于要求要按最小字典序输出,所以要对出边从小到大排序,因此使用 vector 存储。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m,s=1;
int ind[N],outd[N];
int d[N],st[2*N],top;
vector<int> G[N];
bool check(){
int cnts=0,cnte=0;
for(int i=1;i<=n;i++){
if(ind[i]-outd[i]==1) cnte++;
else if(outd[i]-ind[i]==1) cnts++,s=i;
else if(outd[i]!=ind[i]) return 0;
}
if((cnts==1&&cnte==1)||(!cnts&&!cnte)) return 1;
else return 0;
}
void dfs(int u){
for(int i=d[u];i<G[u].size();i=d[u]){
d[u]=i+1;
dfs(G[u][i]);
}
st[++top]=u;
}
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;
G[u].push_back(v);
ind[v]++,outd[u]++;
}
for(int i=1;i<=n;i++) sort(G[i].begin(),G[i].end());
if(!check()){
cout<<"No";
return 0;
}
dfs(s);
for(int i=top;i>0;i--) cout<<st[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;
}
应用
P3520 [POI 2011] SMI-Garbage
若两环有公共边,则可以去除公共边,合并后成为一个简单环。也就是说,对于状态不变的边,可以直接删掉不管。问题转变为给出一个方案用若干个环覆盖每条边恰好一次。
因为欧拉回路就是若干个环的不交并,所以这就是欧拉回路问题。对于欧拉回路中重复出现的点,之间一定形成一个环,因此在栈中弹出之间的点,即可拆成环。因为同一个点可能回到自身若干次,所以不能弹出这个点。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
const int M=1e6+10;
int n,m,k;
int h[N],tot=1;
int deg[N],st[2*N],top;
bool visn[N],vise[2*M],stvis[N];
vector<int> ans[2*M];
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){
visn[u]=1;
for(int &i=h[u];i;i=e[i].nxt){
if(vise[i]) continue;
int v=e[i].to;
vise[i]=vise[i^1]=1;
dfs(v);
}
if(stvis[u]){
ans[++k].push_back(u);
while(top&&st[top]!=u){
ans[k].push_back(st[top]);
stvis[st[top]]=0;
top--;
}
ans[k].push_back(st[top]);
}else{
st[++top]=u;
stvis[u]=1;
}
}
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,s,t;
cin>>u>>v>>s>>t;
if(s^t){
Add(u,v),Add(v,u);
deg[u]++,deg[v]++;
}
}
for(int i=1;i<=n;i++){
if(deg[i]&1){
cout<<"NIE";
return 0;
}
}
for(int i=1;i<=n;i++){
if(!visn[i]) dfs(i);
}
cout<<k<<'\n';
for(int i=1;i<=k;i++){
cout<<ans[i].size()-1<<' ';
for(int j=0;j<ans[i].size();j++) cout<<ans[i][j]<<' ';
cout<<'\n';
}
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;
}
Codeforces 547D Mike and Fish
题目中要求同一列红点和蓝点数量之差不超过 \(1\)。动用人类智慧,想到以横纵坐标为点,原题的点为边,原题点颜色为边的方向。于是题目就转化为:给定无向图,确定每条边的方向,使得每个点的入度、出度差不超过 \(1\)。
不妨先考虑弱化,若每个点都为偶数度,那么在原图上构造有向图欧拉回路,所有点的入度就都会等于出度。然后考虑有奇点怎么办。不难想到,我们要把奇点的入度、出度差构造为 \(1\)。我们把奇点两两配对连边,使其全部变为偶点,此时可以构造出欧拉回路,所有的点入度等于出度,那么原先的奇点要么多一条出边,要么多一条入边,入度和出度的差就为 \(1\)。
像之前一样构造无向图欧拉回路,每个节点从上次没走的边开始走,走完一条边删除正反向边。不过因为我们只需要为边定向,所以不用走完所有边,只需要边走边定向,走完所有节点后,因为要回到起点,剩下没走过的边就必须是反向,无需再去走。
时间复杂度 \(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;
}


浙公网安备 33010602011771号