【洛谷】P1242 新汉诺塔

汉诺塔问题

问题描述
相传在古印度圣庙中,有一种被称为汉诺塔(Hanoi)的游戏。该游戏是在一块铜板装置上,有三根杆(编号A、B、C),在A杆自下而上、由大到小按顺序放置64个金盘。游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。
汉诺塔问题

输入格式
输入数据为一个正整数,表示当前有n个金盘。

输出格式
输出为多行。
第一行为一个正整数w,表示将这n个金盘从A杆移动到C杆的最小步骤数。
接下来为w行,每行的格式为“from x to y”。其中x和y为A、B、C其中之一。

输入样例
2

输出样例
3
from A to B
from A to C
from B to C


算法分析
汉诺塔问题是理解分治算法最经典的一道题。根据分治算法的思想,第一步我们必须对原问题进行分解。在此之前,对大家进行一个提问:
把大象装进冰箱需要几步?
答案是:三步。打开冰箱门、把大象装进去、关闭冰箱门。
现在我要告诉你,求解汉诺塔问题也是三步。根据汉诺塔游戏的机制我们知道,必须先将第n个盘子移动到C杆上。但如果直接将第n个盘子移动到目标位置又是不行的,因为在这第n个盘子前还有n-1个盘子。此时,我们可以假设有一个函数function(),它可以帮我们将前面n-1个盘子移动到除目标位置以及起始位置外的那个杆子处(即B杆)。这样一来,我们就只需要做一件工作,将第n个盘子移动到C杆上。还没结束!因为我们还需要将刚才放到B杆上的前n-1层盘子也放到C杆上。即:
① 移走前n-1层盘子;
② 将第n层盘子移至目标位置;
③ 将前n-1层盘子移至目标位置。
接下来,当我们接手“搬第n-1层到C杆”这任务时也会面临同样的问题——前面的n-2层怎么办?一样的,交给函数function(),他会负责将前面n-2层搬到除目标位置以及起始位置外的那个杆子处(即A杆),此时,我们同样只需要将第n-1个盘子移动到C杆,并将前n-2层盘子也移至C杆即可……以此类推,层层划分下去,最终总会有一个人接到“搬第1层”的任务。第1层怎么搬?很简单,你想搬到哪儿就般到哪儿。换句话说,到这一步就不能再分了,这里可以直接给出答案。
现在我们把重心放在函数function()上。对于接到“搬第i层到x杆”这个任务的人而言,他实际上需要关心的问题就3个:我是谁?我在哪儿?要到哪儿去?(哲学的经典三问)。对应到具体的题目中则是:要搬第几层?该层的所在位置?该层的目标位置?而这三个信息对是会随着当前层次的不同而发生改变,因此,这三个信息构成了函数function()的形参表。在函数function()中,当搬运的层次位于第1层时,其无需再划分,而是直接执行。这实际上就是函数function()的返回条件。其余任意层次,则都需要进行划分(实际上,表现在代码中就是递归调用function()自身),划分后就只需要执行三步:外包(移开前n-1层)、做自己的活、外包(处理前n-1层)。最后,对于任意输入,调用该函数即可求解整个问题。

最后,关于汉诺塔问题的执行次数,这其实是一个数学归纳问题:
证:设移动n个圆盘所需的最少步数为F(n),若要把n个圆盘从A柱移至C柱,则可拆解为如下步骤:
① 将n-1个圆盘从A柱移至B柱,需要F(n−1)步;
② 将余下的最大圆盘从A柱移至C柱,需要1步;
③ 将n-1个圆盘从B柱移至C柱,需要F(n−1)步。
于是可得到最少移动步数的递推公式:

F(n) = 2F(n-1) + 1

易知F(1)=1,则可得通项公式:

F(n) = 2F(n-1) + 1 = 22F(n-2) + 2 + 1 = … = 2n-1 + 2n-2 + … + 2 + 1 = 2n-1

下面给出求解本体的完整代码:

#include<iostream>
#include<cmath>
using namespace std;

// n表示当前盘子上还有多少的盘子,a表示初始位置,b表示中间的过渡位置,c表示目标位置 
void hanoi(int n,char a,char b,char c)
{
    
     
	if(n==1) cout<<a<<"->"<<c<<endl;	// 如果当前只有1个盘子,就直接移动过去 
	else{
    
     								// 否则:外包、做自己的活、回归
		hanoi(n-1,a,c,b);				// 外包:首先把当前a位置上的n-1个盘子过渡到b上
		cout<<a<<"->"<<c<<endl;			// 干自己的活:然后将第n个盘子移动到c上
		hanoi(n-1,b,a,c);				// 外包: 最后再将前n-1层放在c上
	}
}

int main()
{
    
    
	int n;cin>>n;
	cout<<pow(2,n)-1<<endl;
	hanoi(n,'A','B','C');
	return 0;
}


进阶题目



【洛谷】 P1242 新汉诺塔

问题描述
设有n(n≤45)个大小不等的中空圆盘,按从小到大的顺序从1到n编号。将这n个圆盘任意的迭套在三根立柱上,立柱的编号分别为A、B、C,这个状态称为初始状态。
现在要求找到一种步数最少的移动方案,使得从初始状态转变为目标状态。
移动时有如下要求:
1、一次只能移一个盘;
2、不允许把大盘移到小盘上面。

