C++ 学习笔记: 汉诺塔问题的迭代解法

2021/4/13 12:27:40

本文主要是介绍C++ 学习笔记: 汉诺塔问题的迭代解法,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录

  • 启发
  • 思路&部分代码
    • 分解过程
    • 移动盘号
      • 间隔数
      • 第一次移动某盘对应的步骤数
      • 确定某一步要移动的盘号
      • 代码1
    • 移动的起始和终止
      • 代码2
  • 总过程及代码
  • 后记

启发

既然是非递归解法,那么运用的函数中就不能出现之间或间接地对自身的引用。迭代就是利用一个完整的解决算法,对每一步都利用该步数作为参数带入算法得出具体结果。所以要迭代,就必须分析汉诺塔移动过程中每一步体现的规律。


思路&部分代码

分解过程

每一步都可以分解为:
1.决定移动的盘号(假如对n个盘子编号,从小到大为1~n)
2.决定将盘子从哪移动到哪。

由此可知,要解决的问题就是:
1.如何确定每一步对应那个盘子;
2.如何确定盘子当前的位置和要移动的位置。

移动盘号

假设移动4个盘子,默认将盘子从1号移动到3号柱,2号柱作为临时存放点。过程如下(括号内代表移动的盘号):

1–2(1)
1–3(2)
2–3(1)
1–2(3)
3–1(1)
3–2(2)
1–2(1)
1–3(4)
2–3(1)
2–1(2)
3–1(1)
2–3(3)
1–2(1)
1–3(2)
2–3(1)

观察上面式子,可以知道:
1.总步骤数为15(也就是24 -1)
2.移动1号盘的步骤为:[1,3,5,7,9,11,13,15] ,移动2号盘的步骤为[2,6,10,14]; 移动3号盘的步骤为[4,12]; 移动4号盘的步骤为[8]
3.第一次移动1号盘在步骤1;第一次移动2号盘在步骤2;第一次移动3号盘在步骤4,第一次移动4号盘在步骤8。

间隔数

由此可看到, 1号盘的步骤数间隔为2(3-1,5-3,7-5), 2号的间隔数为4(6-2,10-6,14-10), 3号的间隔数为8(12-4).因此猜测每个盘间隔数和它的盘号有关:

盘号间隔数
12=21
24=22
38=23
424=16

以此类推。

证明:
递归地分析移动n个盘子的过程可得:
n=1时,移动一次;
n=2时,移动1+1+1=3次(首尾两个1分别代表移动1号的步骤);
n=3时,先移动2个盘到临时点上,再移动3号,最后把1,2号移动到终点,因此:3+1+3 = 7次.
为n时,设 a n a_{n} an​为总步数,则 a n a_{n} an​=2 a n − 1 + 1 a_{n-1} + 1 an−1​+1
得出 a n = 2 n − 1 a_{n} = 2^{n} -1 an​=2n−1
图解如下:
移动过程
因此可以利用数学归纳法证明步骤间隔数和盘号的关系.
设移动总数为n的盘子们
其中1号:
因为移动了1号盘以后,下一步不可能再动1号,因此排除"存在任意连续的两步都移动1号"的假设.当移动了另外号码(假设a号)的盘子后(此时除1号呆的柱子,其他柱子上的盘子都比1号大),第三步一定会移动1号,因为移动之前a号下要么比a号大,要么没有盘子,又不可能再把a号移回去(无效操作),因此只能挪动1号.得出:一号的步骤间隔为2.
设k号盘的步骤间隔=2k 成立
k+1号:
总过程分解到(k+1)的层次,共有2(n-k-1) 个分叉.每个分叉都代表移动k个盘子的(2k-1 -1)步,因此间隔数:
a k + 1 = 2 k − 1 + 1 + 2 k − 1 + 1 = 2 k + 1 a_{k+1} = 2^{k}-1+1+2^{k}-1+1 = 2^{k+1} ak+1​=2k−1+1+2k−1+1=2k+1
中间的+1代表移动k+2号盘的那一步,最后的+1代表移动第二次移动k+1号盘的那一步.

第一次移动某盘对应的步骤数

依照上图,证明这个也就很简单了。第一次移动第n个盘子的步骤编号为2n-1 。递归下去,第一次移动第n+1个盘子的步骤编号为2n-2 。因此,第一次移动第k个盘子的步骤编号为:
2k-1.


