我們探索某個領域的知識,無不懷揣的核彈級的好奇心與求知欲,那么,今天,我們就將開始對圖論的探索。
觀察一副《機械迷城》 的一處謎題。
不得不承認,《機械迷城》這款解密游戲難度遠勝於《紀念碑谷》, 其中一個困難點就在於——《紀念碑谷》的目標是很明確的,但是《機械迷城》往往需要自己憑感覺設立目標。而這里的關卡的目標,就是堵住第三個出水口。
為了解決這個謎題,如果不去考慮用暴力枚舉的方法去試探(其實很多情況下都是用到這種情況)一開始,我們似乎會從模擬電路的角度來看待這個水管圖,但是會發現它太過復雜,簡單的電路圖似乎很難以表達整個線路的構造,這里,我們會聯想到一些能夠表征點與點之間的關系的數學模型——那就是圖論所擅長的領域。
“聯系”或者“關系”,在自然界中就像任意兩個物體之間的萬有引力一樣常見,而圖論就是致力於將一個集合的個元素的相互關系給找出。
在圖論中,用點指代“事物”,用邊指代“事物間某種聯系”,而在邊上可以添加一種叫做“權值”的信息用以更加詳細的表征這種聯系,這就是圖。
而基於圖最基本的定義,會衍生出一些特殊的圖譬如補圖、二分圖、完全圖,這里暫且不去深究其定義。
那么我們有了強大的數學工具在手,再來解決上面這個謎題,就顯得很小兒科了。我們從水的入口開始,順着管道,分叉口作為圖中的點,而水的流向以及是否有閥門則可以在點與點之間 邊上體現。雖然畫出其抽象圖可能會比較麻煩,但是不難想象,如果從游戲開發人員的角度去看這個謎題,顯然更加需要圖論的應用。
通過上面的介紹,相信讀者已經對圖論有了很初步的了解了。那么我們開始結合具體的問題更加深入的探究圖論知識的奧秘。
關於圖的拓撲排序的問題。(Problem source:pku1128)
這里先非常粗略的給出圖的拓撲排序的定義:即一個圖的所有點排成一個序列,v1v2v3v4……,任取兩點vi、vj,如果兩者之間存在一條通路,那么一定是從vi -> vj , 那么此時我們說這是一個圖的拓撲排序后的序列。
而拓撲排序在實現上其實也非常簡單,我們遍歷當前的圖,找到一個沒有入度(沒有邊進來)的點,作為拓撲序列的第一個點,一次類推,直到最后序列包含原圖的所有點。
基於這種構造方法我們能夠很好的理解——無向圖、帶環圖是沒有拓撲排序的。
題目大意:有五個已知的矩陣如上圖所示,將它們堆疊在一起將形成一個新的矩陣。現在的問題是,給你一個堆疊后形成的矩陣,讓你給出所有可以成立的堆疊順序。
數理分析: 針對這個問題,選好思維出發的角度非常重要。根據題設的要求,我們能夠保證每種字母框的每個邊都會露出一個字母,那么憑借這個信息,我們就可以找到構成這個框的每個點在圖中的位置。基於此,我們再去尋找在這個框上是否出現了其他字母,一旦出現,說明這個字母就在當前字母的上面。這樣遍歷下來,我們就可以將一個矩陣圖,轉化記載了各個字母相對位置的圖——也就是我們所熟悉的點與點之間連接着線(有向)的那種抽象的圖,做到了這一步,我們將構造出來的圖和題意進行比較——出現的先后順序,其實就表征了圖中的有向性,而這其實也是拓撲排序所體現的,所以我們自然而然的要開始進行拓撲排序了。
這里題目的要求是給出所有的拓撲排序方案,結合上面構造某種拓撲排序的簡單方法,再加上遍歷圖的一種方法——深搜,我們便可以找到所有的拓撲排序。
編程實現:圖論相對來數在數理思維上並不是那么困難,而在編程實現上則比較困難。這里的困難點其實就體現在,同一個圖的轉化導致的不同信息呈現——這里就是一個具象的矩陣圖轉化成表征各個字母出現的前后關系的抽象圖。有了這一步關鍵的過渡后,只需構造深度優先搜索把所有的情況遍歷出來即可。
參考代碼如下。(暫時還沒AC)
#include <stdio.h> #include <string.h> #define maxn 35 #define maxm 35 #define kind 5 char ori[maxn][maxn], ans[kind + 1]; int m, n, in[kind], total; bool map[kind][kind]; struct Node { int x, y; } lt[maxm], rb[maxm]; //這里采用記錄一個邊框左上角的點和右下角的點的數據用以后面掃描邊框上的其他字母. //因此初始化的時候對應着,左上角的點應該盡量往右下角初始化,右下角的點往左上角初始化。 void GetMap() { int i, j, t, k, x, y; memset(map , 0 , sizeof(map)); memset(in , -1 , sizeof(in)); memset(lt , 0x3f, sizeof(lt)); memset(rb , -1 , sizeof(rb)); for(i = total = 0; i < n; i++) for(j = 0; j < m; j++) { if(ori[i][j] == '.') continue; t = ori[i][j] - 'A'; if(in[t] == -1) { in[t] = 0; total++; } if(i < lt[t].x) lt[t].x = i; if(i > rb[t].x) rb[t].x = i; if(lt[t].y > j) lt[t].y = j; if(rb[t].y < j) rb[t].y = j; } for(i = 0; i < kind; i++) { for(x = lt[i].x; x <= rb[i].x; ++x) for(y = lt[i].y; y <= rb[i].y; ++y){ if(x > lt[i].x && y > lt[i].y && x < rb[i].x && y < rb[i].y) continue; t = ori[x][y] - 'A'; if(t != i && !map[i][t]){ map[i][t] = true; in[t]++; } } } } void DFS(int id) //fantastic! { if(id == total){ ans[id] = '\0'; puts(ans); return; } for(int i = 0; i < kind; ++i) { if(in[i] == 0) { ans[id] = 'A' + i; in[i] = -1; for(int j = 0; j < kind; ++j) if(map[i][j]) in[j]--; DFS(id + 1); for(int j = 0; j < maxm; ++j) if(map[i][j]) in[j]++; } } } int main() { int i; while(scanf("%d%d", &n, &m) == 2){ for(i = 0; i < n; ++i) scanf("%s", ori[i]); GetMap(); DFS(0); } return 0; }
讓我們再來看一道關於圖的拓撲排序的問題。(Prbolem source : hdu1285)
數理分析:我們在讀題后,是否能將一些具體問題中的量更加抽象化的看待,使我們能否聯想到拓撲排序的關鍵(運用很多數學知識都要依賴抽象化的能力),這里每個隊伍就是圖中的點,而勝負關系就是有向圖中的箭頭。如果抽象到了這一步,我們就會很自然的聯想到拓撲排序了。
編程實現:值得注意的是,這里的后台給出的數據應該都是可解的。而至於多解的情況,這里不用像上面那題要利用dfs遍歷出所有情況,這里只需給出可行解中隊伍序號小的在前的第一種情況。這里在選點排序的時候只需從點集中按序號從小到大的搜索然后選點即可,這樣構造出來的第一個一定是符合要求的。
參考代碼如下:
#include<stdio.h> #include<string.h> const int maxn = 505; int n , m , G[maxn][maxn] , q[maxn] , Indegree[maxn]; using namespace std; void toposort() { int i , j , k; i = 0; while(i < n) { for(j = 1;j <= n;j++) { if(Indegree[j] == 0) { Indegree[j]--; q[i++] = j; for(k = 1;k <= n;k++) if(G[j][k]) Indegree[k]--; break; } } } } int main() { int x , y , i; while(scanf("%d %d" , &n , &m) != EOF) { memset(Indegree , 0 , sizeof(Indegree)); memset(G , 0 , sizeof(G)); memset(q , 0 , sizeof(q)); for(i = 0;i < m;i++) { scanf("%d %d" , &x , &y); if(G[x][y] == 0) //一步對處理輸入數據的小小的優化,少了會引起超時。 { G[x][y] = 1; Indegree[y]++; } } toposort(); for(i = 0;i < n;i++) { if(i == n - 1) printf("%d\n" , q[i]); else printf("%d ",q[i]); } } }
我們再來看一道簡單的拓撲排序的問題(Problem source :hdu2094)
這道題目是非常明顯的拓撲排序的應用,而且十分簡單,可以說是一個判斷性的問題,那么這就大大簡化的程序的復雜性。
這里的冠軍,其實就是我們將實際的信息轉化成圖在轉化拓撲排序后的圖之后入度為0(沒有人戰勝它)的點,而在這里,我們在完成了上述操作后,只需再討論有多少個這樣入度為0的點,如果只有1個,那么顯然就能夠產生冠軍。
參考代碼如下。
#include<stdio.h> #include<string.h> const int maxn = 1005; using namespace std; char name[maxn][15]; int index , indegree[maxn]; int str_find(char * s) { int i; for(i = 0;i < index;i++) { if(strcmp(name[i] , s) == 0) break; } if(i == index) { strcpy(name[index++] , s); } return i; } int main() { int n; char s1[15] , s2[15]; int index1 , index2; while(scanf("%d",&n) != EOF && n) { index = 0 , memset(indegree , 0 ,sizeof(indegree)); for(int i = 0;i < n;i++) { scanf("%s %s" , s1 , s2); index1 = str_find(s1) , index2 = str_find(s2); indegree[index2]++; } int top_1 = 0; for(int i = 0;i < index;i++) { if(indegree[i] == 0) top_1++; if(top_1 > 1) break; } if(top_1 == 1) printf("Yes\n"); else printf("No\n"); } }
讓我們再來看一道關於拓撲排序的簡單問題(Problem source:hdu4324)
題目大意:假設這里有n個人,每個人有着自己的序號[1,n],這里分別給出n條信息,來告訴你第i個人喜歡第j個人。然后讓你判斷,這其中是否存早諸如a喜歡b,b喜歡c,c喜歡a的“三角戀情”。
數理分析:題目中給出的“三角戀”,其實對應着我們生成的graph中的環圖,雖然按理講環圖是無法進行拓撲排序的,但是這里拓撲排序僅僅是用來進行判斷,也就是說,我們對它進行拓撲排序,只要無法進行下去了,我們可以判斷其已經有了環圖。
然而不僅僅是需要環圖,准確地講這道題是讓我們判斷是否有三元的環圖。我們這里假設v1v2v3v4v5……vi形成了環圖,那么會存在v1->v2,v2->v3的關系,我們現在開始分別討論v1和v3的關系。
①如果v1->v3,那么此時去掉v2,形成了i-1元環。
②如果v3->v1,那么此時已經形成了三元環。
而根據題設,v1和v3只存在上述兩種關系。而針對①情況,采取相同的分析思路不斷的縮減,最終一定會得到一個三元環。
編程實現:基於以上的數理分析,我們只需要判斷我們生成的graph圖是否帶有環圖即可,而這在編程上顯然沒有什么難度。
參考代碼如下:
#include<stdio.h> #include<string.h> const int maxn = 2005; using namespace std; char graph[maxn][maxn]; int indegree[maxn]; int main() { int t ,tt, n; tt = 0; scanf("%d",&t); while(t--) { memset(indegree , 0 , sizeof(indegree)); scanf("%d" , &n); for(int i = 0;i < n;i++) { scanf("%s",graph[i]); for(int j = 0;j < n;j++) { if(graph[i][j] == '1') indegree[j]++; } } bool flag = false; for(int i = 0;i < n;i++) { int j; for(j = 0;j < n;j++) //沒有入度為0的點,拓撲排序無法進行,說明出現了環圖 if(indegree[j] == 0) break; if(j == n) { flag = true ; break; } else { indegree[j]--; //有入度為0的點,刪除並遍歷所有點更改對應的入度值,模擬拓撲排序的過程 for(int k = 0;k < n;k++) if(graph[j][k] == '1') indegree[k]--; } } if(flag) printf("Case #%d: Yes\n",++tt); else printf("Case #%d: No\n" ,++tt); } }
我們再來看一道有關拓撲排序的判斷性問題。(Problem source:hdu3342)
題目大意:這里給出n個人之見m條關系,這里如果路人A是B的師傅,也是B的徒弟,則判定為非法關系,這里讓你判斷給出的關系是否合法。
數理分析:根據題意,我們生成graph圖后,要判斷兩個點之間是否是雙向箭頭。
我們不知道給出的graph圖會被分割成多少小圖(即小圖之見彼此獨立,不存在任何通路),那我們就分析某個小圖(此圖中不存在與其他點不存在通路的點)。
題設說明,如果A是B的師傅,B是C的師傅,那么可以認定A是C的師傅,那么此時如果存在關系——C是A的師傅,那么就在graph圖中生成了環,此時是存在違法關系的。而判斷有無環生成,正是拓撲排序所能做的。
上面的分析是將環和拓撲排序聯系了起來,而環的生成最少需要3個元素。於是我們考慮2個點之間的聯系,發現事實上也是和拓撲排序對應的。
編程實現上:按照標准的拓撲排序的寫法,需要設置三層循環,第一層循環表示掃描入度為0的點需要進行n次,第二層循環是找到入度為0的點,第三層循環在找到入度為0的點后,刪除此點,對剩余點的入度值進行處理。拓撲排序后,依次刪掉入度點為0的次數如果少於graph圖含有的所有點的個數,則證明拓撲排序沒有完成,出現了環或者A與B互為師徒,即非法關系。
參考代碼如下。
#include<stdio.h> #include<string.h> using namespace std; const int maxn = 105; int G[maxn][maxn] , indegree[maxn]; int n , m; int cnt; void toposort() { int i , j ,k; for(i = 0;i < n;i++) { for(j = 0;j < n;j++) { if(indegree[j] == 0) { indegree[j]--; cnt++; for(k = 0;k < n;k++) if(G[j][k]) indegree[k]--; break; } } } } int main() { int x , y; while(scanf("%d%d",&n,&m) && (m || n)) { memset(G,0,sizeof(G)); memset(indegree , 0 , sizeof(indegree)); cnt = 0; for(int i = 0;i < m;i++) { scanf("%d%d",&x,&y); if(G[x][y] == 0) { G[x][y] = 1; indegree[y]++; } } toposort(); if(cnt == n) printf("YES\n"); else printf("NO\n"); } }
我們研究圖所呈現出來的事物間的聯系,一個很重要的方面即時圖中的兩點是否連通,而這個連通又可以分為直接連通和間接連通。而對於圖的連通性的一個工具,叫做並查集。
這里其實不必引入過多的概念性的語言,我們可以這樣簡單的理解,所謂並查集,就是將一個完整的圖中划分成一些小圖,而這些小圖需要具備的特征就是其含有的任意元素都是連通的,而小圖和小圖之見顯然就是不連通的,這就是我們所說的——不相交集合,也就是並查集。
我們通過一個簡單的題目再來認識一下所謂並查集的概念。(Problem source:hdu1232)
數理分析,這里其實就是讓我們來找出在一個大圖中,有多少個互不連通的小圖,假設這里有x個小圖,也就是所謂的連通分量,然后將小圖看成整體,所需要建的道路顯然是x-1。
編程實現:雖然城市與城市之間有道路,體現在圖上面無所謂方向,但是這里為了編程實現,我們就生成有向圖,這樣就會方便我們找根節點然后判斷城市是否在一個連通分量當中。
我們假設每次輸入的城市a、b代表a->b,這里我們在儲存方式上用F[a] = b來表示。那么我們開始假設有n(城市的個數)個連通分量,每次輸入的道路所連接的兩條城市的信息,遞歸尋找他們的根節點是否相同,如果相同 ,對應的F[a],F[b]存入他們所在連通分量的根節點(這里借用樹中的概念),如果不相同,那么就需要建立新的連通分量。
最后遍歷F[],有多少個-1,就說明有多少個連通分量。
參考代碼如下。
#include<stdio.h> const int maxn = 1005; using namespace std; int F[maxn]; int find(int t) { if(F[t] == -1) return t; else return F[t] = find(F[t]); } void bing(int a , int b) { int t1 = find(a); int t2 = find(b); if(t1 != t2) F[t1] = t2; } int main() { int m , n; int a , b; while(scanf("%d",&n) , n) { for(int i = 1;i <= n;i++) F[i] = -1; scanf("%d",&m); for(int i = 1;i <= m;i++) { scanf("%d%d",&a,&b); bing(a,b); } int ans = 0; for(int i = 1;i <= n ;i++) if(F[i] == -1) ans++; printf("%d\n",ans - 1); } }
讓我們再來看一道簡單的並查集應用的題目。(Problem source : hdu 1213)
題目大意:給你一個數n表示有n個朋友,然后給出m條信息,分別表征了n個朋友中兩兩的相互關系,只有互相認識的朋友才能做一桌,問你需要幾個桌子。
數理分析:我們從抽象化的graph圖來看問題,顯然是要求一個圖里的連通分量嘛。所以這里使用最基本的並查集就可以解決問題了。
參考代碼同上。
讓我們再看一道有關並查集應用的問題。(Problem source:hdu1272)
數理分析:從這個題目描述中,需要兩個節點有且只有一條路。基於存在性,我們可以想象到,這個graph圖只能有一個連通分量。否則的話無法做到任意兩個點之間有通路。基於唯一性,我們要判斷只有一個連通分量的圖是否有環(包括自環)。
編程實現:基於並查集原有的模板,我們只需在尋找父節點的時候加一步判斷,即我們當前輸入的道路連接的兩個節點,他們的父節點之間存在通路,那么就將形成環,此時便不滿足要求了。
參考代碼如下。
#include<iostream> using namespace std; int const MAX = 100005; int F[MAX],flag,sign[MAX]; int Find(int t) { if(F[t] == t) return t; else return F[t] = Find(F[t]); } void bing(int x,int y) { int t1 =Find(x); int t2 =Find(y); if(t1!=t2) F[t1]=t2; else flag = 0; //父節點相同,成環 } void Init(int a,int b) { for(int i=1;i<MAX;i++) { F[i]=i; sign[i]=0; } sign[a] = sign[b]=1; flag=1; bing(a,b); } int main() { int i,a,b; while(cin>>a>>b) { if(a==-1&&b==-1) break; if(a==0&&b==0) { cout<<"Yes"<<endl; continue; } Init(a , b); while(cin>>a>>b) { if(a==0&&b==0) break; bing(a,b); sign[a]=sign[b]=1; } int cnt=0; for(i=1;i<MAX;i++) { if(sign[i]&&F[i]==i) //判斷連通分量的個數 cnt++; if(cnt>1) {flag=0;break;} } if(flag) cout<<"Yes"<<endl; else cout<<"No"<<endl; } return 0; }
讓我們再看一道基於並查集的變式問題。(Problem source : hdu 1856)
數理分析:這里其實就是給出一張graph圖,讓你找到含元素最多的連通分量的元素個數。
編程實現:基於已有的關於並查集的模板上,我們需要另開一個數組num[],來記錄各個連通分量含有的元素個數。
這里非常值得注意的一點是,我們通過並查集實現對Graph圖連通分量的記錄,是在假定Graph圖是有向的情況下的,所以這里在修改代碼完成各個連通分量含有元素的時候,也需要注意圖的有向性。這里如果方向弄反,訪問一次父節點是不會出錯,但如果訪問第二次父節點,就會出現錯誤。
參考代碼如下。
#include<stdio.h> using namespace std; const int maxn = 10000000+ 5; int F[maxn] , num[maxn]; int Find(int t) { if(F[t] == t) return t; else return F[t] = Find(F[t]); } void bing(int a , int b) { int t1 = Find(a); int t2 = Find(b); if(t1 != t2) { F[t1] = t2; num[t2] += num[t1]; //圖是有向的,這里不能寫成num[t1] += num[t2]。 } } void init() { for(int i = 1;i < maxn;i++) { num[i] = 1 , F[i] = i; } } int main() { int n; int i; int x , y; while(scanf("%d",&n) != EOF) { init(); if(n == 0) { printf("1\n"); continue; } int Max1 = 0; for(i = 0;i < n;i++) { scanf("%d%d",&x,&y); if(x > Max1) Max1 = x; if(y > Max1) Max1 = y; bing(x,y); } int Max2 = 0; for(i = 1;i <= Max1;i++) { if(Max2 < num[i]) Max2 = num[i]; } printf("%d\n",Max2); } }
讓我們再來看一道關於並查集的簡單應用(Problem source:poj 2236)
題目大意:給出數字n,表示有n台編號[1,n]的電腦,再給出兩台電腦之間最大的通信距離d。隨后輸入n個電腦的平面坐標。隨后開始進行兩種操作,一種操作‘O’表示修電腦(之前給出的n台電腦是壞的),另外一種操作‘S’是輸入兩個編號,此時你就需要判斷這兩台電腦之間能否進行通信。
數理分析:這里我們注意到題目敘述的最后一句話,如果A、B同時可以和C進行通信,那么A、B之間就可以忽略距離限制也可以進行通信,所以我們這里考慮將可以互相通信的電腦放入一個集合,那么在進行‘S’操作的時候,我們只要判斷這兩個電腦是否在一個集合中即可了。
這也就聯系到了我們所熟悉的並查集。
編程實現:基於最基礎的並查集模板,我們考慮怎樣恰到好處的運用以及微小的改動。在進行'S'操作的時候,顯然是基於已經構建好的Graph圖,所以在進行'O'操作的時候,我們應該開始用並查集的方法構建Graph圖。這里我們通過一個數組來記錄每個電腦的好壞情況,然后每次進行'O'操作的時候,我們都遍歷一遍,將該操作下修好的電腦和剩余的好的電腦依次構造並查集。
參考代碼如下。
#include<iostream> #include<cstdio> #include<cstring> #include<string> #include<algorithm> using namespace std; const int maxn = 1005; int d; struct point { int pre; int x , y; }p[maxn]; int Find(int t) { if(p[t].pre == t) return t; else return p[t].pre = Find(p[t].pre); } void bing(point p1,point p2) { int t1 = Find(p1.pre); int t2 = Find(p2.pre); if(t1 != t2) if((p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y) <= d*d) p[t2].pre = t1; } int main() { int num; char ope; int ok; int from, to; scanf("%d%d", &num, &d); for(int i = 1; i <= num; ++i) p[i].pre = i; bool use[maxn]; memset(use, false, sizeof(use)); for(int i = 1; i <= num; ++i) scanf("%d%d", &p[i].x, &p[i].y); while(scanf("\n%c", &ope) != EOF) { if(ope == 'O') { scanf("%d", &ok); use[ok] = true; for(int i = 1; i <= num; ++i) if(use[i] && i != ok) bing(p[i], p[ok]); } else { scanf("%d%d", &from, &to); if(Find(from) == Find(to)) printf("SUCCESS\n"); else printf("FAIL\n"); } } return 0; }
下面我們開始討論圖的遍歷性問題。
對於圖的遍歷算法,我們常用拿來分析的是歐拉圖和哈密頓圖,這里我們先介紹歐拉圖。
針對著名的格尼斯堡七橋問題,歐拉回路其實就是研究一個圖中,能否存在這樣一個回路,使得這個回路能夠不重復的訪問這個圖中的所有邊。通俗點說,就是從一個點出發,不重復地遍歷了圖中所有的邊和所有的頂點,這樣的路徑,就稱為歐拉回路。
對於一個圖歐拉回路存在性的分析,我們可以用到並查集做簡單判斷(隨后會給出一個簡單的證明),而如果想要具體路徑,則需要借助dfs來實現。
我們簡單的來分析一下如何來判斷一個圖中是否存在歐拉回路。基於歐拉回路的定義,它需要滿足如下的條件。
①這個圖應該是連通的,即不存在孤立的店,或者說只有一個父節點。
②對於一個圖來說,我們先從一個點出發找到一條回路,如果這條回路沒有包含圖中所有的邊,我們則需要重新尋找。但是我們可以看到的是最終的當前路徑對應的圖G1一定是最終歐拉回路對應的圖G2的一個子圖,因此我們需要在G1的基礎上進行拓展。而在G1的基礎上進行拓展(遍歷剩余的邊)並且又要滿足歐拉回路的定義,我們看到只有一種方法,即在G1圖所表示的路徑<v1v2v3……vn>中,在vi節點處又形成了以vi為起始點的新回路,這就是先了在原有G1的基礎上,且滿足歐拉回路的限制條件,並遍歷到了更多的邊,這樣反復下去,即可判斷出歐拉回路存在與否。
基於上面的分析,我們容易看到,歐拉回路存在的充要條件是對於任意節點Vi,deta(Vi)是偶數。(Vi的度數是偶數),上面的分析是基於無向圖,推廣到有向圖之后,就是任意節點的度數是0。
基於此,我們就可以通過並查集來簡單的判斷一個圖是否存在歐拉回路了。
然我們結合一個問題來具體實現一下這個算法。(Problem source : 1878)
參考代碼如下。
#include<stdio.h> #include<string.h> const int maxn = 1010; using namespace std; int degree[maxn] , father[maxn]; int Find(int x) { if(x == father[x]) return x; else return father[x] = Find(father[x]); } int main() { int N,M,x,y,root; while(scanf("%d",&N) ,N) { int flag = 1 , root = 0; scanf("%d",&M); for(int i = 0;i < N;i++) father[i] = i; memset(degree , 0 , sizeof(degree)); for(int i = 0 ;i < M;i++) { scanf("%d%d",&x,&y); int a = Find(x); int b = Find(y); if(a != b) father[x] = y; degree[x]++ , degree[y]++; } for(int i = 1;i <= N; i++) { if(father[i] == i) root++; if(root > 1) { flag = 0 ;break;} if(degree[i]&1) {flag = 0;break;} } if(flag) printf("1\n"); else printf("0\n"); } }
基於我們上文對歐拉回路問題的探討,這里我們在補充一些內容。
上文我們提到,判斷一個圖的是否具有歐拉回路的充要條件:
①針對無向圖,所有節點的度數均為偶數。
②針對有向圖,所有節點的度數均為奇數。
那么現在我們可以將歐拉回路拓展一下,叫做歐拉路徑,即不重復得遍歷圖中所有邊,但是終點不與起點重合,其實就是我們說的一筆畫的問題。
基於我們對歐拉回路的分析,這里其實很容易得到充要條件。
①對於無向圖,度數為奇數的點有且僅有兩個。(起點與終點)
②對於有向圖,度數為+1和-1的節點各一個,其余的節點度數均為0。
基於對這個模型的分析,我們來結合一個具體的問題來實現它的代碼。(Problem source : hdu 1116)
There is a large number of magnetic plates on every door. Every plate has one word written on it. The plates must be arranged into a sequence in such a way that every word begins with the same letter as the previous word ends. For example, the word ``acm'' can be followed by the word ``motorola''. Your task is to write a computer program that will read the list of words and determine whether it is possible to arrange all of the plates in a sequence (according to the given rule) and consequently to open the door.
題目大意:給出很多單詞,判斷這些單詞能夠首尾連接起來(類似於成語接龍)。
數理分析:我們很容易將其與歐拉回路的模型聯系起來,我們將一個單詞的首位字母看成節點,並且在這里,無論是歐拉回路還是歐拉路徑,都是可以滿足題目要求的,因此我們在判斷的時候都需要考慮在內。
編程實現:再編程實現的時候需要注意一下判斷流程的一些細節。根據上文的分析,我們編程判斷的流程如下。
step1:遍歷所有節點,記錄父節點。如果大於1,跳出循環,flag標記為0。
step2:如果該節點訪問過(因為在這個問題中無法保證所有的字母都會出現),那么考察其入度和出度。如果差值大於1,跳出循環,flag標記為0。
step3:回到step1,直到遍歷完成。
遍歷完成后,我們只需根據flag的值和對於訪問過的節點的度數進行判斷即可。
參考代碼如下。
#include<stdio.h> #include<string.h> #include<cmath> const int maxn = 30; using namespace std; int indegree[maxn] , father[maxn],outdegree[maxn]; bool visit[maxn]; int Find(int x) { if(x == father[x]) return x; else return father[x] = Find(father[x]); } int main() { int wordnum; char word[1010]; int s , e; int ncases; scanf("%d",&ncases); while(ncases--) { scanf("%d",&wordnum); for(int i = 1;i <= maxn;i++) father[i] = i; memset(indegree , 0 , sizeof(indegree)); memset(outdegree, 0 , sizeof(outdegree)); memset(visit,false,sizeof(visit)); for(int i = 1 ;i <= wordnum;i++) { scanf("%s",word); int len = strlen(word); s = word[0] - 'a' + 1; e = word[len-1] - 'a' + 1; visit[s] = true; visit[e] = true; int a = Find(s); int b = Find(e); if(a != b) father[e] = s;//這里是有向圖,還需要記錄入度出度,因此在構造並查集的時候需要搞清楚誰是誰的父節點 outdegree[s]++ , indegree[e]++;//這與下面的度數統計要呼應起來。 } int flag = 1; int indegree1 = 0 , outdegree1 = 0; int root = 0; for(int i = 1;i <= maxn; i++) { if(visit[i]) { if(father[i] == i) root++; if(root > 1) {flag = 0; break;} if(indegree[i] != outdegree[i]) { if(indegree[i] - outdegree[i]== 1) indegree1++; else if(outdegree[i] - indegree[i] == 1) outdegree1++; else {flag = 0;break;} } } } if(flag && indegree1 == 1 && outdegree1 == 1) printf("Ordering is possible.\n"); else if(flag && indegree1 == 0 && outdegree1 == 0) printf("Ordering is possible.\n"); else printf("The door cannot be opened.\n"); } }
參考系:《圖論及應用》 馮林 金博