漢諾塔問題學習報告

漢諾塔問題學習報告

  • (如解析和參考有錯誤或可以精進之處,歡迎批評指正。)

法國數學家愛德華·盧卡斯曾編寫過一個印度的古老傳說:在世界中心貝拿勒斯(在印度北部)的聖廟裏,一塊黃銅板上插着三根寶石針。印度教的主神梵天在創造世界的時候,在其中一根針上從下到上地穿好了由大到小的64片金片,這就是所謂的漢諾塔。不論白天黑夜,總有一個僧侶在按照下面的法則移動這些金片:一次只移動一片,不管在哪根針上,小片必須在大片上面。僧侶們預言,當所有的金片都從梵天穿好的那根針上移到另外一根針上時,世界就將在一聲霹靂中消滅,而梵塔、廟宇和衆生也都將同歸於盡。
不管這個傳說的可信度有多大,如果考慮一下把64片金片,由一根針上移到另一根針上,並且始終保持上小下大的順序。這需要多少次移動呢?
(來源 百度百科)

這就是最基本的漢諾塔問題。

一.基本漢諾塔的最少移動步數求解
一個未移動過的漢諾塔模型。例題1:現在有一個N層的漢諾塔,要解決這個漢諾塔問題,至少要移動多少步? (難度1)

漢諾塔的特點就在於小的只能放在大的上面,這也是解題的關鍵。
設a[N]表示解決N層漢諾塔的最少步數,每一層表示爲a,b,c,…。
顯然a[1]=1。
解決2層的時候,先把a放在B柱以騰出C柱,然後把b放在C柱,再把a放到C柱。因此a[2]=3。
那麼怎麼求解a[3]呢?我們依照a[2]的思路來考慮:爲了完成任務,我們需要把c放到C柱,在此之前a、b應該已經放在B柱(否則不可能使c被拿出來),然後再把a、b疊在C柱。移動a、b的過程中完全可以忽略c的影響,因此解法就變爲:先使a、b組成的2層漢諾塔ab疊在B柱,再把c放在C柱,最後把ab放在C柱。因此a[3]=a[2]+1+a[2]=2 * a[2]+1。
綜上所述,對於一個N層的漢諾塔,先在B柱完成一個(N-1)層的漢諾塔,然後把n放在C柱,最後在B柱完成一個(N-1)層的漢諾塔,a[N]=2 * a[N-1]+1。(核心結論1)
這樣一來,基本漢諾塔求解的遞推式已經得出,剩下的就不是難事了。
核心代碼:






int a[101],n;
scanf("%d",&n);
a[0] = 0;
for(i = 1;i <= n;i++){
   
   
	a[i] = 2 * a[i - 1] + 1;
}
printf("%d",a[n]);

(注意:遞推法不要忘記給起始條件)

除此之外,根據算出的結果,還可以發現a[N]=2^N-1(使用的時候可能需要配合快速冪)。這樣就得出了一種漢諾塔步數問題的結果式。

因此我們可以知道,要想毀滅世界(我相信他們做這個的目的不是爲了毀滅世界而只是爲了忽悠人),需要操作漢諾塔2^64-1次。假設這羣僧侶能做到每時每刻都有人在操作它,而且手速快到2秒中就能操作一次、耐心到即使一直在操作也不會搞錯,也需要上萬億年。這是什麼概念,假設僧侶從這一刻開始操作,那麼即使到了太陽毀滅的那一天(這應該纔是真的世界末日),他們也纔剛操作完5%。

和基本的漢諾塔直接相關的還有雙塔問題(無非就是原式的基礎上全部乘2)。但是值得思考的是下面這個問題:

