學習《算法設計與分析基礎》,習題2.4 第5題要求為漢諾塔游戲設計一個非遞歸的算法。
思,不得其解。看書后答案提示:
你如果做不到,也不要沮喪:這個問題的非遞歸算法雖然不復雜,但卻不容易發現。作為一種安慰,可以在因特網上尋找答案。
好吧,話都說得這么直接了,遂百度之,得到一個感覺很好的答案,略做修改,摘錄於下:
原文地址:http://blog.sina.com.cn/s/blog_48e3f9cd01000474.html
#################################################################################
#################################################################################
在版上看有人討論漢諾塔的非遞歸算法,有人介紹怎么樣非遞歸,自己想了半天,總算想明白了。整理了下方便大家:
漢諾塔問題介紹:
在印度,有這么一個古老的傳說:在世界中心貝拿勒斯(在印度北部)的聖廟里,一塊黃銅板上插着三根寶石針。印度教的主神梵天在創造世界的時候,在其中一根針上從下到上地穿好了由大到小的64片金片,這就是所謂的漢諾塔。不論白天黑夜,總有一個僧侶在按照下面的法則移動這些金片,一次只移動一片,不管在哪根針上,小片必在大片上面。當所有的金片都從梵天穿好的那根針上移到另外一概針上時,世界就將在一聲霹靂中消滅,梵塔、廟宇和眾生都將同歸於盡。
在印度,有這么一個古老的傳說:在世界中心貝拿勒斯(在印度北部)的聖廟里,一塊黃銅板上插着三根寶石針。印度教的主神梵天在創造世界的時候,在其中一根針上從下到上地穿好了由大到小的64片金片,這就是所謂的漢諾塔。不論白天黑夜,總有一個僧侶在按照下面的法則移動這些金片,一次只移動一片,不管在哪根針上,小片必在大片上面。當所有的金片都從梵天穿好的那根針上移到另外一概針上時,世界就將在一聲霹靂中消滅,梵塔、廟宇和眾生都將同歸於盡。
遞歸算法:
定義 void Hanoi(char src, char des, char via, int n)
表示把n個盤子從src上借助via移動到des上。
顯然有
表示把n個盤子從src上借助via移動到des上。
顯然有
void Hanoi(char src, char des, char via, int n) { Hanoi(src, via, des, n - 1); Move(src, des, n); //把第n個盤子直接從src移動到des Hanoi(via,des, src, n - 1); }
根據遞歸算法,設f(n)為n個盤子要移動的次數。
那么顯然 :
那么顯然 :
f(n + 1) = 2*f(n) + 1 -> [f(n + 1) + 1] = 2*[f(n) + 1] f(1) = 1,-> f(n) + 1 = (1 + 1)^n -> f(n) = 2^n - 1。 f(64)= 2^64-1=18446744073709551615
假如每秒鍾一次,共需多長時間呢?一年大約有 31536926 秒,計算表明移完這些金片需要5800多億年,比地球壽命還要長,事實上,世界、梵塔、
廟宇和眾生都已經灰飛煙滅。
非遞歸算法:
定義從小到大的盤子序號分別為1,2,……n。
可以用一個1到2^n - 1的2進制序列可以模擬出n個盤子的漢諾塔過程中被移動的盤子的序號序列。
定義從小到大的盤子序號分別為1,2,……n。
可以用一個1到2^n - 1的2進制序列可以模擬出n個盤子的漢諾塔過程中被移動的盤子的序號序列。
即給定一個n,我們通過0到2^n - 1序列可以判斷出任意一步應該移動那個盤子。
判斷方法:第m步移動的盤子序號是m用二進制表示的最低位bit為1的位置。
判斷方法:第m步移動的盤子序號是m用二進制表示的最低位bit為1的位置。
證明: n = 1,顯然成立。
假設n = k 成立。
n = k + 1時,對應序列1到2^(k+1) - 1,顯然這個序列關於2^k左右對稱。
假設我們要把k + 1個盤子從A移動C。
那么2^k可以對應着Move(k + 1, A, C)。 1 到 2^k - 1 根據假設可以
對應Hanoi(A, B, C, k)。至於2^k + 1 到 2^(k + 1) - 1把最高位的1去掉對應序列變成1到2^k - 1,顯然2^k + 1 到 2^(k + 1) - 1和1到2^k - 1這兩個序列中的對應元素的最低位bit為1的位置相同。因此2^k + 1 到 2^(k + 1) - 1可以對應Hanoi(B, C,A,k)。
所以對n = k + 1也成立。
假設n = k 成立。
n = k + 1時,對應序列1到2^(k+1) - 1,顯然這個序列關於2^k左右對稱。
假設我們要把k + 1個盤子從A移動C。
那么2^k可以對應着Move(k + 1, A, C)。 1 到 2^k - 1 根據假設可以
對應Hanoi(A, B, C, k)。至於2^k + 1 到 2^(k + 1) - 1把最高位的1去掉對應序列變成1到2^k - 1,顯然2^k + 1 到 2^(k + 1) - 1和1到2^k - 1這兩個序列中的對應元素的最低位bit為1的位置相同。因此2^k + 1 到 2^(k + 1) - 1可以對應Hanoi(B, C,A,k)。
所以對n = k + 1也成立。
下面討論第m步應該移動對應的盤子從哪到哪?
定義順序為 A->B->C->A, 逆序為C->B->A->C。
定義順序為 A->B->C->A, 逆序為C->B->A->C。
性質對n個盤子的漢諾塔,任意一個盤子k(k <= n)k在整個漢諾塔的移動過程中要么一直順序的,要么一直逆序的。而且如果k在n個盤子移動過程的順序和k - 1(如果k > 1)以及k + 1(如果k < n)的順序是反序。
比如:n = 3
1 A->C 2 A->B 1 C->B 3 A->C 1 B->A 2 B->C 1 A->C
其中1的軌跡A->C->B->A>C逆序,2的軌跡A->B->C順序,3的軌跡A->C逆序
證明:假設n <= k成立
對於n = k + 1 根據遞歸算法
Hanoi(A,C,B,k + 1) = Hanoi(A, B, C, k) + Move(A, C, k + 1) + Hanoi(B, C,A,k);
整個過程中盤子k + 1只移動一次A->C為逆序對應着2^k。
對於任意盤子m < k + 1,
m盤子的移動由兩部分組成一部分是前半部分Hanoi(A, B, C, k)以及后半部分的Hanoi(B, C,A,k)組成。顯然有如果m在Hanoi(A, C, B, k)軌跡順序的話,則m在Hanoi(A, B, C, k)以及Hanoi(B, C,A,k)都是逆序。反之亦然。這兩部分銜接起來就會證明m在Hanoi(A,C,B,k)和Hanoi(A,C,B,k + 1)中是反序的。
同時有Hanoi塔中最大的盤子永遠是逆序且只移動1步,A->C。
這樣的話:
m盤子的移動由兩部分組成一部分是前半部分Hanoi(A, B, C, k)以及后半部分的Hanoi(B, C,A,k)組成。顯然有如果m在Hanoi(A, C, B, k)軌跡順序的話,則m在Hanoi(A, B, C, k)以及Hanoi(B, C,A,k)都是逆序。反之亦然。這兩部分銜接起來就會證明m在Hanoi(A,C,B,k)和Hanoi(A,C,B,k + 1)中是反序的。
同時有Hanoi塔中最大的盤子永遠是逆序且只移動1步,A->C。
這樣的話:
m = k + 1,在Hanoi(A,C,B,k + 1)中是逆序。
m = k,由於在Hanoi(A,C,B,k)中是逆序的,所以Hanoi(A,C,B,k + 1)中是順序的。
m = k - 1,由於在Hanoi(A,C,B,k - 1)是逆序的,所以Hanoi(A,C,B,k)是順序的,所以Hanoi(A,C,B,k + 1)是逆序的。
依次下去……
結論得證。
m = k,由於在Hanoi(A,C,B,k)中是逆序的,所以Hanoi(A,C,B,k + 1)中是順序的。
m = k - 1,由於在Hanoi(A,C,B,k - 1)是逆序的,所以Hanoi(A,C,B,k)是順序的,所以Hanoi(A,C,B,k + 1)是逆序的。
依次下去……
結論得證。
總結:在n個漢諾中n, n - 2, n - 4……是逆序移動,n - 1, n - 3,n - 5……是順序移動。
有了以上結論,非遞歸的程序就很好寫了。寫了個遞歸和非遞歸比較程序:
#include <iostream> using namespace std; void Hanoi(char src, char des, char via, int n) { if(n == 1) { cout << n <<" : "<< src <<" --> " <<des << endl; return; } Hanoi(src, via, des, n - 1); cout << n <<" : "<< src <<" --> " <<des << endl; Hanoi(via, des, src, n - 1); } int main() { int n; cin >> n; cout<<"recusive:"<< endl; Hanoi('A','C','B', n); cout << endl; cout<<"normal:"<<endl; char order[2][256]; char pos[64]; for(int i=0;i<64;i++) { pos[i]='A'; //初始的時候,所有的圓盤位置都是 'A'; } order[0]['A'] = 'B'; order[0]['B'] = 'C'; order[0]['C'] = 'A'; order[1]['A'] = 'C'; order[1]['B'] = 'A'; order[1]['C'] = 'B'; //0是順序 1是逆序 int index[64]; //確定軌跡的順序還是逆序 int i, j, m; for(i = n; i > 0; i -= 2) index[i] = 1; for(i = n - 1; i > 0; i -= 2) index[i] = 0; memset(pos, 'A', sizeof(pos)); for(i = 1; i < (1 << n); i ++) { for(m = 1, j = i; j%2 == 0; j/=2, m ++); //計算出當前步驟序號的最低的 bit 為 1 的位置。 cout << m <<" : "<< pos[m] <<" --> " << order[index[m]][pos[m]] << endl; pos[m] = order[index[m]][pos[m]]; //更改當前位置 } return 0; }
感嘆原作者提出的算法真是精妙。
另外補充:關於計算一個整數 bit 為 1 的最低位的問題,可以如下計算:
int lowestbit(int n) { int tmp=n-1; tmp=tmp^n; //假設n 的最低bit 為 1 的位為m,則此時 tmp 為低 m 位都為 1 、剩余高位都為 0 的數字。 tmp+=1; //此時 tmp= pow(2,m); return log2(tmp); }