欧拉回路路径求解

基本概念:
今天讨论的主题是一类问题,就是欧拉路问题。有两种欧拉路。第一种叫做 Eulerian path(trail),沿着这条路径走能够走遍图中每一条边;第二种叫做 Eularian cycle,沿着这条路径走,不仅能走遍图中每一条边,而且起点和终点都是同一个顶点。注意:欧拉路要求每条边只能走一次,但是对顶点经过的次数没有限制。

满足什么性质的图才能有欧拉路?根据 wikipedia 对欧拉路的介绍:

在无向图中,所有顶点的度数均为偶,则存在 Eularian cycle;若有且仅有两个顶点的度数为奇,其余的都为偶,则存在 Eularian path;
在有向图中,所有顶点的入度数等于出度数,则存在 Eularian cycle;若有且仅有两个顶点:其中一个入度数比出度数大 1,另一个入度数比出度数小 1,其余的顶点入度数等于出度数,则存在 Eularian path.
另外我们还需要知道,对于那些 Eularian path,起点和终点分别在那两个度数为奇的顶点上(对于无向图)或是入度数不等于出度数的顶点上(对于有向图)。

Fleury算法:
然而知道这些并没有给我们带来多少实惠。因为我们除了判定一个图有没有欧拉路之外,更想找到其中的一条欧拉路径。于是这就是我们今天的重点:寻找欧拉路径的算法。
Fleury算法的思想是:能不走桥就尽量不走桥。(桥:在未被走过的边集中,如果去掉某条边使得剩下的图不连通,则称这条边为桥。)
一个比较经典的算法是 Fleury 算法。Fleury 算法的思想就是:在过河拆桥之前,先想想有没有退路。为什么这么说?Fleury 算法每个回合进行到一个顶点上的时候,都会删除已经走过的边。在选择下一条边的时候,不应该出现这样的状况:在删除下一条边之后,连通图被分割成两个不连通的图。除非没有别的边可选择。该算法从一个奇度数顶点开始(若所有顶点度数均为奇,则任选一个顶点)。当所有的边都走完的时候,该算法结束,欧拉路径为删除路径的顺序。用算法伪代码描述就是:

v_0 <- a vertex with odd degree or, if no such vertex, any arbitrary vertex.
Repeat:
    select an vertex v_i+1 adjacent of v_i, which should not separate the graph or, the only adjacent vertex of v_i
    remove edge <v_i, v_i+1> and jump to v_i+1
Until all edges have been visited.
Return the sequence of visited edges.

但是该算法的问题就是,怎么判断一条边是否是一个桥呢?如果使用 Tarjan 算法判断,则算法运行时间就是 O(E2) 。在实际写代码的时候,我可没考虑那么多。我只考虑,如果在某一点处深搜的结果导致图被分离,那么在某一个边必然走过了一个桥,那么就返回走另一条边。这样的思想形成的算法如下:

include <cstdio>
#include <stack>
#include <iostream>
#include <string>

using namespace std;

int G[1001][1001];
int N,M;
stack<int> S;

bool dfs(int u){ //返回的状态说明是否走过了一个桥
    S.push(u); //进入某一节点时推入节点,如果误入歧途还要负责弹出。
    if(G[u][0]==0){ //G[u][0]代表该节点所邻接的边的数目。此段代码判断是否走完了所有边,或者没有走完。
        bool flag=true;
        for(int i=1; i<=N; i++){
            if (i==u) continue;
            flag=((G[i][0]==0) && flag);
        }
        if(flag==false){
            S.pop();
        }
        return flag;
    }

    for(int v=1; v<=N; v++)
        if(G[u][v]){
            //删除边
            G[u][v]-=1;
            G[v][u]-=1;
            G[v][0]-=1;
            G[u][0]-=1;
            if(dfs(v)) return true;
            else{//撤销删除边
                G[u][v]+=1;
                G[v][u]+=1;
                G[v][0]+=1;
                G[u][0]+=1;
            }
        }

    S.pop();
    return false;
}


int main(){
    freopen("testcase", "r", stdin);
    cin>>N>>M;
    int u,v;
    for(int i=0; i!=M; i++){
        cin>>u>>v;
        G[u][v]+=1;
        G[v][u]+=1;
        G[u][0]+=1;
        G[v][0]+=1;
    }
    //寻找起点
    for(u=1; u<=N; u++){
        if(G[u][0]&1) break;
    }
    if(u==N+1) dfs(1);
    else dfs(u);
    while(!S.empty()){
        cout<<S.top()<<" ";
        S.pop();
    }
    cout<<endl;
    return 0;
}

粗略分析一下,由于算法要经过每条边,所以时间必然是 Ω(E) 。在最坏情况下,在每个节点处进行一次 DFS,节点会重复走所以以边计算,所以算法复杂度应该是 O(E(E+V))

Hierholzer 算法:
另一种计算欧拉路的算法是 Hierholzer 算法。这种算法是基于这样的观察:
这里写图片描述
在手动寻找欧拉路的时候,我们从点 4 开始,一笔划到达了点 5,形成路径 4-5-2-3-6-5。此时我们把这条路径去掉,则剩下三条边,2-4-1-2 可以一笔画出。

这两条路径在点 2 有交接处(其实点 4 也是一样的)。那么我们可以在一笔画出红色轨迹到达点 2 的时候,一笔画出黄色轨迹,再回到点 2,把剩下的红色轨迹画完。

由于明显的出栈入栈过程,这个算法可以用 DFS 来描述。
如果想看得更仔细一点,下面是从点 4 开始到点 5 结束的 DFS 过程,其中 + 代表入栈,- 代表出栈。
4+ 5+ 2+ 3+ 6+ 5+ 5- 6- 3- 1+ 4+ 2+ 2- 4- 1- 2- 5- 4-
我们把所有出栈的记录连接起来,得到
5-6-3-2-4-1-2-5-4

诸位看官可以自己再选一条路径尝试一下。不过需要注意的是,起始点的选择和 Fleury 要求的一样。
这个算法明显要比 Fleury 高效,它不用判断每条边是否是一个桥。我写的代码如下:

include <cstdio>
#include <stack>
#include <iostream>
#include <string>
#include <vector>

using namespace std;

int G[1001][1001];
int N,M;
stack<int> S;

void dfs(int u){
    for(int v=1; v<=N; v++)
        if(G[u][v]){
            G[u][v]-=1;
            G[v][u]-=1;
            dfs(v);
            //不用恢复边!
        }
    S.push(u);//出栈时记录
}


int main(){
    freopen("testcase", "r", stdin);
    cin>>N>>M;
    int u,v;
    vector<int> cnt(N+1,0);
    for(int i=0; i!=M; i++){
        cin>>u>>v;
        G[u][v]+=1;
        G[v][u]+=1;
        cnt[u]^=1;//利用了异或运算,0表示度为偶数,1表示度为奇数。
        cnt[v]^=1;
    }
    for(u=1; u<=N; u++){
        if(cnt[u]) break;
    }
    if(u==N+1) dfs(1);
    else dfs(u);
    while(!S.empty()){
        cout<<S.top()<<" ";
        S.pop();
    }
    cout<<endl;
    return 0;
}

需要注意的是这个算法时间复杂度是 O(E) 。其在 DFS 的过程中不用恢复边,靠出栈记录轨迹。

  • 16
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值