思考題:小明同學已經學會了漢諾塔怎樣操作最快,現在他買了兩套漢諾塔,一套爲白色,一套爲黑色。他把這兩套漢諾塔各取出N層大小完全相同的部分,如下圖所示擺放。若同樣大小的層可以互相堆疊,且堆疊時不必考慮顏色差別,該漢諾塔最快需要多少步才能完成,此時顏色是否與原來的分佈完全一致?(難度2)
思考題
很顯然,顏色與原來的分佈是不一致的,因爲如果不考慮顏色,完全可以直接把不同色的同樣大小的兩層看作一層,就是雙塔問題;但是如果考慮顏色,且希望在整個操作過程中都嚴格要求同色不能疊加,其實就是一個2N層的單塔;如果過程中不作要求,最下方的一層也肯定要分成黑白兩色操作。篇幅有限,具體的最少步數在此就不作說明了。

二.漢諾塔操作的模擬

漢諾塔模擬的難度是大幅高於求解的難度的,爲了模擬這一過程,既需要我們明白漢諾塔問題求解的根本方法,也需要我們熟練地掌握遞歸(顯然後者更難一些)。

例題2:現在有一個N層的漢諾塔,單數層爲白色,雙數層爲黑色。現在要將這個漢諾塔移動到B柱上,且操作過程中不允許相同顏色的層疊在一起。試模擬這個漢諾塔的操作過程,並在最後輸出總操作步數。(難度3)
例題2
首先需要說明的是,這題的顏色純粹是一個干擾項,因爲即使沒有這個限制在操作的時候也是一樣的(原因下面會解釋到)。
根據我們得出的核心結論,解決各種漢諾塔問題的關鍵就在於第N層這個分界點。 同樣來思考這個模擬,我們肯定要保證最終n從A移動到B,而爲此,我們需要在此之前把所有n上面的這(N-1)個層全部移到C柱去,但是爲了把n-1移到C柱去,還得保證n-2要移到B柱去……如上,一個前半部分的遞歸方案就基本成型了。後半部分同理,(N-1)個層全部都在C柱,爲了使n-1到B柱,又需要使n-2到A柱……這個過程在上半部分回溯的時候就可以得到解決。
核心代碼如下:



int count;
void Move(int n, char a, char b){
   
   
    count++;
    printf("%d %c %c\n",n,a,b);
}
void Hanoi(int n, char a,char b,char c){
   
   //註解
    if(n == 1) Move(n,a,c);
    else{
   
   
    	Hanoi(n - 1,a,c,b);
    	Move(n,a,c);
		Hanoi(n - 1,b,a,c);
    }
}
int main(){
   
   
    int n;
    scanf("%d",&n);
    Hanoi(n,'A','B','C');
    printf("%d",count); 
    return 0;
}

(注:a相當於起點,b相當於終點,c相當於中轉)

解決了這一問題,再來看下面這個問題:

例題3:(P1242 新漢諾塔)(難度5)
例題3
此題又是一個模擬,這題由於不像例題2初始和結果那麼有規律,所以有很多東西需要我們運用剛纔的經驗。
根據上一道題的經驗(又或者說是沒有根據),我們最終的目的是把每一層放到他該去的地方,而且在此之前要騰出給他的空間。因此我們應該從大到小循環進入遞歸,直到每一層都歸位爲止。
同時我們要注意到這道題的無規律性,所有盤在移動過程當中的操作確確實實需要我們手動模擬,因此要即時地改變每一個盤的所在位置。
核心代碼如下:




struct yjx{
   
   
	int from,to;
}a[101];//記每一層所在位置爲from,目的地爲to
int n,sum;
void hanoi(int x,int y){
   
   //把x移動到y位置去
	if(a[x].from == y) return;//已到達的不必再排
	else{
   
   
		int i;
		for(i = x - 1;i >= 1;i--) hanoi(i,6 - (a[x].from + y));//註解1
		printf("move %d from %c to %c\n",x,a[x].from - 1 + 'A',y - 1 + 'A');//註解2
		a[x].from = y;
		sum++;
	}
}
int main(){
   
   
	......//輸入部分略過
	for(i = n;i >= 1;i--) hanoi(i,a[i].to);
	printf("%d",sum);
	return 0;
}//此題很容易混淆x和i、a[x].from和x,要多加註意

