PTA 汉诺塔的非递归实现
- 7-11 汉诺塔的非递归实现 (25分)
借助堆栈以非递归(循环)方式求解汉诺塔的问题(n, a, b, c),即将N个盘子从起始柱(标记为“a”)通过借助柱(标记为“b”)移动到目标柱(标记为“c”),并保证每个移动符合汉诺塔问题的要求。
输入格式:
输入为一个正整数N,即起始柱上的盘数。
输出格式:
每个操作(移动)占一行,按柱1 -> 柱2的格式输出。
输入样例:
3
输出样例:
a -> c
a -> b
c -> b
a -> c
b -> a
b -> c
a -> c
如下面这段代码一样。递归代码往往都十分简洁,分析出递归出口和递归规则以后也比较容易实现,但是每一次的递归调用都需要压栈占用内存,效率不高.所以才需要递归转非递归来提高效率,减轻函数栈的负担。
#include<iostream>
#include<cstdio>
using namespace std;
void move(int n, char x, char y, char z);
//将n个盘子从x借助y移动到z
int main()
{
int n;
scanf("%d", &n);
move(n, 'a', 'b', 'c');
return 0;
}
void move(int n, char x, char y, char z)
{
if(n == 1)
printf("%c -> %c\n",x,z);
else
{
move(n - 1, x, z, y); //将n-1个盘子从x借助z移动到y上
printf("%c -> %c\n",x,z); //将最底下的第n个盘子从x移动到z上
move(n - 1, y, x, z); //将n-1个盘子从y借助x移动到z上
}
}
递归求解汉诺塔问题,能解决问题,但不是我们真正想做的。
非递归求解汉诺塔问题给出以下三种方案。
1.方案一:
这段代码是实现了一位美国学者对于汉诺塔问题提出的解决方案(第一次写不太会写非递归,就去百度翻,翻到了这个自己觉得可以实现的算法),用3个stack栈来模拟三根柱子,用一个长度为3的char数组对应三根柱子的名字(‘a’,‘b’,‘c’),一方面便于输出由哪一根移动到哪一根,另一方面也是由于这种算法会因为盘子数的奇偶来简单调整后两根柱子的顺序。
算法介绍:一位美国学者发现一种出人意料的简单方法,只要轮流进行两步操作就可以了。
若n为偶数,柱子摆放为 A B C;若n为奇数,柱子摆放 A C B。
(1)把圆盘1从现在的柱子移动到下一根柱子,即当n为偶数时,若圆盘1在柱子A,则把它移动到B;若圆盘1在柱子B,则把它移动到C;若圆盘1在柱子C,则把它移动到A。
(2)接着,把另外两根柱子(圆盘1移动前所在的柱子以外的那两根柱子)上可以移动的圆盘移动到新的柱子上。即把非空柱子上的圆盘移动到空柱子上;当两根柱子都非空时,移动较小的圆盘。
(3)反复进行(1)(2)
操作3阶汉诺塔的移动:A→C,A→B,C→B,A→C,B→A,B→C,A→C
#include <iostream>
#include <stack>
#include <cstdio>
using namespace std;
char column_name[3] = {'a', 'b', 'c'};
stack<int> column[3];
int main()
{
int n, move_count = 0, flag = 2, temp;
scanf("%d", &n);
for(int i = 0; i < n; i++)
{
column[0].push(n - i);//将原盘从大到小放到第一根柱子上
}
if(n % 2 == 1)//如果n是奇数,交换后两根柱子的位置
{
char alpha;
alpha = column_name[1];
column_name[1] = column_name[2];
column_name[2] = alpha;
flag = 1;
}
while(true)
{
if((int)column[flag].size() != n)//对应算法中的步骤(1),移动圆盘1
{
printf("%c -> %c\n", column_name[(move_count) % 3], column_name[(move_count + 1) % 3]);
temp = column[(move_count) % 3].top();
column[(move_count) % 3].pop();
column[(move_count + 1) % 3].push(temp);
}
//对应算法中的步骤(2),把另外两根柱子上可以移动的圆盘移动到新的柱子上
if((!column[(move_count) % 3].empty()) && (!column[(move_count + 2) % 3].empty()))
{//当两根柱子都非空时,移动较小的圆盘。
if(column[(move_count) % 3].top() - column[(move_count + 2) % 3].top() > 0 && (int)column[flag].size() != n)
{
printf("%c -> %c\n", column_name[(move_count + 2) % 3], column_name[(move_count) % 3]);
temp = column[(move_count + 2) % 3].top();
column[(move_count + 2) % 3].pop();
column[(move_count ) % 3].push(temp);
}
else if(column[(move_count) % 3].top() - column[(move_count + 2) % 3].top() < 0 && (int)column[flag].size() != n)
{
printf("%c -> %c\n", column_name[(move_count) % 3], column_name[(move_count + 2) % 3]);
temp = column[(move_count ) % 3].top();
column[(move_count ) % 3].pop();
column[(move_count + 2) % 3].push(temp);
}
}
else
{
if(column[(move_count) % 3].empty() && (int)column[flag].size() != n)
{
printf("%c -> %c\n", column_name[(move_count + 2) % 3], column_name[(move_count) % 3]);
temp = column[(move_count + 2) % 3].top();
column[(move_count + 2) % 3].pop();
column[(move_count ) % 3].push(temp);
}
else if(column[(move_count + 2) % 3].empty() && (int)column[flag].size() != n)
{
printf("%c -> %c\n", column_name[(move_count) % 3], column_name[(move_count + 2) % 3]);
temp = column[(move_count ) % 3].top();
column[(move_count ) % 3].pop();
column[(move_count + 2) % 3].push(temp);
}
}
move_count++;
if((int)column[flag].size() == n)
break;
}
return 0;
}
可能光是文字还是不太好理解,以三个圆盘为例具体的移动过程如下。
A->C
A→B
C→B
A→C
B→A
B→C
A→C
2.方案二
从我的一位同学那里学习到的,代码的作者是他,我简单做了些修改,添加了一些注释便于理解。比第一种的可迁移性要高很多,理解起来的难度可能和第一个差不多(因人而异把,我自己觉得第一个可能更好理解些嘻嘻),因为是去模拟函数栈的工作过程,所以代码也相当简洁。
与递归函数高度对应,具体怎么对应参见主函数注释,用一个自己写的stack来辅助模拟函数递归的过程,有种生拆的感觉O(∩_∩)O哈哈~。
#include <iostream>
#include <cstdio>
using namespace std;
const int defaultSize = 10000;
class hanoi
{
public:
int n;
char a1;
char a2;
char a3;
hanoi(int _n = 5):n(_n), a1('a'), a2('b'), a3('c'){}
hanoi(int _n, char _a1, char _a2, char _a3):n(_n),
a1(_a1), a2(_a2), a3(_a3){}
};
class Stack
{
hanoi* data;
int top;
int maxSize;
public:
Stack(int s_size = defaultSize):data(NULL), top(-1), maxSize(defaultSize)
{
data = new hanoi[s_size];
}
bool IsFull(){return top == (maxSize - 1);}
bool IsEmpty(){return top == -1;}
bool Pop()
{
if(IsEmpty())
return false;
top--;
return true;
}
bool Push(hanoi x)
{
if(IsFull())
return false;
data[++top] = x;
return true;
}
hanoi getTop(){return data[top];}
};
int main()
{
int n;
scanf("%d", &n);
if(n < 1)
return 0;
Stack a;
hanoi x(n);
a.Push(x);//对应主函数中的传参,因为只有1个参数的构造函数会将三根柱子
//依次赋值为'a','b','c',所以依然是对应的
while(!a.IsEmpty())
{
if(a.getTop().n == 1)
{
//cout << a.getTop().a1 << " -> " << a.getTop().a3 << endl;
printf("%c -> %c\n", a.getTop().a1, a.getTop().a3);
a.Pop();
continue;
}
hanoi x1(a.getTop().n - 1, a.getTop().a1, a.getTop().a3, a.getTop().a2);
//第一个参数为1是为了利用循环中a.getTop()为1时会移动并打印一次移动过程
hanoi x2(1, a.getTop().a1, a.getTop().a2, a.getTop().a3);
hanoi x3(a.getTop().n - 1, a.getTop().a2, a.getTop().a1, a.getTop().a3);
a.Pop();
//本着栈后进先出的工作原理,所以调整三者的入栈顺序
a.Push(x3);
a.Push(x2);
a.Push(x1);
}
return 0;
}
同样的,以三个盘子的情况为例,具体的过程大概是这样的
3.方案三
这段代码的作者也不是我(T ^ T),是来自我的一位舍友,这种递归转非递归的可迁移性我觉得应该是三者之中最高的,但是可能也是最难理解的一种。此种方法模拟的是函数工作栈的过程,是去模拟类似codeblocks中call stack的进入和返回的过程。
对于递归代码,每当当前工作栈的n值为1时,需要进行打印。而因为原递归函数中,每当把总问题或者原子问题的n - 1个盘子从a借助c移动到b时,递归代码else中的这一步就逐步返回了,此时它第二次出现在栈顶。进行递归代码else中的第二行,也需要进行打印。这就是非递归代码中if部分的原理。
而为了标记它第几次出现在栈顶,我们加一个mark标记,而刚进入栈顶(mark由0变成1)时,我们应该做的是先处理原子问题,所以应该让n为n - 1的结点后入栈而出现在栈顶。这是非递归代码中else的if分支的原理。
若其又一次进入栈顶(mark由1变成2),意味着下一次一定会是第二次出现在栈顶,所以下一次是需要打印这个mark为2的结点的信息的(严格的说不能叫做结点,而是函数工作栈的一个元素块),所以要让当前栈顶结点也就是n为n(当前所在函数中给的形参对应的元素块)的结点入栈。这是非递归代码中else的else if分支的原理。
#include <iostream>
#include <stdio.h>
#include <stack>
using namespace std;
struct Node
{
int n;
char a;
char b;
char c;
int mark = 0;
Node(int N, char A, char B, char C)
{
n = N;
a = A;
b = B;
c = C;
}
};
stack<Node> Hanno;
int main()
{
int n;
//scanf("%d", &n);
cin >> n;
Node hano(n, 'a', 'b', 'c');
Hanno.push(hano);
while (!Hanno.empty())
{
hano = Hanno.top();
if (Hanno.top().n == 1 || Hanno.top().mark == 2)
{
printf("%c -> %c\n", Hanno.top().a, Hanno.top().c);
Hanno.pop();
}
else
{
if (Hanno.top().mark == 0)
{
hano.mark++;
Hanno.pop();
Hanno.push(hano);
Hanno.push(Node(hano.n - 1, hano.a, hano.c, hano.b));
}
else if (Hanno.top().mark == 1)
{
hano.mark++;
Hanno.pop();
Hanno.push(Node(hano.n - 1, hano.b, hano.a, hano.c));
Hanno.push(hano);
}
}
}
return 0;
}
这道题画出来的模拟图其实和第二种方案差不多,但会由于标记,比第二种过程少一些,具体的图就不画出来了,但是可以根据代码去自己画一下。可能是由于汉诺塔问题已经比较成熟了,出现了多种解题方案,第一种毕竟有些就题论题,还是应该多试着写一些后两种这样的通用的递归转非递归的代码。第一篇博客就到此结束啦Hi~ o( ̄▽ ̄)ブ。