歐拉回路:該回路遍歷了一個圖中所有的邊,並且每條邊只遍歷一次。(一筆畫)
歐拉路徑:從起點開始到終點,遍歷了圖中所有的邊,並且每條邊只遍歷一次。
度數:一個點連接了幾條邊。
入度和出度分別指:進入該點的邊的數量,走出該點的邊的數量。
連通無向圖存在歐拉回路的充要條件:所有點的度數都為偶數。
連通無向圖存在歐拉路徑的充要條件:僅存在兩個度數為奇數的點,其他點的度數都為偶數。(這兩個度數為奇數的點,一個為奇數,一個為偶數)
連通有向圖存在歐拉回路的充要條件:對於所有的點,入度等於出度。
連通有向圖存在歐拉路徑的充要條件: 僅存在兩個點,其中一個點的入度比出度大一,另一個店的出度比入度大一。(出度大的為起點,入度大的為終點)
根據連通性和度數可判斷出無向圖和有向圖是否存在歐拉回路和歐拉路徑,可用 dfs 構造歐拉回路和歐拉路徑。
基本思路:使用 dfs 的方式,遍歷圖中所有的點。dfs 一個環,然后在回溯的過程中,可能會遇到一個公共點連接着另一個環,此時再對這個公共點進行 dfs 遍歷另一個環…… 如此遞歸。在 dfs 完與 u 結點相連的 v 結點后,再將 u,v 這條邊壓入輸出棧(采用逆序的順序輸出結果,因為 dfs 棧幀入棧的順序與出棧的順序相反,起始點最后才出棧)。
例如:用 dfs 求該圖的歐拉路徑
dfs 遍歷該圖的順序為: (1, 2) (2, 3) (3, 4) (4, 5) (此時 dfs(5) 出棧,dfs(6) 入棧) (4, 6) (6, 7) (7, 2) (2, 8) (8, 4) (此時 dfs(4) 出棧,開始回溯)
dfs 棧幀出棧后,邊壓入輸出棧的順序為: (4, 5) (8, 4) (2, 8) (7, 2) (6, 7) (4, 6) (3, 4) (2, 3) (1, 2)
從輸出棧的棧頂開始輸出,輸出結果為: (1, 2) (2, 3) (3, 4) (4, 6) (6, 7) (7, 2) (2, 8) (8, 4) (4, 5)
核心代碼:
void dfs(int u){ for(int v=0; v<n; v++){ if(node[u][v] && !vis[u][v]){ //若存在u,v邊,並且沒有訪問過 vis[u][v] = vis[u][v] = 1; //表示已經訪問過 dfs(v); //dfs v 結點,繼續尋找回路。 output.push(Node(u, v)); //dfs(v) 出棧后,將u,v邊壓入輸出棧 } } }
注意:一定要在 dfs(v) 執行完之后再將 u,v 邊壓入輸出棧。
錯誤代碼:
cout << u << " " << v << endl; dfs(v);
如果在 dfs(v) 執行之前就輸出 u,v 邊,則有可能輸出的邊不能構成一筆畫。原因:v 結點可能是終點(該節點不再有邊能走),而中間可能會有多個環。如果先輸出了 u,v 邊,棧中的 dfs(u) 可能還會再訪問其他的邊,這時再訪問其他的邊時,是輸出從 u 結點到其他結點,而不是從 v 結點到其他節點。
例如:以1為起點
3 為兩個環的公共點。
若采用錯誤代碼,則會出現輸出 3, 4 后,輸出 3, 5 這樣的錯誤結果。
使用核心代碼,dfs 棧幀出棧后,邊壓入輸出棧的順序為:(3, 4) (8, 3) (7, 8) (2, 7) (6, 2) (5, 6) (3, 5) (2, 3) (1, 2),輸出從輸出棧的棧頂開始,逆序輸出。
輸出的結果為:
1 2
2 3
3 5
5 6
6 2
2 7
7 8
8 3
3 4
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
例題 6-16
Play On Worlds (UVa 10129)
題目:輸入 n 個單詞,判斷是否可以把所有這些單詞排成一個序列,使得每個單詞的第一個字母和上一個單詞的最后一個字母相同(例如 acm、malform、mouse)。
基本思路:
將字母看成點,每個單詞看成有向的邊,從首字母指向尾字母,構成一個有向圖。題目就變成了,是否能每個邊都只走一次,就遍歷完所有的邊,即變成了歐拉路徑的問題。
要構造歐拉路徑,當且僅當兩個條件都滿足:
① 底圖(忽略邊方向后得到的無向圖)是連通的
② 所有點的入度等於出度,或者僅存在兩個點,其中一個點出度比入度大一,另一個點入度比出度大一,其他的點出度等於入度。
條件 ① 可以用 dfs 來解決。一個圖是連通的當且僅當圖中只存在一個連通塊。
條件 ② 可以循環遍歷 26 個結點來判斷。(有的結點不在圖中,但 出度=入度=0,因此沒有影響)。
AC代碼:
/* Play in Words (UVa 10129) */ /* 歐拉路徑 */ #include <iostream> #include <cstring> #include <string> using namespace std; const int MAX = 26; int in[MAX], out[MAX]; //每一個點的出度和入度 int G[MAX][MAX], vis[MAX][MAX]; //兩點之間是否存在邊,以及這條邊是否走過。 bool dfs(int u); //若存在連通塊則返回 true ,否則返回 false bool communication(); //檢查圖是否連通 bool check_in_out(); //通過出入度數判斷是否可以構成歐拉路徑 int main(){ //freopen("input.txt", "r", stdin); //freopen("output.txt", "w", stdout); int T, N; cin >> T; while(T--){ bool ok = true; //是否能構成歐拉路徑 memset(vis, 0, sizeof(vis)); memset(G, 0, sizeof(G)); memset(in, 0, sizeof(in)); memset(out, 0, sizeof(out)); cin >> N; while(N--){ string s; cin >> s; int begin=s[0]-'a', end=s[s.length()-1]-'a'; //單詞為邊,從首字母出發,到尾字母 /*if(begin == end) //若單詞的首尾字母相同,則可以忽略這個單詞 continue;*/ in[end]++; //到尾字母 out[begin]++; //從首字母出發 G[begin][end] = G[end][begin] = 1; //構成底圖(無向圖) } if(communication()){ //如果圖是連通的,就再檢查出入度數是否滿足要求 if(!check_in_out()) ok = false; }else{ ok = false; } if(ok) cout << "Ordering is possible." << endl; else cout << "The door cannot be opened." << endl; } return 0; } bool communication(){ //如果圖是連通的,則圖中當且僅能存在一個連通塊 bool flag = false; //是否已經找到一個連通塊 for(int u=0; u<MAX; u++){ if(dfs(u)){ //如果找到了一個連通塊 if(flag == true) //並且已經存在一個連通塊了,則圖不是連通的 return false; flag = true; } } return true; } bool dfs(int u){ bool find = false; //是否找到連通塊 for(int v=0; v<MAX; v++){ if(G[u][v] && !vis[u][v]){ //如果存在邊,並且沒有走過 find = true; //找到連通塊了 vis[u][v] = vis[v][u] = 1; //標記已經走過這條邊 dfs(v); } } return find; } bool check_in_out(){ bool in_node = true; //存在一個入度比出度大一的點(最多只能存在一個這樣的點) bool out_node = true; //存在一個出度比入讀大一的點(最多只能存在一個這樣的點) for(int i=0; i<MAX; i++){ if(in[i] == out[i]){ //如果出度和入度相等,則檢查下一個點 continue; }else if(in[i]-1 == out[i] && in_node){ //如果入度比出度大一,並且這個點存在 in_node = false; continue; }else if(out[i]-1 == in[i] && out_node){ out_node = false; continue; }else{ return false; } } if(in_node != out_node) //如果兩個點一個存在一個不存在,則不能構成歐拉路徑 return false; return true; }