欧拉路径与欧拉回路

感觉这一块网上说的有点乱,很多东西都没有说清楚,或者都缺一些东西,所以在这里打算好好的总结与归纳一下关于欧拉路径与欧拉回路的问题。

概念

欧拉路径:从某一起点开始,可以沿某路径遍历图中每一条边一次且仅一次,则称此路径为欧拉路径
欧拉回路:若欧拉路径中的起点和终点相同,则其为欧拉回路

一般情况下,如果一个图是由欧拉回路构成的,我们则称之为欧拉图。否则,当其是由欧拉路径构成的话,我们就称之为半欧拉图。

充要条件关系:

欧拉回路:所有点的满足:入度 = 出度,那么沿任意一点为起点,一定存在方案形成欧拉回路。

欧拉路径:有仅有一个起点:入度 = 出度 - 1,有且仅有一个终点:出度 = 入度 - 1,。其他的点都满足:入度 = 出度。则我们从起点开始遍历即可。

细心的读者可能会发现,如果我们在欧拉路径上画一条由终点指向起点的路径的话,那么欧拉路径就会变成欧拉回路,所以我们可以看出他们事实上很容易相互转化的。

所以欧拉路径与欧拉回路除了在判断其充要条件时略有不用外,他们的求法是一样的,所以我们这里以欧拉路径为例。

无向图&有向图

Fleury + dfs

最简单也是最基础的做法。
这里采用邻接表存放图,在优化中大家就会发现这样有意想不到的好处。

@Frosero
#include <cstdio>
#include <cstring>
#include <stack>
#define MAXN 100010

using namespace std;

struct N{       //邻接表存放图会有意想不到的好处
    int v,nex;
    bool vis;
};
N edge[MAXN];
int fir[MAXN],tot;

void add_edge(int u,int v){
    edge[tot].v = v;
    edge[tot].vis = false;
    edge[tot].nex = fir[u];
    fir[u] = tot++;
}

stack<int>stk;
int ans[MAXN],cnt;

void dfs(int s){        //Fluery寻找增广路
    stk.push(s);
    for(int i=fir[s];i!=-1;i=edge[i].nex)if(!edge[i].vis){
        edge[i].vis = true;
        dfs(edge[i].v);
        break;
    }
}

void Fluery(int s){      //Fluery主过程
    while(!stk.empty()) stk.pop(); stk.push(s);
    while(!stk.empty()){
        int u =  stk.top(); stk.pop();
        bool flag = false;
        for(int i=fir[u];i!=-1;i=edge[i].nex)if(!edge[i].vis){
            flag = true;    //已检测到存在增广路
            break;
        }
        if(flag) dfs(u);
        else ans[--cnt] = u;    //注意要逆序存放,才能正序输出
    }
}

int n,m;    //n为点的数量,从1开始计数,m为边的数量
int is[MAXN],os[MAXN];  //is代表入度,os代表出度

int main(){
    //freopen("in.in","r",stdin);
    //freopen("out.txt","w",stdout);
    while(scanf("%d %d",&n,&m)!=EOF){
        memset(fir,-1,sizeof(fir));
        memset(is,0,sizeof(is));
        memset(os,0,sizeof(os));
        tot = 0;
        int u,v;
        for(int i=0;i<m;i++){
            scanf("%d %d",&u,&v);
            add_edge(u,v);
            os[u]++; is[v]++;
        }
        int s = 1,t = 1,s_cnt = 0,t_cnt = 0;    //s为起点,t为终点
        bool flag = true;       //s_cnt为适合做起点的点数,t_cnt为适合做终点的点数
        for(int i=1;i<=n;i++){
            if(is[i] + 1 == os[i]){
                s = i;
                s_cnt++;
            }
            else if(is[i] == os[i] + 1){
                t = i;
                t_cnt++;
            }               //若一个点入度和出度相差2及以上,则不可能存在欧拉路径
            else if(is[i] != os[i]) flag = false;
        }                                               //欧拉路径存在的充要条件
        if(flag && ((s_cnt == 1 && t_cnt == 1) || (s_cnt + t_cnt == 0))){   
            cnt = m + 1;    //m条边则一定且只经历m+1个点,否则也不是欧拉路径
            Fluery(s);
            if(cnt != 0) printf("No Eulers !\n");
            else{
                for(int i=0;i<m+1;i++) printf("%d ",ans[i]); printf("\n");
            }
        }
        else printf("No Eulers !\n");
    }

    return 0;
}

Fleury + bfs

然而上述算法适用性及其有限,其中一点就是:dfs过深导致爆栈

为解决此问题,我们可以把dfs过程用bfs来写。

代码如下:

@Frosero
#include <cstdio>
#include <cstring>
#include <stack>
#define MAXN 100010

using namespace std;

struct N{       //邻接表存放图会有意想不到的好处
    int v,nex;
    bool vis;
};
N edge[MAXN];
int fir[MAXN],tot;

void add_edge(int u,int v){
    edge[tot].v = v;
    edge[tot].vis = false;
    edge[tot].nex = fir[u];
    fir[u] = tot++;
}

stack<int>stk;
int ans[MAXN],cnt;

void bfs(int s){        //Fluery寻找增广路,注意这里改成了bfs
    stk.push(s);
    while(true){
        bool flag = false;
        for(int i=fir[s];i!=-1;i=edge[i].nex)if(!edge[i].vis){
            edge[i].vis = true;
            flag = true;
            s = edge[i].v;
            stk.push(s);
            break;
        }
        if(!flag) break;
    }
}

