1. 程式人生 > >尤拉路徑,歐拉回路,並查集

尤拉路徑,歐拉回路,並查集

1.1 定義
對於圖G,若存在一條路徑,經過G中每條邊有且僅有一次,稱這條路為尤拉路徑;如果存在一條迴路經過G每條邊有且僅有一次,稱這條迴路為歐拉回路。具有歐拉回路的圖成為尤拉圖。

1.2 判斷尤拉路徑是否存在的方法

有向圖:圖連通,且只有一個頂點出度大入度1,有一個頂點入度大出度1,其餘都是出度=入度。
無向圖:圖連通,奇度數的結點個數不多於2個,其餘都是偶數度的。

1.3 判斷歐拉回路是否存在的方法

有向圖:圖連通,且所有的頂點出度=入度。
無向圖:圖連通,且所有頂點都是偶數度。

定理一:如果連通無向圖 G 有 2k 個奇頂點(奇定點數必然為偶數個),那麼它可以用 k 筆畫成,並且至少要用 

k 筆畫成。
證明:將這 2k 個奇頂點分成 k 對後分別連起,則得到一個無奇頂點的連通圖。由上知這個圖是一個環,因此去掉新加的邊後至多成為 k 條尤拉路徑,因此必然可以用 k 筆畫成。但是假設全圖可以分為 q 條尤拉路徑,則由定理一知,每條鏈中只有不多於兩個奇頂點,於是 2q \ge 2k。因此必定要 k 筆畫成。

1.4 尤拉路徑與歐拉回路的題目
程式實現一般是如下過程:
(1).利用並查集判斷圖是否連通,即判斷p[i] < 0的個數,如果大於1,說明不連通。
(2).根據出度入度個數,判斷是否滿足要求。
(3).利用DFS輸出路徑。

/*函式過程中是維護一個連通圖點集A,初始時,每個單獨的結點都是一個連通圖,每加入一條邊,則將兩個連通圖合併在一起,組成一個新的連通圖。且對於每個連通圖,都有其代表結點x。陣列p[]用來記錄結點與所在連通圖代表結點之間的關係。*/

#include <iostream>
#include <stdio.h>
using namespace std;
const int maxv = 100 + 2;
int odds[maxv];                 /* 頂點 */
int du[maxv];                   /* 每個頂點的度數 */
int p[maxv];                    /* 並查集陣列 */
bool used[maxv];
int scc[maxv];                  /* scc個數 */
void init(int n)                /*初始化陣列*/
{
    for(int i = 0; i <= n; ++i)
    {
        odds[i] = 0;
        p[i] = -1;
        du[i] = 0;
        used[i] = 0;
    }
}
int utf_find(int x)     /*本函式用來尋找結點x的代表結點。如果x尚未加入到陣列當中或者x即 */
{                       /* 為自己以及其他結點的代表結點,p[x]<0. 此時返 回自己x;*/
    if(0 <= p[x])       /*若結點x已被加入到集合A中且此時x的代表結點不是自己*/
    {                   /* 則遞迴呼叫尋找所在連通圖的代表結點x',特徵是p[x']<0。*/
        p[x] = utf_find(p[x]);
        return p[x];
    }
    return x;
}
void utf_union(int a, int b)  //當加入一條邊的時候,是合併兩個連通圖。
{
    int r1 = utf_find(a);     //尋找新加入邊的兩個結點所在連通圖的代表結點是否一樣,
    int r2 = utf_find(b);     //一樣的話則為“冗餘邊”;否則即是將兩個連通圖連通在一塊。
    if(r1 == r2)
        return;
    int n1 = p[r1];           //每個連通圖中結點的個數,連通圖A1中的結點個數為|n1|,連通圖A2中的結點個數為|n2|.
    int n2 = p[r2];		
    if(n1 < n2)               //A1中結點個數大於A2中結點個數
    {                         //將A2中的代表結點,再連結到A1中的代表結點。
p[r2] = r1; //這樣A1,A2中結點的代表結點將都是A1的代表結點。 p[r1] += n2; //實現兩個連通圖的合併.更新新的連通圖A1中的結點個數。 } else { p[r1] = r2; //同上。 p[r2] += n1; } } int main() { int n = 0; int m = 0; int a = 0; int b = 0; int i = 0; int cnt = 0; while(scanf("%d%d", &n, &m) != EOF) { init(n); cnt = 0; for(i = 1; i <= m; ++i) //本段為加入邊,並對邊做並查集歸併。具體如上所示。 { scanf("%d%d", &a, &b); du[a]++; du[b]++; utf_union(a, b); } for(i = 1; i <= n; ++i) { int f = utf_find(i); if(!used[f]) //檢查圖中的連通圖個數 { used[f] = 1; scc[cnt++] = f; } if(1 == du[i]%2) //檢查每個連通圖中度數為奇數的結點個數 odds[f]++; } int ret = 0; for(i = 0; i < cnt; ++i) //對每個連通圖做判斷 { if(0 == du[scc[i]]) //如果連通圖是單個結點,則繼續。 continue; if(0 == odds[scc[i]]) //否則如果連通圖中度數為為奇數的結點個數為0,則只需 ++ret; //一筆或者或者只需一條路徑就能遍歷所有的邊 else //若連通圖中度數為奇數的節點個數不為0, ret += odds[scc[i]]/2;//設為2k,則需要k條路徑才能遍歷所有的邊。 } printf("%d\n", ret); //函式最後返回遍歷所有的邊需要的路徑數 } return 0; }

P2: 給出n個單詞,如果一個單詞的尾和另一個單詞的頭字元相同,那麼這兩個單詞可以相連。求這n個單詞是否可以排成一列。應用尤拉圖,構圖:一個單詞的頭尾字幕分別作為頂點,每輸入一個word,則該word的頭指向word的尾有一條邊。記錄每個頂點的出入度。利用並查集先判斷是否為SCC。如果是的話再判斷奇數度節點為0個或者2個。

P3: 給出n個field以及m條邊連線這m個field,要求遍歷每條邊2次。求這樣的一條路徑。
若該field組成的圖是連通分量,那麼由於每條邊遍歷2次,即等效的每個結點的度均為偶數。故必然存在歐拉回路。此時只需對原圖作一次DFS,即可得到這樣的路徑。需要記錄下每次訪問的結點順序,重複訪問的也需打印出來。

P4: 給出一組單詞,如果一個單詞的尾和另一個單詞的頭字元相同,那麼這兩個單詞可以相連。問這組單詞能否排成一排,如果可以求出字典序最小的那個。構圖:單詞作為邊,單詞的首字元和尾字元作為結點。讀入一個單詞即新增一條邊。用鄰接連結串列法存邊。首先判斷圖是否連通,如果是,再判斷該圖能否構成尤拉路徑或者回路。如果是迴路,從字元'a'開始尋找,找到第一個存在的字母,從這個字母遍歷就行。如果是路徑,那麼必須從出度大於入度的那個結點開始訪問。由於這個題目要求的是最小字典序,然後考慮我們建立鄰接表時是採用頭插法,那麼我們將單詞從小到大排序,由於我們遍歷結點肯定是從小的結點開始的,這樣遍歷一個結點的鄰接邊的時候就會從大到小訪問這個結點的邊,無法滿足字典最小。所以從大到小排序,遍歷依node陣列為準,每次遍歷一條邊設定下標表示已訪問過。