Chapter 3 | Stacks and Queues--栈解决汉诺塔问题

3.4  In the classic problem of the Towers of Hanoi, you have 3 rods and Ndisks of different sizes which can slide onto any tower. The puzzle starts with disks sorted in ascending order of size from top tobottom (e.g., each disk sits on top of an even larger one). You have the following constraints:
  • Only one disk can be moved at a time.
  • A disk is slid off the top of one rod onto the next rod.
  • A disk can only be placed on top of a larger disk.

Write a program to move the disks from the first rod to the last using Stacks

译文:就是利用栈这一数据结构来解决汉诺塔问题。

汉诺塔是一个经典问题:有三根杆子编号 A,B,C,A 上有N(N>1)穿孔圆盘,盘的尺寸由下到上依次变小,呈塔形,要求按以下规则将盘移动到C:

  • 每次只能移动一个盘
  • 盘只能从顶部移动到其余杆
  • 大盘不能叠在小盘上面

利用栈解决汉诺塔问题。

Chapter 3 | Stacks and Queues--栈解决汉诺塔问题_第1张图片

一、递归

对于汉诺塔这个经典问题,我们先用递归来实现(递归:不必纠结于内部实现,明确其功能即可

如上图所示,要将 A 杆的 n 个圆盘移动到 C 盘,B 盘则是作为一个中介,假定初始状态为(1~n, 0, 0),那么其最终状态为(0, 0, 1~n)。考虑到递归实现,则必然会有这样一个中间状态(n,1~n-1,0),然后其下一个状态就是(0,1~n-1,n),编号 n 的最大盘位置已经正确,这样等效于(0,1~n-1,0)n位置已经确定可忽略,这个状态和初始状态如出一辙,此时 A 盘成了中介盘,然后就是(1~n-2,n-1,0),接下来便是(1~n-2,0,n-1)已经确定好位置的忽略,这样 n 和 n-1 位置确定,忽略,就变成了初始状态(1~n-2,0,0)。实际状态为(1~n-2,0,n-1~n)。当 A 只有一个盘子,也就是状态为(n,0,0)时(位置确定的忽略)直接移动到 C 盘,当 n = 1 时,为递归的终止条件。

将上面整理一下:移动过程中有三个状态

初始状态:(1~n,0,0)

中间状态:(n,1~n-1,0)

最终状态:(0,0,1~n)

先定义汉诺塔函数原型

void hanoi(int n, char A, char B, char C);
函数的功能就是:A 借助 B 移动 n 个盘到 C。(其中 n A B C 均表示函数 hanoi 对应位置参数,而不是某个具体盘)

总体:很明显初始状态(1~n,0,0)达到最终状态(0,0,1~n)就是 

hanoi(n, A, B, C);    //A 借助 B 移动 n 个盘到 C
分步一:先要从初始状态(1~n,0,0)进入中间状态(n,1~n-1,0)就是

hanoi(n-1, A, C, B);  //A 借助 C 移动 n-1 个盘到 B
分步二:然后从中间状态(n,1~n-1,0)达到最终状态(0,0,1~n),由于此时的中间状态 A 上只有一个盘子,且为未确定位置中的最大盘子,则直接将该盘子移动到 C,然后进入下一轮的递归。所以接下来进入递归的是这样一个情况:

从中间状态(0,1~n-1,0)达到最终状态(0,0,1~n)就是

hanoi(n-1, B, A, C);  //B 借助 A 移动 n-1 个盘到 C
每一轮后有一个盘子确定好位置,即下一轮只需考虑 n-1 个盘子。

下面贴程序:

这里修改一下函数原型,使其可以返回移动的次数。

using namespace std;

void hanoi(int n, char A, char B, char C, int *cnt)
{
	if (1 == n)
	{
		++(*cnt);
		cout << "直接将编号为" << n << "的盘子从" << A << "移动到" << C << endl;
	}
	else
	{
		++(*cnt);
		hanoi(n - 1, A, C, B, cnt);
		cout << "直接将编号为" << n << "的盘子从" << A << "移动到" << C << endl;
		hanoi(n - 1, B, A, C, cnt);
	}
}

int main()
{
	int n;
	int cnt = 0;
	cout << "输入要移动的盘子个数 n = ";
	cin >> n;

	hanoi(n, 'A', 'B', 'C', &cnt);
	cout << "移动的总次数为:" << cnt << endl;

	return 0;
}
输入数值的时候,不要手一抖输入 64 或其余相对大的值,不然。。。

二、栈

这里就是真正的解答题目了,递归是一个天然栈,我们可以沿用前面递归的思想,将其通过栈来实现。

前面讨论到汉诺塔的移动要经过以下几个状态

初始状态:(1~n,0,0)

中间状态:(n,1~n-1,0)

次中间状态:(0,1~n-1,n)

最终状态:(0,0,1~n)

上面的第二个中间状态是当 A 只有一个盘子时,就直接将其移动到 C 盘。

这里是将移动过程状态压栈,首先需要定义一个数据结构来保存这些操作的过程状态。

struct oprt
{
	int begin, end;
	char src, bri, dst;

	oprt(){}
	oprt(int _begin, int _end, char _src, char _bri, char _dst)
		:begin(_begin), end(_end), src(_src), bri(_bri), dst(_dst){}
};
上面的 begin 和 end 表明 src 参数位置盘子的起止位置,当(begin == end)表示 src 参数只有一个盘子,这里指的是第三个参数标明的杆的盘子情况。

后面三个参数位置表示:src 通过 bri 将盘子移动到 dst。(将src,bri,dst 换为参数指定的位置更容易理解)

前面说明有四个状态,那么就有三个过程状态,分别为:

初始状态 --> 中间状态:(1~n,0,0) --> (n,1~n-1,0)

中间状态 --> 次中间状态:(n,1~n-1,0) --> (0,1~n-1,n)

次中间状态 --> 最终状态:(0,1~n-1,n) --> (0,0,1~n)

其中,中间状态 --> 次中间状态,A 杆只有一个盘子,那么直接将该盘子移动到 C 盘即可(同前面递归一样)。

这些过程需要进行压栈出栈处理,压栈的时候不作处理,出栈时进行处理,所以在压栈顺序与实际顺序相反。一开始我们把目的压栈,即(初始状态-->最终状态)压栈,然后将其弹出,并判断其是否为原子操作,也就是说是否只需移动一个盘,如果是则直接移动,否则将其分解为三个状态,然后再行判断,直至操作为原子操作。

void hanoi(int n, char src, char bri, char dst, int *cnt)
{
	stack<oprt> Stack;
	oprt temp;
	Stack.push(oprt(1, n, src, bri, dst));   //初始状态 ——> 最终状态 

	while (!Stack.empty())
	{
		temp = Stack.top();
		Stack.pop();

		if (temp.begin != temp.end)  //非原子操作,分解
		{			
			//次中间状态 ——> 最终状态
			//将B中1~n-1的盘子通过A移动到C
			Stack.push(oprt(temp.begin, temp.end - 1, bri, src, dst));

			//中间状态 ——> 次中间状态
			//将A中编号n的盘子通过B移动到C
			Stack.push(oprt(temp.end, temp.end, src, bri, dst));

			//初始状态 ——> 中间状态
			//将A中1~n-1的盘子通过C移动到B
			Stack.push(oprt(temp.begin, temp.end - 1, src, dst, bri));
		}
		else   //原子操作,直接移动
		{
			cout << "Move disk " << temp.begin << " from " << temp.src << " to " << temp.dst << endl;
			++(*cnt);    //移动次数
		}
	}
}
原子操作也叫不可分割的操作,这里是说只移动一个盘子。上面程序需要理解的是压栈保存的是过程状态,不是单一的状态。


你可能感兴趣的:(递归,栈,coding,汉诺塔,the,Cracking)