【算法】欧拉回路和欧拉路径

· · 个人记录

欧拉回路和欧拉路径是一种判断一笔画问题的算法,回路是从源点出发后可以遍历过所有的边且经过一次后可以回到原点,路径是从原点出发后遍历所有的边且经过一次后可以到达终点

实现欧拉回路首先需要判断是否存在欧拉回路。

以上图为例,在因为欧拉回路是重新回到了源点,也说明欧拉回路必须至少有一个环,并且没有一条边会被孤立。所以这也就说明每个点的出度和入度和必须成对,不然绝对会有边遍历不到。

对于无向图来说,也就是度数为偶数。源点的选择也就是它有边连接,因为可能会出现孤立点的情况。

然后就是如何求欧拉回路。其实欧拉回路就是一个爆搜的过程(没有任何技术含量)。

每次枚举每个点的出边,如果这条边没有遍历过就继续搜,无向图需要将正反两条边都干掉,再每次记录走过的边。

还需注意跑完 dfs 后,如果欧拉回路的边的数量没有和所有边的数量一样,也就是说没有遍历所有的边,也不是一个欧拉回路。

#include<bits/stdc++.h>
using namespace std;
struct edge{
    int to,next;
}ed[1000001];
int he[1000001],idx;
int din[1000001],dout[1000001];
int ans[1000001],cnt;
bool vis[1000001];
int n,m,t;
void insert(int u,int v)
{
    ed[idx].to=v;
    ed[idx].next=he[u];
    he[u]=idx++;
}
void dfs(int u)
{
    for(int i=he[u];i!=-1;i=ed[i].next)
    {
        if(vis[i]) continue;
        vis[i]=1;
        if(t==1) vis[i^1]=1;//双向 
        dfs(ed[i].to);
        if(t==1) ans[++cnt]=(i>>1)+1;//点的编号
        else ans[++cnt]=i+1;
    }
}
int main()
{
    memset(he,-1,sizeof(he));
    scanf("%d %d %d",&t,&n,&m);//1是无向图,2是有向图 
    for(int i=1;i<=m;i++)
    {
        int x,y;
        scanf("%d %d",&x,&y);
        insert(x,y);
        if(t==1) insert(y,x);
        din[y]++;dout[x]++;
    }
    if(t==1)
    {
        for(int i=1;i<=n;i++)
            if((din[i]+dout[i])&1)//度数为偶数 
            {
                puts("NO");
                return 0;
            }
    }
    else
    {
        for(int i=1;i<=n;i++)
            if(din[i]!=dout[i])//出度入度成对 
            {
                puts("NO");
                return 0;
            }
    }
    for(int i=1;i<=n;i++)
        if(he[i]!=-1)//有边连接 
        {
            dfs(i);
            break;
        }
    if(cnt<m)//没有遍历所有的边 
    {
        puts("NO");
        return 0;
    }
    puts("YES");
    for(int i=1;i<=cnt;i++) printf("%d ",ans[i]);
    return 0;
}

首先还是需要判断这个图是否存在欧拉路径。

因为没有求是否最后需要回到源点。所以对于无向图而言,至多有两个点度数可以不为偶数,并且这两个点分别是源点终点

有向图也是一样,但还需满足源点只出不进,也就是出度比入度大 1,终点只进不出,也就是入度比出度1 。还需要记得,欧拉回路是一种特殊的欧拉路径,所以如果满足欧拉回路的性质,它也必然存在一条欧拉路径。

和欧拉回路十分相近,只是在判别的时候不太一样,实现方法还是 dfs,以下代码以有向图为例不会告诉你我不想写无向图了