确定某一步要移动的盘号

现在我们可以解决第一个大问题了,就是如何确定每一步要移动的盘号。根据以上两个性质可得,在第(m)步时,定有某个盘(k)移动第(i)次,即:
m = 2 k − 1 + ( i − 1 ) ∗ 2 k m = 2^{k-1}+(i-1)*2^{k} m=2k−1+(i−1)∗2k
其中“2”的出现频率之高让人不禁想到二进制(?)或许可以利用二进制的某些特征来通过(m)反向确定(k)。
当 i ≠ 0 i \neq 0 i​=0 时, ( i − 1 ) ∗ 2 k (i-1)*2^{k} (i−1)∗2k 一定大于 2 k − 1 2^{k-1} 2k−1。 也就是说式子的前半部分在二进制数中似乎一直作为最低位的部分。(i)的大小不影响这一事实。
于是有:在第(m)步时,(m)所对应的二进制表示中最低为1的位数代表着移动的盘号。

代码1

将这一部分的功能用一个独立的函数完成:

#include <iostream>
#include <sstream>  //用于将数字转换为字符串
#include <bitset>  //用于将数字转换为二进制形式
#include <string>
#include <cmath>


int lowestBit(int num)
{
    //初始化stringstream对象,
    //用于存储步骤数num的二进制形式的bitForm
    //和其字符串形式的bitString
    stringstream ss;
    bitset<32> bitForm = bitset<32>(num);  //得到数字的二进制形式
    string bitString;
    
    //初始化盘号.
    //考虑到汉诺塔可能很高,所以用范围较大的 unsigned int
    unsigned int position = 0;  
    
    //转换为字符串
    ss << bitForm;
    ss >> bitString;
    
	//for循环从右往左判定第一个1出现的位置
	for (unsigned int i = bitString.size(); i >= 0;i--)
    {
        if (bitString[i] == '1')
        {
        	//注意,bitString的最右边有一个隐式的“\0”
        	//因此若第一位为"1",此时 i = 31
            position = bitString.size() - i;
            break;
        }
    }
    return position;
}

移动的起始和终止

反观移动4个盘子的过程,还可以发现:

1–2(1)
1–3(2)
2–3(1)
1–2(3)
3–1(1)
3–2(2)
1–2(1)
1–3(4)
2–3(1)
2–1(2)
3–1(1)
2–3(3)
1–2(1)
1–3(2)
2–3(1)

(1)号盘的路线是:1–2--3–1--2–3--1–2--3
(2)号盘的路线是:1–3--2–1--3
(3)号盘的路线是:1–2--3
(4)号盘的路线是:1–3
这之中存在着一种“不走回头路”的规律:(1)号不会从1–2之后,再从2–1,23、31的组合同理,剩下(2、3、4)号盘的行动路线也同理。因为要避免无效回合,当一个盘子最初的移动定下来时,它的轨迹就已经决定了。
因此,产生了两种移动路线:
顺序:1–2--3–1
逆序:1–3--2–1

又:最后一个盘一定只有 1–3 这一步(默认初始都在1号,终点为3号)。由上递归分析图可得,(3)的移动轨迹和(4)相反,(2)和(3)相反,(1)和(2)相反,以此类推。

总结出:
当移动总数为n的盘子时,第k个盘子的行动轨迹:
1.与n相同【n-k为偶数】
2.与n相反【n-k为奇数】

那么我们只要根据盘号确定每一个盘子的轨迹,并根据其移动次数 i i i 来确定始终即可。

代码2

代码分成两个函数,一个用于收集用户提供的起点、终点和临时点模拟出轨迹,一个根据当前步骤数和盘号判断具体移动:

string movement(unsigned int plateNum,unsigned int stepNum,unsigned int position,int ini,int fin,int tem)
{
	//根据盘号和步骤数,利用上面提供的 [步骤,盘号,次数] 三者的关系式逆推次数 i
	//由 i 判断具体移动
	//plateNum -- 盘子总数
	//stepNum -- 步骤数
	//position -- 盘号
	//ini -- 起点
	//fin -- 终点
	//tem -- 临时点

	//初始化移动次数moveTimes 和 字符串move
    unsigned int moveTimes = 0;
    string move;
    
    //计算moveTimes
    moveTimes = stepNum - static_cast<unsigned int>(pow(2, static_cast<double>(position)-1));
    moveTimes /= static_cast<int>(pow(2, static_cast<double>(position)));
    moveTimes += 1;
    
	//用 moveTimes 判断具体行动
	if((plateNum - position)%2 == 0)
    {
        if ((moveTimes % 3) == 1)
            move = movementGenerator(ini,fin);
        else if ((moveTimes % 3) == 2)
            move = movementGenerator(fin,tem);
        else
            move = movementGenerator(tem,ini);
    }
    else
    {
        if ((moveTimes % 3) == 1)
            move = movementGenerator(ini,tem);
        else if ((moveTimes % 3) == 2)
            move = movementGenerator(tem,fin);
        else
            move = movementGenerator(fin,ini);
    }
    return move;
}


string movementGenerator(int start,int end)
{
	//返回对应的行动轨迹字符串
	//如:起点=1,终点=2
	//则返回字符串"1--2"
    stringstream ss;
    string moveString;
    ss << start;
    ss<<"--";
    ss << end;
    ss >> moveString;
    return moveString;
}

总过程及代码

利用一个总函数hanoi集合运行上面的几个函数,解决汉诺塔问题。 总代码如下:

//tower of hanoi
//iteration type
#include <iostream>
#include <bitset>
#include <sstream>
#include <string>
#include <cmath>
using namespace std;

void hanoi(unsigned int, int, int, int);
int lowestBit(int);
string movement(unsigned int, unsigned int, unsigned int,int,int,int);
string movementGenerator(int, int);

int main()
{
    unsigned int plateNum = 0;
    int ini = 0;
    int fin = 0;
    int tem = 0;
    cout << "Enter the plate number: ";
    cin >> plateNum;
    cout << "Enter the ini, fin, and tem: ";
    cin >> ini >> fin >> tem;
    hanoi(plateNum, ini, fin, tem);
    
    

}

//return the position of the first "1" from right to left, AKA the plate number
int lowestBit(int num)
{
    stringstream ss;
    bitset<32> bitForm = bitset<32>(num);
    string bitString;
    unsigned int position = 0;
    ss << bitForm;
    ss >> bitString;
    for (unsigned int i = bitString.size(); i >= 0;i--)
    {
        if (bitString[i] == '1')
        {
            position = bitString.size() - i;
            break;
        }
    }
    return position;
}

//return a string including the movement, such as "A--B"
string movement(unsigned int plateNum,unsigned int stepNum,unsigned int position,int ini,int fin,int tem)
{
    unsigned int moveTimes = 0;
    string move;
    moveTimes = stepNum - static_cast<unsigned int>(pow(2, static_cast<double>(position)-1));
    moveTimes /= static_cast<int>(pow(2, static_cast<double>(position)));
    moveTimes += 1;
    if((plateNum - position)%2 == 0)
    {
        if ((moveTimes % 3) == 1)
            move = movementGenerator(ini,fin);
        else if ((moveTimes % 3) == 2)
            move = movementGenerator(fin,tem);
        else
            move = movementGenerator(tem,ini);
    }
    else
    {
        if ((moveTimes % 3) == 1)
            move = movementGenerator(ini,tem);
        else if ((moveTimes % 3) == 2)
            move = movementGenerator(tem,fin);
        else
            move = movementGenerator(fin,ini);
    }
    return move;
}

string movementGenerator(int start,int end)
{
    stringstream ss;
    string moveString;
    ss << start;
    ss<<"--";
    ss << end;
    ss >> moveString;
    return moveString;
}

//总函数hanoi
void hanoi(unsigned int plateNum, int ini, int fin, int tem)
{
    unsigned int position;
    string move;
    for (unsigned int i = 1; i <= pow(2,static_cast<double>(plateNum))-1;i++)
    {
        position = lowestBit(i);
        move = movement(plateNum, i, position,ini,fin,tem);
        cout << move << endl;
    }
}

后记

该解法参考c++迭代递归实现汉诺塔(5种迭代方法满足你)。笔者初觉得只用迭代的方法实现汉诺塔问题很难,惊叹于此文章用到的思维方式。为学习如何具体地分析一个较复杂的问题,特记此思考过程。如有疑问,欢迎讨论。



这篇关于C++ 学习笔记: 汉诺塔问题的迭代解法的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程