Description
汉诺塔问题中限制不能将一层塔直接从最左侧移动到最右侧,也不能直接从最右侧移动到最左侧,而是必须经过中间。求当有N层塔的时候移动步数。
Input
输入第一行为用例个数, 每个测试用例输入的第一行为N。
Output
移动步数。
Sample Input 1
1
2
Sample Output 1
8
思路
思路一:递归
倒着看这个问题,把前n-1个盘子看作一个整体,则问题变为如何将处于A柱的压在n-1个盘子下的第n个盘子移动到C柱。
step1. 将前n-1个盘子从A柱移动到B柱
step2. 将前n-1个盘子从B柱移动到C柱
step3. 将第n个盘子从A柱移动到B柱
step4. 将前n-1个盘子从C柱移动到B柱
step5. 将前n-1个盘子从B柱移动到A柱
step6. 将第n个盘子从B柱移动到C柱
step7. 将前n-1个盘子从A柱移动到B柱
step8. 将前n-1个盘子从B柱移动到C柱
可以看到,n-1个盘子只要移动就要连续移动两次,期间第n个盘子移动两次,从而得出递归公式:
递归边界条件是n==1时,只需将盘子从A移动到B,再从B移动到C,共2次
代码如下:
#include <iostream>
using namespace std;
int hanoi(const int &n) {
if (n == 1) return 2;
else return 3 * hanoi(n - 1) + 2;
}
int main()
{
int t;
//scanf("%d", &t);
cin >> t;
for (int turn = 0; turn < t; turn++) {
int n;
//scanf("%d", &n);
cin >> n;
int step = 0;
step = hanoi(n);
if (turn + 1 == t) {
printf("%d", step);
}
else {
printf("%d\n", step);
}
}
return 0;
}
思路二:非递归——栈
修改后的的汉若塔问题不能在从“左”直接移到“右”,也不能直接“右”直接移到“左”,而是要经过中间的过程。也就是说,实际只有4个动作 “左”到“中”、“中”到“右”、“右”到“中”、“中”到“左”
现在我们把左、中、右三个地点抽象成栈,依次记为LS、MS和RS。最初所有的塔都在LS上。那么四个动作就可以看作是:某一个栈(src)把栈顶元素弹出,然后压入另一个栈里(dst),作为这一个栈(dst)是栈顶。
例如,如果是7层塔,在最初时所有的塔都在LS上,LS从栈顶到栈底就依次1~7,如果现在发生了“左”到“中”的动作,这个动作对应的操作是LS栈将栈顶元素1弹出,然后1压入到MS栈中,成为MS的栈顶。其他操作同理。
一个动作能发生的先决条件是不违反小压大的原则。
src栈弹出的元素src.top()如果想压入到dst栈中,那么num的值必须小于当前to栈顶。
还有一个原则不是很明显,但也非常重要,叫相邻不可逆原则,解释如下:
1. 我们把4个动作依次定义为:L -> M,M -> L、M -> R和R -> M。
2. 很明显,L -> M和M -> L过程互为逆过程,M -> R和R -> M互为逆过程,
3. 在修改后的汉若塔游戏中,如果想走出最少步数,那么如何两个相邻的动作都不是互为逆的过程的。举例:如果上一步的动作是L -> M,那么这一步绝不是M -> L,直观地解释为:你在上一步把一个栈顶数从“左”移动到“右”,这一步为什么又要移回去呢?这必然不是取得最小步数的作法。同理,M -> R动作和R -> M动作也不可能相邻发生。
有了小压大和相邻不可逆序原则后,可以推导出两个十分有用的结论——非递归的方法核心结论:
1.游戏的第一个动作一定是L -> M,这显而易见的。
2.在走出最少步数过程中的任何时刻,4个动作只有一个动作不违反小压大和相邻不可逆原则,另外三个动作一定都会违反。
对于结论2,现在进行简单的证明
因为游戏的第一个动作已经确定是L -> M,则以后的每一步都会有前一步动作。
假设前一步的动作是L -> M:
1. 根据小压大原则,L -> M的动作不会重复发生
2. 根据相邻不可逆原则,M -> L的动作也不该发生
3. 根据小压大原则,M -> R和R -> M只有一个达标
假设前一步的动作是M -> L:
1. 根据小压大原则,M -> L的动作不会重复发生
2. 根据相邻不可逆原则,L -> M的动作也不该发生
3. 根据小压大原则,M -> R和R -> M只有一个达标
假设前一步的动作是M -> R:
1. 根据小压大原则,M -> R的动作不会重复发生
2. 根据相邻不可逆原则,R -> M的动作也不该发生
3. 根据小压大原则,M -> L和L -> M只有一个达标
假设前一步的动作是R -> M:
1. 根据小压大原则,R -> M的动作不会重复发生
2. 根据相邻不可逆原则,M -> R的动作也不该发生
3. 根据小压大原则,M -> L和L -> M只有一个达标
综上所述,每一步只会有一个动作达标,那么只要每一步都根据这两个原则考查所有的动作就可以,那个动作达标就走哪一个动作,反正每一次都只有一个动作满足要求,按顺序走下来即可。
代码如下:
#include <iostream>
#include <stack>
using namespace std;
enum Action
{
No = 0,
LtoM = 1,
MtoL = -1,
RtoM = 2,
MtoR = -2
};
/*
*函数介绍:移动汉诺塔上的圆盘。
*输入参数:record为上一个移动的步骤;now为即将移动的步骤;src为源栈;dst为目的栈。
*输出参数:pre记录即将发生的移动步骤;src移动圆盘后的栈;dst移动圆盘后的栈。
*返回值:若圆盘移动成功,则返回移动歩数1;若失败,则返回移动歩数0。
*/
int moveDisc(Action& record, Action now, stack<int>& src, stack<int>& dst)
{
/*
*一个动作能发生的先决条件是:
*1.不违反小压大的原则
* src栈弹出的元素src.top()如果想压入到dst栈中,那么num的值必须小于当前to栈顶
*2.相邻不可逆原则
* a. 我们把4个动作依次定义为:L -> M,M -> L、M -> R和R -> M。
* b. 很明显,L -> M和M -> L过程互为逆过程,M -> R和R -> M互为逆过程,
* c. 在修改后的汉若塔游戏中,如果想走出最少步数,那么如何两个相邻的动作都不是互为逆的过程的。
*
*综上总结出非递归的方法核心结论:
* 1.游戏的第一个动作一定是L -> M,这显而易见的。
* 2.在走出最少步数过程中的任何时刻,4个动作只有一个动作不违反小压大和相邻不可逆原则,另外三个动作一定都会违反。
*
*
*/
if (!src.empty() && (abs(record) != abs(now)) && (dst.empty() || src.top() < dst.top()))
{
dst.push(src.top());
src.pop();
/*switch (now)
{
case 1:
cout << "Move " << dst.top() << " from left to mid;" << endl;
break;
case -1:
cout << "Move " << dst.top() << " from mid to left;" << endl;
break;
case 2:
cout << "Move " << dst.top() << " from right to mid;" << endl;
break;
case -2:
cout << "Move " << dst.top() << " from mid to right;" << endl;
break;
default:
break;
}*/
record = now;
return 1;
}
return 0;
}
/*
*函数介绍:解决限制移动方式的汉诺塔问题的C++实现
*输入参数:layer为汉诺塔的层数
*输出参数:无
*返回值:为移动的歩数
*/
int hanoiStack(int layer)
{
//定义3个栈分别表示左中右三个柱子
stack<int> lS, mS, rS;
//先将所有盘子以下层比上层大的顺序压入左栈
for (int i = layer; i > 0; i--)
{
lS.push(i);
}
//初始情况,前一次Action为No
Action record = No;
int step = 0;
/*
*修改后的的汉若塔问题不能在从“左”直接移到“右”,也不能直接“右”直接移到“右”,而是要经过中间的过程。
*也就是说,实际只有4个动作 “左”到“中”、“中”到“右”、“右”到“中”、“中”到“左”
*现在我们把左、中、右三个地点抽象成栈,依次记为LS、MS和RS。最初所有的塔都在LS上。
*那么四个动作就可以看作是:
*某一个栈(src)把栈顶元素弹出,然后压入另一个栈里(dst),作为这一个栈(dst)是栈顶。
*/
while (rS.size() != layer)
{
step += moveDisc(record, LtoM, lS, mS);
step += moveDisc(record, MtoL, mS, lS);
step += moveDisc(record, RtoM, rS, mS);
step += moveDisc(record, MtoR, mS, rS);
}
return step;
}
int main()
{
int t;
//scanf("%d", &t);
cin >> t;
for (int turn = 0; turn < t; turn++) {
int n;
//scanf("%d", &n);
cin >> n;
int step = 0;
step = hanoiStack(n);
if (turn + 1 == t) {
printf("%d", step);
}
else {
printf("%d\n", step);
}
}
return 0;
}
参考文章:
最后再来回顾下经典汉诺塔问题
汉诺塔问题(又称为河内塔问题),是一个大家熟知的问题。在left,mid,right三根柱子上,有n个不同大小的圆盘(假设半径分别为1到n吧),一开始他们都叠在我A上(如图所示),你的目标是在最少的合法移动步数内将所有盘子从left塔移动到right塔。
游戏中的每一步规则如下:
1. 每一步只允许移动一个盘子(从一根柱子最上方到另一个柱子的最上方)
2. 移动的过程中,你必须保证大的盘子不能在小的盘子上方(小的可以放在大的上面,最大盘子下面不能有任何其他大小的盘子)
思路
递归
如果只有1个盘子,则直接将其从left移动到right上;
否则,就执行以下步骤:
step1.前n-1个从left借助right移动到mid上
step2.第n个从left直接移动到right上
step3.前n-1个从mid借助left移动到right上
每轮前n-1个移动2步,第n个移动1步
代码
#include <iostream>
#include <string>
using namespace std;
int hanoiCount(int n) {
int res = 0;
if (n == 1) {
res = 1;
}
else {
/*
step1.前n-1个从left借助right移动到mid上
step2.第n个从left直接移动到right上
step3.前n-1个从mid借助left移动到right上
每轮前n-1个移动2步,第n个移动1步
*/
res = 2 * hanoiCount(n - 1) + 1;
}
return res;
}
void hanoi(int n, string from, string mid, string to) {
if (n == 1) {
cout << "move 1 from " << from << " to " << to << endl;
}
else {
hanoi(n - 1, from, to, mid);
cout << "move " << n << " from " << from << " to " << to << endl;
hanoi(n - 1, mid, from, to);
}
}
int main()
{
int t;
cin >> t;
for (int turn = 0; turn < t; turn++) {
int n;
cin >> n;
int res = hanoiCount(n);
hanoi(n, "left", "mid", "right");
cout << "Move " << res << " times." << endl;
}
return 0;
}
运行结果