既然是非递归解法,那么运用的函数中就不能出现之间或间接地对自身的引用。迭代就是利用一个完整的解决算法,对每一步都利用该步数作为参数带入算法得出具体结果。所以要迭代,就必须分析汉诺塔移动过程中每一步体现的规律。
每一步都可以分解为:
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).因此猜测每个盘间隔数和它的盘号有关:
盘号 | 间隔数 |
---|---|
1 | 2=21 |
2 | 4=22 |
3 | 8=23 |
4 | 24=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的位数代表着移动的盘号。
将这一部分的功能用一个独立的函数完成:
#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 来确定始终即可。
代码分成两个函数,一个用于收集用户提供的起点、终点和临时点模拟出轨迹,一个根据当前步骤数和盘号判断具体移动:
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种迭代方法满足你)。笔者初觉得只用迭代的方法实现汉诺塔问题很难,惊叹于此文章用到的思维方式。为学习如何具体地分析一个较复杂的问题,特记此思考过程。如有疑问,欢迎讨论。