【轉】哈密頓回路
原文鏈接:http://www.cnblogs.com/Ash-ly/p/5452580.html
概念:
哈密頓圖:圖G的一個回路,若它通過圖的每一個節點一次,且僅一次,就是哈密頓回路.存在哈密頓回路的圖就是哈密頓圖.哈密頓圖就是從一點出發,經過所有的必須且只能一次,最終回到起點的路徑.圖中有的邊可以不經過,但是不會有邊被經過兩次.
與歐拉圖的區別:歐拉圖討論的實際上是圖上關於邊的可行便利問題,而哈密頓圖的要求與點有關.
判定:
一:Dirac定理(充分條件)
設一個無向圖中有N個頂點,若所有頂點的度數大於等於N/2,則哈密頓回路一定存在.(N/2指的是⌈N/2⌉,向上取整)
二:基本的必要條件
設圖G=<V, E>是哈密頓圖,則對於v的任意一個非空子集S,若以|S|表示S中元素的數目,G-S表示G中刪除了S中的點以及這些點所關聯的邊后得到的子圖,則W(G-S)<=|S|成立.其中W(G-S)是G-S中聯通分支數.
三:競賽圖(哈密頓通路)
N(N>=2)階競賽圖一點存在哈密頓通路.
算法:
一:在Dirac定理的前提下構造哈密頓回路
過程:
1:任意找兩個相鄰的節點S和T,在其基礎上擴展出一條盡量長的沒有重復結點的路徑.即如果S與結點v相鄰,而且v不在路徑S -> T上,則可以把該路徑變成v -> S -> T,然后v成為新的S.從S和T分別向兩頭擴展,直到無法繼續擴展為止,即所有與S或T相鄰的節點都在路徑S -> T上.
2:若S與T相鄰,則路徑S -> T形成了一個回路.
3:若S與T不相鄰,可以構造出來一個回路.設路徑S -> T上有k+2個節點,依次為S, v1, v2, ..., vk, T.可以證明存在節點vi(i屬於[1, k]),滿足vi與T相鄰,且vi+1與S相鄰.找到這個節點vi,把原路徑變成S -> vi -> T -> vi+1 -> S,即形成了一個回路.
4:到此為止,已經構造出來了一個沒有重復節點的的回路,如果其長度為N,則哈密頓回路就找到了.如果回路的長度小於N,由於整個圖是連通的,所以在該回路上,一定存在一點與回路之外的點相鄰.那么從該點處把回路斷開,就變回了一條路徑,同時還可以將與之相鄰的點加入路徑.再按照步驟1的方法盡量擴展路徑,則一定有新的節點被加進來.接着回到路徑2.
證明:
可利用鴿巢原理證明.
偽代碼:
設s為哈密頓回路的起始點,t為哈密頓回路中終點s之前的點.ans[]為最終的哈密頓回路.倒置的意思指的是將數組對應的區間中數字的排列順序方向.
1:初始化,令s = 1,t為s的任意一個鄰接點.
2:如果ans[]中元素的個數小於n,則從t開始向外擴展,如果有可擴展點v,放入ans[]的尾部,並且t=v,並繼續擴展,如無法擴展進入步驟3.
3:將當前得到的ans[]倒置,s和t互換,從t開始向外擴展,如果有可擴展點v,放入ans[]尾部,並且t=v,並繼續擴展.如無法擴展進入步驟4.
4:如果當前s和t相鄰,進入步驟5.否則,遍歷ans[],尋找點ans[i],使得ans[i]與t相連並且ans[i +1]與s相連,將從ans[i + 1]到t部分的ans[]倒置,t=ans[i +1],進如步驟5.
5:如果當前ans[]中元素的個數等於n,算法結束,ans[]中保存了哈密頓回路(可看情況是否加入點s).否則,如果s與t連通,但是ans[]中的元素的個數小於n,則遍歷ans[],尋找點ans[i],使得ans[i]與ans[]外的一點(j)相連,則令s=ans[i - 1],t = j,將ans[]中s到ans[i - 1]部分的ans[]倒置,將ans[]中的ans[i]到t的部分倒置,將點j加入到ans[]的尾部,轉步驟2.
時間復雜度:
如果說每次到步驟5算一輪的話,那么由於每一輪當中至少有一個節點被加入到路徑S -> T中,所以總的輪數肯定不超過n輪,所以時間復雜度為O(n^2).空間上由於邊數非常多,所以采用鄰接矩陣來存儲比較適合.
代碼:
1 const int maxN = 100;
2 inline void reverse(int arv[maxN + 7], int s, int t){//將數組anv從下標s到t的部分的順序反向
3 int temp;
4 while(s < t){
5 temp = arv[s];
6 arv[s] = arv[t];
7 arv[t] = temp;
8 s++;
9 t--;
10 }
11 }
12
13 void Hamilton(int ans[maxN + 7], bool map[maxN + 7][maxN + 7], int n){
14 int s = 1, t;//初始化取s為1號點
15 int ansi = 2;
16 int i, j;
17 int w;
18 int temp;
19 bool visit[maxN + 7] = {false};
20 for(i = 1; i <= n; i++) if(map[s][i]) break;
21 t = i;//取任意鄰接與s的點為t
22 visit[s] = visit[t] = true;
23 ans[0] = s;
24 ans[1] = t;
25 while(true){
26 while(true){//從t向外擴展
27 for(i = 1; i <= n; i++){
28 if(map[t][i] && !visit[i]){
29 ans[ansi++] = i;
30 visit[i] = true;
31 t = i;
32 break;
33 }
34 }
35 if(i > n) break;
36 }
37 w = ansi - 1;//將當前得到的序列倒置,s和t互換,從t繼續擴展,相當於在原來的序列上從s向外擴展
38 i = 0;
39 reverse(ans, i, w);
40 temp = s;
41 s = t;
42 t = temp;
43 while(true){//從新的t繼續向外擴展,相當於在原來的序列上從s向外擴展
44 for(i = 1; i <= n; i++){
45 if(map[t][i] && !visit[i]){
46 ans[ansi++] = i;
47 visit[i] = true;
48 t = i;
49 break;
50 }
51 }
52 if(i > n) break;
53 }
54 if(!map[s][t]){//如果s和t不相鄰,進行調整
55 for(i = 1; i < ansi - 2; i++)//取序列中的一點i,使得ans[i]與t相連,並且ans[i+1]與s相連
56 if(map[ans[i]][t] && map[s][ans[i + 1]])break;
57 w = ansi - 1;
58 i++;
59 t = ans[i];
60 reverse(ans, i, w);//將從ans[i +1]到t部分的ans[]倒置
61 }//此時s和t相連
62 if(ansi == n) return;//如果當前序列包含n個元素,算法結束
63 for(j = 1; j <= n; j++){//當前序列中元素的個數小於n,尋找點ans[i],使得ans[i]與ans[]外的一個點相連
64 if(visit[j]) continue;
65 for(i = 1; i < ansi - 2; i++)if(map[ans[i]][j])break;
66 if(map[ans[i]][j]) break;
67 }
68 s = ans[i - 1];
69 t = j;//將新找到的點j賦給t
70 reverse(ans, 0, i - 1);//將ans[]中s到ans[i-1]的部分倒置
71 reverse(ans, i, ansi - 1);//將ans[]中ans[i]到t的部分倒置
72 ans[ansi++] = j;//將點j加入到ans[]尾部
73 visit[j] = true;
74 }
75 }
二:N(N>=2)階競賽圖構造哈密頓通路
N階競賽圖:含有N個頂點的有向圖,且每對頂點之間都有一條邊.對於N階競賽圖一定存在哈密頓通路.
數學歸納法證明競賽圖在n >= 2時必存在哈密頓路:
(1)n = 2時結論顯然成立;
(2)假設n = k時,結論也成立,哈密頓路為V1, V2, V3, ..., Vk;
設當n = k+1時,第k + 1個節點為V(k+1),考慮到V(k+1)與Vi(1<=i<=k)的連通情況,可以分為以下兩種情況.
1:Vk與V(k+1)兩點之間的弧為<Vk, V(k+1)>,則可構造哈密頓路徑V1, V2,…, Vk, V(k+1).
2:Vk與V(k+1)兩點之間的弧為<V(k+1),Vk>,則從后往前尋找第一個出現的Vi(i=k-1,i>=1,--i),滿足Vi與V(k+1)之間的弧為<Vi,V(k+1)>,則構造哈密頓路徑V1, V2, …, Vi, V(k+1), V(i+1), …, V(k).若沒找到滿足條件的Vi,則說明對於所有的Vi(1<=i<=k)到V(k+1)的弧為<V(k+1),V(i)>,則構造哈密頓路徑V(k+1), V1, V2, …, Vk.
證畢.
競賽圖構造哈密頓路時的算法同以上證明過程.
用圖來說明:
假設此時已經存在路徑V1 -> V2 -> V3 -> V4,這四個點與V5的連通情況有16種,給定由0/1組成的四個數,第i個數為0代表存在弧<V5,Vi>,反之為1,表示存在弧<Vi,V5>
sign[]={0, 0, 0, 0}.
很顯然屬於第二種情況,從后往前尋找不到1,即且不存在弧<Vi, V5>.
則構造哈密頓路:V5 -> V1 -> V2 -> V3 -> V4.
sign[]={0, 0, 0, 1}.
屬於第一種情況,最后一個數字為1,即代表存在弧<Vi, V5>且i=4(最后一個點)
則構造哈密頓路: V1 -> V2 -> V3 -> V4 -> V5.
sign[]={0, 0, 1, 0}.
屬於第二種情況,從后往前找到1出現的第一個位置為3.
構造哈密頓路: V1 -> V2 -> V3 -> V5 -> V4.
sign[]={0, 0, 1, 1}.
屬於第一種情況,最后一個數字為1,即代表存在弧<Vi, V5>且i=4(最后一個點)
則構造哈密頓路: V1 -> V2 -> V3 -> V4 -> V5.
sign[]={0, 1, 0, 0}.
屬於第二種情況,從后往前找到1出現的第一個位置為2.
構造哈密頓路: V1 -> V2 -> V5 -> V3-> V4.
sign[]={0, 1, 0, 1}.
屬於第一種情況,最后一個數字為1,即代表存在弧<Vi, V5>且i=4(最后一個點)
則構造哈密頓路:V1 -> V2 -> V3 -> V4 -> V5.(就不舉末尾為1的栗子了~~)
sign[]={1, 0, 1, 0}.
屬於第二種情況,從后往前找到1出現的第一個位置為3.
構造哈密頓路: V1 -> V2 -> V3 -> V5-> V4.
sign[]={1, 1, 1, 0}.
屬於第二種情況,從后往前找到1出現的第一個位置為3.
構造哈密頓路: V1 -> V2 -> V3 -> V5-> V4.
(還是舉一個吧~~~)
sign[]={1, 1, 1, 1}.
同樣最后一位為1,代表存在<Vi, V5>且i=4(最后一位)
則構造哈密頓路:V1 -> V2 -> V3 -> V4 -> V5.以上是當N=4時(N+1=5),用圖來闡述算法的過程.
注意從后往前找不是找這個點編號之前的點,即不是按照編號來的,而是按照當前哈密頓序列從后往前找的.舉個栗子:
4
2 1
1 3
3 2
4 1
4 2
4 3
第一步ans={1}
第二步ans={2,1}
第三步sign={0, 1}(map[3][2] = 0,map[3][1] = 1,當前序列為2,1) ,而不是{1, 0}(1,2),因為存在弧<V1, V3>和<V3, V2>.這里需要注意下.
代碼:
1 #include <iostream>
2 #include <cmath>
3 #include <cstdio>
4 #include <cstring>
5 #include <cstdlib>
6 #include <algorithm>
7 #include <queue>
8 #include <stack>
9 #include <vector>
10
11 using namespace std;
12 typedef long long LL;
13 const int maxN = 200;
14
15 //The arv[] length is len, insert key befor arv[index]
16 inline void Insert(int arv[], int &len, int index, int key){
17 if(index > len) index = len;
18 len++;
19 for(int i = len - 1; i >= 0; --i){
20 if(i != index && i)arv[i] = arv[i - 1];
21 else{arv[i] = key; return;}
22 }
23 }
24
25 void Hamilton(int ans[maxN + 7], int map[maxN + 7][maxN + 7], int n){
26 int ansi = 1;
27 ans[ansi++] = 1;
28 for(int i = 2; i <= n; i++){//第一種情況,直接把當前點添加到序列末尾
29 if(map[i][ans[ansi - 1]] == 1)
30 ans[ansi++] = i;
31 else{
32 int flag = 0;
33 for(int j = ansi - 2; j > 0; --j){//在當前序列中,從后往前找到第一個滿足條件的點j,使得存在<Vj,Vi>且<Vi, Vj+1>.
34 if(map[i][ans[j]] == 1){//找到后把該點插入到序列的第j + 1個點前.
35 flag = 1;
36 Insert(ans, ansi, j + 1, i);
37 break;
38 }
39 }
40 if(!flag)Insert(ans, ansi, 1, i);//否則說明所有點都鄰接自點i,則把該點直接插入到序列首端.
41 }
42 }
43 }
44
45 int main()
46 {
47 //freopen("input.txt", "r", stdin);
48 int t;
49 scanf("%d", &t);
50 while(t--){
51 int N;
52 scanf("%d", &N);
53 int M = N * (N - 1) / 2;
54 int map[maxN + 7][maxN + 7] = {0};
55 for(int i = 0; i < M; i++){
56 int u, v;
57 scanf("%d%d", &u, &v);
58 //map[i][j]為1說明j < i,且存在弧<Vi, Vj>,因為插入時只考慮該點之前的所有點的位置,與之后的點沒有關系.所以只注重該點與其之前的點的連通情況.
59 if(u < v)map[v][u] = 1;
60 }
61 int ans[maxN + 7] = {0};
62 Hamilton(ans, map, N);
63 for(int i = 1; i <= N; i++)
64 printf(i == 1 ? "%d":" %d", ans[i]);
65 printf("\n");
66 }
67 return 0;
68 }
代碼2:
1 void Hamilton(int ans[maxN + 7], int map[maxN + 7][maxN + 7], int n){
2 int nxt[maxN + 7];
3 memset(nxt, -1, sizeof(nxt));
4 int head = 1;
5 for(int i = 2; i <= n; i++){
6 if(map[i][head]){
7 nxt[i] = head;
8 head = i;
9 }else{
10 int pre = head, pos = nxt[head];
11 while(pos != -1 && !map[i][pos]){
12 pre = pos;
13 pos = nxt[pre];
14 }
15 nxt[pre] = i;
16 nxt[i] = pos;
17 }
18 }
19 int cnt = 0;
20 for(int i = head; i != -1; i = nxt[i])
21 ans[++cnt] = i;
22 }
代碼三:
1 void Hamitton(bool reach[N + 7][N + 7], int n)
2 {
3 vector <int> ans;
4 ans.push_back(1);
5 for(int i=2;i <= n;i++)
6 {
7 bool cont = false;
8 for(int j=0;j<(int)ans.size()-1;j++)
9 if(reach[ ans[j] ][i] && reach[i][ ans[j+1] ])
10 {
11 ans.insert(ans.begin()+j+1,i);
12 cont = true;
13 break;
14 }
15 if(cont)
16 continue;
17 if(reach[ ans.back() ][i])
18 ans.push_back(i);
19 else
20 ans.insert(ans.begin(),i);
21 }
22 for(int i=0;i<n;i++)
23 printf("%d%c",ans[i],i==n-1?'\n':' ');
24 }