输入格式
文件第一行是状态中圆盘总数;
第二到第四行分别是初始状态中A、B、C柱上圆盘的个数和从下到上每个圆盘的编号;
第五到第七行分别是目标状态中A、B、C柱上圆盘的个数和从下到上每个圆盘的编号。

输出格式
每行一步移动方案,格式为:move I from P to Q
最后一行输出最少的步数。

样例输入
5
3 3 2 1
2 5 4
0
1 2
3 5 4 3
1 1

样例输出
move 1 from A to B
move 2 from A to C
move 1 from B to C
move 3 from A to B
move 1 from C to B
move 2 from C to A
move 1 from B to C
7


算法分析
本题相较于经典的汉诺塔问题新增加了三个难点:
① 待移动的盘子并不仅仅在某个柱子上,而是离散分布的;
② 盘子移动的目标状态也可能是离散分布的;
③ 求解最优方案(即步数最小的方案)。
这时,由于盘子不仅仅分布于某根柱子上,且目标状态也是离散的,这就使得我们需要设计出一种数据结构来保存这两种状态,并且要求这种数据结构能够参与到算法在执行时的状态更替中。一种很直接的办法是用二维数组来1:1还原。例如,对于样例数据,可以有(设所有的索引都从1开始):

// 原始状态
original[1][]={“0”, “3”, “2”, “1”};
original[2][]={“0”, “5”, “4”};
original[3][]={“0”};

// 目的状态
target[1][]={“0”, “2” };
target[2][]={“0”, “5” , “4” , “3”};
target[3][]={“0”, “1” };

二维数组的优点是可以很直观地能看到每根柱子上盘子的分布情况,但是缺点很明显。在前面的经典汉诺塔问题中,我们知道每次在划分问题时,目标都是找最大的那个圆盘来进行位置变更。在那种情况下,由于圆盘的初始放置情况仅在一根柱子上,所以递归函数在设计时可以将问题划分为三步:
① 先将当前盘子的前n-1个盘子移至过渡柱子;
② 再移动当前盘子的柱子;
③ 最后将前n-1个盘子移至目的柱子。
这样一来,该递归函数就变得十分简单(只需三步)。在本题中,盘子的放置并不一定满足这样的规则。此时,如果仍然采用1:1还原初始情况的二维数组来作为本题的数据结构,那会使得递归算法的设计变得相当困难(在original[ ]数组中寻找最大盘子只能遍历枚举,浪费时间)。因此,我们必须转变思路,想办法设计一种合理的数据结构来将盘子的离散分布给“屏蔽”掉(这里的屏蔽是指,不关心盘子的分布,而将重点放在“如何去找当前未被放置到正确位置上的最大盘子”)。
试想,如果我们站在盘子的角度,用盘子编号作为数组索引,用数组值来表示该盘子所在的柱子。这样一来我们就可以根据数组索引直接逆序遍历该数组,并进行位置变更。此时,因为盘子编号与其大小相对应(且唯一),则逆序遍历时先遇到的索引就表达着“当前盘子是目前最大的”这一含义(根据贪心的思想,由于题目要求最优解,故每次都需要先安排最大圆盘)。这样一来,我们在设计递归函数时就可以采用和经典汉诺塔问题相似的3步。稍有不同的是,由于本题中盘子的初始和目的状态都是离散的,因此在对我们设计的数组进行递归遍历时,需要用一层外循环来控制遍历过程,达到“屏蔽”盘子离散分布的目的。
下面给出本题的完整代码:

#include<iostream>
using namespace std;

// first[i],last[i]数组分别存放盘子i所在的初始柱子和目的柱子
int first[50],last[50],ans=0; 

// ch[]数组将柱子的编号(A、B、C)转换为数字(1、2、3)
char ch[]={
    
    '0','A','B','C'};

// 编号为n的盘子要去tar柱子
void dfs(int n,int tar)
{
    
    
	// 初始位置是目标位置则不用移动
	if(first[n]==tar)	return;	
	// 先移动编号为1~n-1的这些盘子到除当前盘子所在位置和目标位置之外的第三个位置
	// 由于这三个位置的数字代号满足1+2+3=6,因此知道其中任意两个参数便可以求出第三个位置
	for(int i=n-1;i>=1;i--)	dfs(i,6-first[n]-tar);	
	// 打印信息
	cout<<"move "<<n<<" from "<<ch[first[n]]<<" to "<<ch[tar]<<endl;
	//移动之后需要将其位置改变,并且计下步数
	first[n]=tar,ans++; 
 } 

int main()
{
    
    
	int n,m,x;
	cin>>n;
	// 记录初始状态
	for(int i=1;i<=3;i++){
    
    
		cin>>m;
		for(int j=1;j<=m;j++)	
			cin>>x,first[x]=i;
	}
	// 记录目标状态
	for(int i=1;i<=3;i++) {
    
    
		cin>>m;
		for(int j=1;j<=m;j++)
			cin>>x,last[x]=i;
	}
	// 要求最优解,则最小步数必须是从最大盘子开始(贪心)
	for(int i=n;i>=1;i--)
		dfs(i,last[i]); 
	cout<<ans<<endl;
	return 0;
 }

END


猜你喜欢

转载自blog.csdn.net/the_ZED/article/details/125526839
今日推荐