(注1:這裏的6-(a[x].from + y)其實求的是某個盤的當前位置和目的地以外的那個柱,因爲三個柱的和是6)
(注2:這裏的一番操作是要把1、2、3轉化爲A、B、C)

就在我們愉快地AC(對多數人來說應該也是AK)比賽之後,把這道題交回到洛谷上衝業績的時候,我們卻發現這隻能得到90分,打開最後一個點,給出了一個看起來相當人畜無害的輸入文件:
在這裏插入圖片描述
這個的結果顯然應該是五步,但是程序結果如下:
在這裏插入圖片描述
爲什麼?
(注意:以下完全屬於本人分析,因此可能存在大量極其複雜反人類的內容,難以接受不建議閱讀)
根據我們的程序,我們總是在爲更大的讓出位置,只有完全騰出位置纔會繼續向更小的考慮,程序當中就是先把1、2兩層堆到了B柱,完全騰出C柱後才繼續向A柱操作。但事實上這沒必要,其實最快的方案應該是3給12騰出A柱,讓1、2先在A柱操作完,然後把3移到C柱。
分析一下可以知道,之所以最快的方案比我們的代碼還快,是因爲我們的代碼當中總是移動小的,使大的先到目的地以防止討論大小;但這恰恰意味着,大的限制了小的到達目的地,使得小的總要先全部移到非目的地的柱才能進行下去。
在此之前,我們試驗了一下,對於我們90分的代碼,如果把輸入數據中3的初始位置就設成B柱,結果是4步,也就是最少步數。因此我們不必推翻過去的代碼,只要適當的插入操作,使得這個特殊的操作強行先於接下來的部分就可以了。
首先我們明確,如果這個盤本身可以以原方式操作,那麼就不必再進行特殊操作(畢竟特殊操作是插入的,錯誤的插入會導致強行多了一步完全沒用的操作)。因此就需要嚴密的討論。
這裏我們不妨舉出反例:什麼時候我們不能無視大小直接移動這個盤?
首先既然我們要求可以無視大小,那麼就得保證它是最上面的一個 (強行操作也是要符合基本法的) 。因此如果它不是,就不能特殊操作。
其次,我們騰出空間是因爲後面的盤可以直接到達目的地,如果壓根沒有哪一個盤到達它所在的位置,就不必騰出這個空間,不能特殊操作。
另外,由於我們要把這個盤放到一箇中轉柱,如果這個盤比中轉柱上的任意一個盤大,那就不能特殊操作;如果下面確實有比它大的,如果這些盤已經到了目的地,我們的特殊操作就沒有影響,可以操作;如果沒有到的話,以後還是要移走讓下面的大盤去目的地,也沒有影響,也可以操作。
考慮到這三點,就已經算是比較全面的了。綜合一下,可以得出以下的結論:
要想進行這個特殊操作,要保證我們要移動的盤所在的柱沒有任何的盤擋在上面,而且我們要移到的中轉柱上原來的盤都比它大(全稱);除此之外,要保證盤所在的位置是還未到達的至少一個盤的目的地(存在)。
因此特判如下:















void hanoi(int x,int y){
   
   
	if(a[x].from == y) return;
	else{
   
   
		int i;
		lipu = 0,telipu = 1;
		for(i = n;i >= 1;i--){
   
   
			if(a[i].from == 6 - (a[x].from + y) && i != x && a[i].from != a[i].to || a[i].from == a[x].from && i < x || a[i].to == 6 - (a[x].from + y) && a[x].to != a[x].from){
   
   
				lipu = 1;//前兩條
				break;
			}
		}
		if(lipu == 0){
   
   
			for(i = n;i >= 1;i--){
   
   
				if(a[x].from == a[i].to && x != i && a[i].from != a[i].to){
   
   
					telipu = 0;//第三條
					break;
				}
			}
		}
		......//特判條件:lipu和telipu都爲0

這個時候我們就已經戰勝了洛谷並拿到了AC。

只是我們還是要思考一下:
爲了使得總移動數儘可能少,要使得1到n-1這一堆的移動次數儘可能的少。因此如果有條件可以先移動大盤到目的地,這樣有可能使n-1堆少移動一次,節省很多操作;但是如果操作大盤的過程中n-1層反覆操作多組,操作就會變多。這一過程很難特判,因此我們嘗試分類比較,先按照從大到小的基本思想操作,再按照優先移大的思想操作,比較一下究竟哪一種操作少,然後單獨輸出這一種的操作過程就可以了。
核心代碼如下:

struct yjx{
   
   
	int from,to;
}a[3][101];
int n,sum,ans[3];
bool go;
void hanoi(int w,int x,int y){
   
   //註解1
	if(a[w][x].from == y) return;
	else{
   
   
		int i;
		for(i = x - 1;i >= 1;i--){
   
   
			if(a[w][i].from != 6 - (a[w][x].from + y)) hanoi(w,i,6 - (a[w][x].from + y));
		}
		if(go == 1) printf("move %d from %c to %c\n",x,a[w][x].from - 1 + 'A',y - 1 + 'A');
		a[w][x].from = y;
		sum++;
	}
}
int main(){
   
   
	for(i = n;i >= 1;i--) hanoi(1,i,a[1][i].to);
	ans[1] = sum;
	sum = 0;
	for(i = n;i >= 1;i--){
   
   
		if(a[2][i].from != a[2][i].to){
   
   
			hanoi(2,i,6 - (a[2][i].from + a[2][i].to));
			break;//只需要操作一次
		}
	}
	for(i = n;i >= 1;i--) hanoi(2,i,a[2][i].to);
	ans[2] = sum;
	go = 1;
	if(ans[1] <= ans[2]){
   
   
		for(i = n;i >= 1;i--) hanoi(0,i,a[0][i].to);
	}
	if(ans[1] > ans[2]){
   
   
		for(i = n;i >= 1;i--){
   
   
			if(a[0][i].from != a[0][i].to){
   
   
				hanoi(0,i,6 - (a[0][i].from + a[0][i].to));
				break;
			}
		}
		for(i = n;i >= 1;i--) hanoi(0,i,a[0][i].to);
	}
	printf("%d",min(ans[1],ans[2]));
	return 0;
}

(注1:這裏多了一個w,是用來記錄代表的是方法1,方法2還是答案。因爲我們要操作三次,每次都是從初始狀態開始操作,因此需要一個二維數組,省去初始化的麻煩)
這道題給了我們一個啓發:
一個漢諾塔問題有時可以拆分成多個塔的操作,爲了使一個漢諾塔整體操作步數儘可能少,要使其層數最多的那一部分操作次數儘可能少。(核心結論2)

得到了又一個利器,來看本篇最後一道題:
例題4(奇怪的漢諾塔):
小華現在有一個奇怪的漢諾塔,這個漢諾塔有4個柱子。現在小華有一個N層的漢諾塔,試計算出解決這個漢諾塔問題需要的最少步數。
(難度3)
這道題相比剛纔那些費腦筋的遞歸還是要簡單一些的。現在我們面對的是一個四柱問題,但是如果我在某一柱上放下M個,剩下的(N-M)個就構成了一個三柱問題。記res[i]爲四柱的最少步數,res1[i]爲三柱的最少步數(這一數組應該已經提前準備好值),有遞推式爲:
res[i]=res[j]+res1[i-j]
這樣問題就得到解決了。



總而言之,漢諾塔問題是一個前後規律性強而又能千變萬化的題型,對我們的遞歸和遞推以至於數學邏輯能力都是不小的考驗,要善於化總體爲部分、化複雜爲簡單。
Thank you for reading!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章