#include<bits/stdc++.h>
using namespace std;
struct edge{
    int to,next;
}ed[1000001];
struct node{
    int u,v;
}a[1000001];
int he[1000001],idx=1;
int din[1000001],dout[1000001];
bool vis[1000001];
int ans[1000001],cnt;
int n,m;
void insert(int u,int v)
{
    ed[idx].to=v;
    ed[idx].next=he[u];
    he[u]=idx++;
}
void dfs(int u)//寻找欧拉路径
{
    for(int i=he[u];~i;i=ed[i].next)
        if(!vis[i])
        {
            vis[i]=1;
            dfs(ed[i].to);
        }
    ans[++cnt]=u;
    return ;
}
int main()
{
    memset(he,-1,sizeof(he));
    int s=0,cnta=0,cntb=0;
    scanf("%d %d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int x,y;
        scanf("%d %d",&x,&y);
        insert(x,y);
        din[y]++;dout[x]++;
    }
    for(int i=1;i<=n;i++)
    {
        if(din[i]-dout[i]>1||dout[i]-din[i]>1)//有两个以上差相差2的点
        {
            puts("No");
            return 0;
        }
        if(dout[i]-din[i]==1) cnta++,s=i;//找原点
        else if(din[i]-dout[i]==1) cntb++;
    }
    if(!s)//欧拉回路时
    {
        for(int i=1;i<=n;i++)
            if(!(din[i]+dout[i]&1))
            {
                s=i;
                break;
            }
    }
    if((!cnta&&!cntb)||(cnta==1&&cntb==1))//只有原点和终点或为欧拉回路 
    {
        dfs(s);
        for(int i=cnt;i>0;i--) printf("%d ",ans[i]);
    }
    else puts("No");
    return 0;
}

题型

1. 与并查集相结合

有时,我们可以不用输出欧拉回路或欧拉路径的具体路径,而是需要求出其他的问题。比如说,在判断欧拉回路时,图有可能不会连通。这时,我们就可用并查集来维护。

以[POI1999]原始生物为例。

题意

给定 m 个数对 (l,r),现在需要构造出一个序列,使 a_{i}=l,a_{i+1}=r,问这个序列最短为多少?

思路

我们考虑,如果要使序列最短,就要使每个数对在序列中尽可能有重复的数字,即首尾相连。

提到首尾相连,我们会轻易的想到一个构建一个有向图(因为 (l,r)(r,l) 不是一个东西,所以是有向图)。

因为我们要使每个数对都出现在序列当中,所以抽象到图中即为若干个不连通的连通块,我们可以很轻松的联想到多个不连通的欧拉路径(回路),求用最少的欧拉路径画满整个图与所经过的边数相加。

我们先考虑所经过的边数为多少。显然,即为整个图中的边数。

然后再考虑要用多少笔画完,再欧拉回路和欧拉路径两方面分别考虑。

综上所述,答案即为边的数目加奇数点入度出度之差的和除以 2 加不同集合的个数(不包含欧拉回路)。

#include<bits/stdc++.h>
using namespace std;
int f[100001];
int din[100001],dout[100001];
bool vis[100001],is[100001];
map<pair<int,int>,int>mp;
int n,m,sum,ans,rep;
int find(int x)
{
    if(f[x]==x) return x;
    return f[x]=find(f[x]);
}
void merge(int x,int y)
{
    int p=find(x),q=find(y);
    if(p!=q) f[q]=p;
}
int main()
{
    for(int i=1;i<=1000;i++) f[i]=i;
    scanf("%d",&m);
    for(int i=1;i<=m;i++)
    {
        int x,y;
        scanf("%d %d",&x,&y);
        if(!mp.count({x,y}))
        {
            n=max(n,max(x,y));
            merge(x,y);
                is[x]=is[y]=1;
            din[y]++;dout[x]++;
        }
        else rep++;
    }
    for(int i=1;i<=n;i++)
        if((din[i]-dout[i])!=0) 
            vis[find(i)]=1,sum+=abs(din[i]-dout[i]);
    for(int i=1;i<=n;i++)
        if(!vis[i]&&f[i]==i&&is[i]) 
            ans++;
    printf("%d",m+(sum>>1)+ans-rep);
    return 0;
}

总结

当一个有向图不为欧拉图时,我们最少需要奇数点入度出度之差的和除以 2 加不同集合的个数次才可以将这个有向图画完。

当推广到无向图时,就为度数为奇数点的个数除以 2 不同集合的个数次可以将这个无向图画完。

习题

单词游戏 直接用并查集判连通即可

Ant Trip 直接引用结论即可

相框 结论的逆运用