void Fluery(int s){      //Fluery主过程
    while(!stk.empty()) stk.pop(); stk.push(s);
    while(!stk.empty()){
        int u =  stk.top(); stk.pop();
        bool flag = false;
        for(int i=fir[u];i!=-1;i=edge[i].nex)if(!edge[i].vis){
            flag = true;    //已检测到存在增广路
            break;
        }
        if(flag) bfs(u);
        else ans[--cnt] = u;    //注意要逆序存放,才能正序输出
    }
}

int n,m;
int is[MAXN],os[MAXN];  //is代表入度,os代表出度

int main(){
    //freopen("in.in","r",stdin);
    //freopen("out.txt","w",stdout);
    while(scanf("%d %d",&n,&m)!=EOF){
        memset(fir,-1,sizeof(fir));
        memset(is,0,sizeof(is));
        memset(os,0,sizeof(os));
        tot = 0;
        int u,v;
        for(int i=0;i<m;i++){
            scanf("%d %d",&u,&v);
            add_edge(u,v);
            os[u]++; is[v]++;
        }
        int s = 1,t = 1,s_cnt = 0,t_cnt = 0;    //s为起点,t为终点
        bool flag = true;       //s_cnt为适合做起点的点数,t_cnt为适合做终点的点数
        for(int i=1;i<=n;i++){
            if(is[i] + 1 == os[i]){
                s = i;
                s_cnt++;
            }
            else if(is[i] == os[i] + 1){
                t = i;
                t_cnt++;
            }               //若一个点入度和出度相差2及以上,则不可能存在欧拉路径
            else if(is[i] != os[i]) flag = false;
        }                                               //欧拉路径存在的充要条件
        if(flag && ((s_cnt == 1 && t_cnt == 1) || (s_cnt + t_cnt == 0))){   
            cnt = m + 1;    //m条边则一定且只经历m+1个点,否则也不是欧拉路径
            Fluery(s);
            if(cnt != 0) printf("No Eulers !\n");
            else{
                for(int i=0;i<m+1;i++) printf("%d ",ans[i]); printf("\n");
            }
        }
        else printf("No Eulers !\n");
    }

    return 0;
}

优化

然而自习分析算法复杂度可得知,时间复杂度是为O(VE)的。
在Fluery的主过程中,我们在不断寻找是否存在增广路时重复搜索已标记vis = true 的边,因此我们只要利用邻接表的“删除边”的功能,就能把其复杂度降为O(E)

代码如下:

@Frosero
#include <cstdio>
#include <cstring>
#include <stack>
#define MAXN 100010

using namespace std;

struct N{       //邻接表存放图会有意想不到的好处
    int v,nex;  //注意这里不再使用vis来标记
};
N edge[MAXN];
int fir[MAXN],tot;

void add_edge(int u,int v){
    edge[tot].v = v;
    edge[tot].nex = fir[u];
    fir[u] = tot++;
}

stack<int>stk;
int ans[MAXN],cnt;

void bfs(int s){        //Fluery寻找增广路,注意我们改的还是这里  ^*_*^....嘿嘿
    stk.push(s);
    while(true){
        bool flag = false;
        int las = -2;
        for(int i=fir[s];i!=-1;i=edge[i].nex){
            flag = true;
            if(las == -2) fir[s] = edge[i].nex;
            else edge[s].nex = edge[i].nex;
            s = edge[i].v;
            stk.push(s);
            las = i;
            break;
        }
        if(!flag) break;
    }
}

void Fluery(int s){      //Fluery主过程
    while(!stk.empty()) stk.pop(); stk.push(s);
    while(!stk.empty()){
        int u =  stk.top(); stk.pop();
        bool flag = false;
        for(int i=fir[u];i!=-1;i=edge[i].nex){
            flag = true;    //已检测到存在增广路
            break;
        }
        if(flag) bfs(u);
        else ans[--cnt] = u;    //注意要逆序存放,才能正序输出
    }
}

int n,m;
int is[MAXN],os[MAXN];  //is代表入度,os代表出度

int main(){
    //freopen("in.in","r",stdin);
    //freopen("out.txt","w",stdout);
    while(scanf("%d %d",&n,&m)!=EOF){
        memset(fir,-1,sizeof(fir));
        memset(is,0,sizeof(is));
        memset(os,0,sizeof(os));
        tot = 0;
        int u,v;
        for(int i=0;i<m;i++){
            scanf("%d %d",&u,&v);
            add_edge(u,v);
            os[u]++; is[v]++;
        }
        int s = 1,t = 1,s_cnt = 0,t_cnt = 0;    //s为起点,t为终点
        bool flag = true;       //s_cnt为适合做起点的点数,t_cnt为适合做终点的点数
        for(int i=1;i<=n;i++){
            if(is[i] + 1 == os[i]){
                s = i;
                s_cnt++;
            }
            else if(is[i] == os[i] + 1){
                t = i;
                t_cnt++;
            }               //若一个点入度和出度相差2及以上,则不可能存在欧拉路径
            else if(is[i] != os[i]) flag = false;
        }                                               //欧拉路径存在的充要条件
        if(flag && ((s_cnt == 1 && t_cnt == 1) || (s_cnt + t_cnt == 0))){   
            cnt = m + 1;    //m条边则一定且只经历m+1个点,否则也不是欧拉路径
            Fluery(s);
            if(cnt != 0) printf("No Eulers !\n");
            else{
                for(int i=0;i<m+1;i++) printf("%d ",ans[i]); printf("\n");
            }
        }
        else printf("No Eulers !\n");
    }

    return 0;
}

到此,欧拉路径及欧拉回路知识基础就学好了,可以进军更复杂的混合图等啦~~