Towers of Hanoi題解

咸鱼爱学习發表於2024-04-26

Towers of Hanoi

題面翻譯

給定盤子數\(n\)和步數\(m\),求漢諾塔移動\(m\)步之後,三根柱子上各有多少個盤子。

\(n\)最大為\(100\)\(m\)最大為\(2^n-1\).

題目描述

PDF

輸入格式

輸出格式

樣例 #1

樣例輸入 #1

3 5
64 2
8 45
0 0

樣例輸出 #1

1 1 1
62 1 1
4 2 2

題目分析

仔細閱讀題面,可知目的為:漢諾塔中 \(n\) 個盤子移動 \(m\)​ 步之後,三根柱子上各有多少個盤子。

首先,漢諾塔問題的遞迴解決步驟為:

  1. 先將上面的 \(n-1\) 個盤子從起點柱子移動到中轉柱子。
  2. 然後將最底下、最大的盤子從起點柱子移動到目標柱子。
  3. 最後將 \(n-1\) 個盤子從中轉柱子移動到目標柱子。

程式碼框架為:

/*
n : 盤子數量
from : 起點柱子
to:目標柱子
tmp:中轉柱子
*/
void hanoi(int n,int from,int to,int tmp){
	if(n==1){
		cout<<from<<"->"<<to<<endl;
		return ;
	}
	hanoi(n-1,from,tmp,to);
	hanoi(1,from,to,tmp);
	hanoi(n-1,tmp,to,from);
}

由此,我們可以使用模擬的方式來統計各個柱子上盤子的數量,但是盤子數量最多為 \(100\) 這麼遞迴下去會超時,我們需要找到更快的方法。

從漢諾塔的遞迴實現,我們來尋找一下執行次數的規律。設 \(f_n\)\(n\) 個盤子的移動次數,從遞迴的實現過程可得到移動次數的計算公式:\(f_n=f_{n-1}+1+f_{n-1}=2\times f_{n-1}+1\)。且 \(f_1=1\),手動遞推計算一下可發現 \(n\) 個盤子的執行次數為 \(2^n-1\)

這個發現能否對於程式最佳化帶來幫助呢?

還是回到原始的遞迴實現漢諾塔的部分,程式分為了 \(3\) 個部分,對於第一部分將 \(n-1\) 個盤子進行移動的部分根據我們剛剛求到的公式可計算出總的執行次數為 \(2^{n-1}-1\),若步驟數 \(m>2^{n-1}-1\) 我們完全不用去模擬這 \(n-1\) 個盤子的具體移動過程,直接將這 \(n-1\) 個盤子進行整體的移動即可。若是 \(m\le2^{n-1}-1\) 則第三步移動 \(n-1\) 個盤子從中轉柱子到目標柱子的部分也不用去做了,因為執行不到那就會有 \(m\) 次了。

依據這樣的思路,不斷縮小問題的規模,直到計算出執行次數為 \(m\) 位置,我們可以採用遞迴的方式進行實現。

另外題目中注意一些易錯的細節,首先是資料範圍,\(m\) 最多為 \(2^n-1\) 會超過 long long 我們可以使用 __int128 來進行儲存;其次是注意題目中要求盤子總數是偶數時正常從AC,而總數為奇數時則是從AB透過C中轉。

程式碼實現

#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
using i128 = __int128;
int cnt[3];
i128 m;
i128 read(){
	i128 num=0;
	char c=getchar();
	while(c<'0' ||c>'9') c=getchar();
	while(c>='0' && c<='9'){
		num=num*10+c-'0';
		c=getchar();
	}
	return num;
}

void hanoi(i128 sum,int n,int from,int to,int tmp){
	//sum-總的執行次數 n-盤子數量 from-起點柱子 to-目標柱子 tmp-中轉柱子
	if(sum==m) return ;//次數到了就結束
	i128 num=1;
	num=(num<<(n-1))-1;//計算(n-1)個盤子的移動次數 2^{n-1}-1
	if(sum+num>m){//m較小,遞迴模擬
		hanoi(sum,n-1,from,tmp,to);
		return ;
	}
	//上面n-1個盤子移動結束了,還沒有m次,就直接整體移動,不用一步步模擬
	//n-1個從from 移動到tmp
	cnt[from]-=n-1;
	cnt[tmp]+=n-1;
	
	if(sum+num==m){//移動完n-1個後正好m次
		return ;
	}
	//1個 從 from 移動到 to
	cnt[from]--;
	cnt[to]++;
	
	if(sum+num+1==m){//移動完底下的正好m次
		return ;
	}
	// 上面兩步移動完還沒有m次,則遞迴模擬
	hanoi(sum+num+1,n-1,tmp,to,from);
}
int main(){
	i128 n;
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	while(1){
		n=read();
		m=read();
		if(n==0 && m==0) break;
		cnt[0]=n;cnt[1]=cnt[2]=0;
		if(n&1)
			hanoi(0,n,0,2,1);
		else
			hanoi(0,n,0,1,2);
		cout<<cnt[0]<<" "<<cnt[2]<<" "<<cnt[1]<<endl;
	}
	return 0;
}