汉诺塔的五种解法
Misophiliac · · 算法·理论
0x00 前言
汉诺塔是一个很古老的问题了,本文将介绍求解该问题的五种实现方式。
汉诺塔有五种解法,你知道么? ——鲁迅
0x01 问题介绍
汉诺塔问题简单来说就是给三根柱子,一开始其中一根上叠放着多个圆盘,圆盘由上到下直径依次增大,另外两根柱子为空。按一定的规则在柱子间移动圆盘,求将所有圆盘移动到另一根柱子上的最小步数。
移动规则:
- 一次只能移动一个圆盘;
- 大的圆盘不能放在小的圆盘上面。
0x02 解法
如果记圆盘数量为
(自己想一想就明白了)
但是如果
其实从上面的列表就可以猜出
事实上这是对的,证明如下:
首先求递推式:
标记三个柱子分别为
- 用
f_{n-1} 步将A 最上面的n-1 个圆盘挪到B 上 - 用
1 步将A 上剩的最大的圆盘挪到C 上 - 用
f_{n-1} 步将B 上的n-1 个圆盘挪到C 上
由于挪较小的
总步数
利用高中的数列知识,可以求出
然后用高精度你就能做 P1760 了。
0x03 编程解决
以下默认是模意义下的解决办法,且 #define int unsigned long long,const int mod = 1e9+7。
代码均在 Xcode 内,C++14(GCC9) 下编写并运行。
如果仅使用递推式,我们有两种解法。
1. 递归
前置知识:C++ 语言基础。
int hanoi1(int n) {
if (n == 1) return 1;
return (2 * hanoi1(n - 1) + 1) % mod;
}
时空均
2. 递推
前置知识:C++ 语言基础。
int hanoi2(int n) {
int x = 1;
while (n--) x = (x * 2 + 1) % mod;
return x;
}
时间
但是如果
3.快速幂
前置知识:幂的性质。
以下内容是为没学过快速幂的萌新准备的,大佬请跳过。
快速幂就是用来在
例如,给出
最简单的就是循环暴力乘,但是这样是
但是如果我们把
即:
再利用幂的性质:
就可以得出
乍一看好像变复杂了,但是我们发现,a=a*a 的循环得出,而最大可能的
又注意到一个
所以可以写出如下代码:
int quickpow(int a, int n) {//a^n%mod
int ans = 1;
while (n) {
if (n & 1) ans = ans * a % mod;
a = a * a % mod;
n >>= 1;
}
return ans;
}
用来过模板题。
而对于汉诺塔,底数
int hanoi3(int n) {
int ans = 1, a = 2;
for (int i = 1; i <= n; i <<= 1) {
if (n & 1) ans = ans * a % mod;
a = a * a % mod;
}
return ans - 1;
}
时间
4. ???
前置知识:位运算。
我们要求的是
回想一下,位运算中 1<<n 表示的就是
但是
所以将 1<<L 爆炸的数,而
代码:
int hanoi4(int n) {
const int L = 63;
int a = n / L, b = n - a * L;
int x = 1, y = 1, z = 1;
x = (x << L) % mod;
y = (y << b) % mod;
while (a) {
if (a & 1) z = z * x % mod;
x = x * x % mod;
a >>= 1;
}
return (y * z - 1) % mod;
}
时间
当然可以对该方法进行各种奇奇怪怪的修改,不幸的是它们的常数都超过了原始算法。
以上都是对单次询问的解法。如果有多次询问,则又有更优的做法。
5.光速幂
前置知识:与约数有关的知识、模算术。
其实方法
对于一般的底数:
其实很简单,就是
根据定义,有
对于之前的问题
最小值发生
预处理代码:
int f[32000], g[32000];//分别存a^1~L,a^L~p/L*L
int a = 2;//底数
int L = sqrt(p);//sqrt函数需要#include <cmath>
f[0] = g[0] = 1;
for (int i = 1; i <= L; i++)
f[i] = f[i-1] * a % p;
for (int i = 1; i <= L; i++)
g[i] = g[i-1] * f[L] % p;
然后求模数的欧拉函数,本来应该讲讲的,但是我懒模数
最后,f[n%L] * g[n/L] % mod。
所以
int f[32000], g[32000];
int L;
void init() {
L = sqrt(mod);
f[0] = g[0] = 1;
for (int i = 1; i <= L; i++)
f[i] = f[i-1] * 2 % mod;
for (int i = 1; i <= L; i++)
g[i] = g[i-1] * f[L] % mod;
}
int hanoi5(int n) {
n %= mod - 1;
return (f[n%L] * g[n/L] - 1) % mod;
}
用的时候 init() 一遍,然后就可以疯狂
空间复杂度
模板题
0x04 各解法运行时间比较
使用代码:
#include <cstdio>
#include <ctime>
/*...hanoi1~5...*/
void test(int n, int t) {
clock_t t0, t1, t2, t3, t4, t5;
t0 = clock();
for (int i = 0; i < t; i++) hanoi1(n);
t1 = clock();
for (int i = 0; i < t; i++) hanoi2(n);
t2 = clock();
for (int i = 0; i < t; i++) hanoi3(n);
t3 = clock();
for (int i = 0; i < t; i++) hanoi4(n);
t4 = clock();
init();
for (int i = 0; i < t; i++) hanoi5(n);
t5 = clock();
printf("1: %lu\n", t1 - t0);
printf("2: %lu\n", t2 - t1);
printf("3: %lu\n", t3 - t2);
printf("4: %lu\n", t4 - t3);
printf("5: %lu\n", t5 - t4);
}
多次结果取平均后整理如下表(注意
0x05 结语
是不是感觉费这么大的劲,就做了道红题?
但是,探索解决同一问题的不同方法总是好的。这可以帮助我们巩固已学知识,并在知识之间建立联系,加深对知识的理解,从而引发思考。
总之,受益匪浅。
希望各位在今后的学习中,也要勤于思考,敢于创新。
0x06 参考文献
-
关于汉诺塔问题 - 知乎
-
P1226 的各题解
-
「学习笔记」光速幂
-
数论 - 欧拉定理 - 知乎
-
欧拉函数φ(n)的计算及欧拉定理 - 知乎