多柱汉诺塔问题
作者:互联网
多柱汉诺塔问题
题意分析
题目链接 : 2021 江西省赛 F 题
题意:大家都知道传统意义上的汉诺塔问题:
相传在古印度圣庙中,有一种被称为汉诺塔(Hanoi)的游戏。该游戏是在一块铜板装置上,有三根杆(编号A、B、C),在A杆自下而上、由大到小按顺序放置64个金盘(如图1)。游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。
现在将柱子改成四根,其他规则不变,求初始柱上有 \(n\) 个盘子时,要将其全部移到目标柱所用的最小次数。
要想解决这个问题,我们要先回顾一下传统的汉诺塔解决方案。
假设 \(a\) 柱上有 \(n\) 个盘子,记 \(n\) 个盘子移动所需的次数为 \(f(n)\) 。考虑递归求解问题,首先可以借助 \(c\) 柱从 \(a\) 柱转移 \(n - 1\) 个盘子到 \(b\) 柱上,再将剩余的一个最大的盘子放到 \(c\) 柱上,然后再借助 \(a\) 柱将 \(b\) 柱上的 \(n - 1\) 个盘子转移到 \(c\) 柱上。而将 \(n - 1\) 个盘子从一个柱上借助另一个柱转移到目标柱上显然与 \(n\) 个柱子时的情况类似,所以我们可以从小到大逐次求解其转移次数。
具体解法可以通过递归方式理解,代码如下
void move(char a, char b) { printf("%c -> %c \n", a, c); }
void hanoi(char a, char b, char c, int n)
{
if (n == 1) move(a, c); //递归边界
else
{
hanoi(a, c, b, n - 1);
move(a, b);
hanoi(b, a, c, n - 1);
}
}
考虑边界条件:当 \(n = 1\) 时, \(f(1) = 1\) 。
而由上面的分析可知,上面的 \(n - 1\) 个盘子都要移动 \(2\) 次,而最下面的只移动一次,递推式为 \(f(x)= \begin{cases} 1,& \text{n = 1} \\ 2 f(n - 1) + 1, & \text{n > 1} \end{cases}\)
也进而能求出其通项公式 \(f(n) = 2^n - 1\)
过程分析
现在将问题变为四根柱子,为方便起见,令四根柱子分别为 \(a,b,c,d\) 。这就意味着我们可以通过两个中间柱子转移盘子。令 \(a\) 柱(初始柱)上最开始有 \(n\) 个盘子,令 \(g(n)\) 表示从 \(a\) 柱转移到 \(d\) 柱(目标柱)的最小次数。
同样地,我们试图通过递归的方式去思考这个问题。由于中间柱的数量增加,可以移动的路径方案也增多了。通过传统的汉诺塔问题可知,递归解决方案中必然包含两部分,一部分是递归求解上面的盘子移动次数,另一部分是移动下面剩余的几个盘子所需要的次数。
如果继续采用前面的做法必然有一个空闲的柱子,所以问题的关键就是如何利用这增加的一个中间柱。
此时我们可以将剩余的盘子从一个改成两个,在借助一个柱子将上面的 \(n - 2\) 个盘子转移到一个柱子(记为 \(b\) 柱)后,因为还有两个柱子空闲,显然可以通过三次移动将剩余的两个盘子移动到目标柱(记为 \(d\) 柱)上,再将 \(b\) 柱上的 \(n - 2\) 个盘子转移到目标柱上。在推导递推式时,要注意到第一次转移 \(n - 2\) 个盘子时,只借助了一个中间柱,而第二次移动可以借助两个中间柱,因此其递推式为
\[g(n) = f(n - 2) + g(n - 2) + 3 \,\,(*) \]在前一个方案中,我们仅仅用多出来的柱子转移了一个盘子,那么如果转移多个呢?
如果没有中间柱的话,只能转移一个盘子,我们也完全可以像三柱汉诺塔那样,用中间的两个柱子转移 \(n - 1\) 个盘子,再去转移最后一个盘子,显然中间的 \(n - 1\) 个盘子都要移动两次,而下面的一个就移动一次,可以将这 \(n - 1\) 个盘子分成两部分,令 \(n - 1 = x + y\) ,前 \(x\) 个先移动到 \(b\) 柱,此时除 \(a,b\) 柱外有两个空闲的可以作为中间柱,转移需要次数为 \(g(x)\) ,再转移后 \(y\) 个时,除 \(a,c\) 柱只剩下一个空闲柱,转移次数为 \(f(y)\) ,最后剩下的一个盘子直接移动到 \(d\) 柱,然后分别将 \(c\) 柱上的 \(y\) 个盘子和 \(b\) 柱上的 \(x\) 个盘子分别按照只经过一个中间柱和两个中间柱转移到 \(d\) 柱,,易知递推方程为
\[g(n) = 2 (g(x) + f(y) ) + 1, \text{(x +y = n - 1)} (**) \]很明显,递推方程 \((*)\) 可作为 \((**)\) 的一种特殊情况。
但是,由于 \(x, y\) 的取值还无法确定,无法确定 \(g(n)\) 的值。这里我们可以通过编程对比不同的取值取最小数,在进行小规模数据的打表时可以发现规律。代码如下,
#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll f[50], g[50];
int main()
{
f[1] = 1;
for (int i = 2; i <= 20; i++)
f[i] = 2 * f[i - 1] + 1;
g[1] = 1; g[2] = 3;
for (int i = 3; i <= 20; i++)
{
ll ans = 1e18;
for (int j = 1; j < i - 1; j++)
{
int k = i - 1 - j;
ll temp = g[j] + f[k];
if (temp < ans) ans = temp;
}
g[i] = 2 * ans + 1;
cout << i << " " << ans << endl; //ans 为 g[x] + g[y] 的最小值
}
return 0;
}
运行结果如下
第一行为 \(n\) ,第二行为计算出来的 \(min(g[x] + f[y])\) ,可能现在的规律不容易发现,但是我们可以考虑观察其差分数组,如下图
可以发现该数列为分组规律,可以分成若干组,从第一组开始,第 \(i\) 组有 \(i\) 个 \(2^{i - 1}\) 。这样就可以通过 \(\mathcal{O}(N)\) 的时间复杂度维护出其差分数组,再通过前缀和得到 \(min(g[x] + f[y])\) ,则有 \(g(n) = 2 * min(g[x] + f[y]) +1\) 。
\(AC\) 代码
这么大的数还不能取模,肯定要用 \(py\) 啊
a = [0] * 10010
a[2] = 1
base = 1
cnt = 1
sum = 1
for i in range(3, 10010):
if cnt <= sum:
a[i] = base
cnt = cnt + 1
else:
base = 2 * base
a[i] = base
sum = sum + 1
cnt = 1
for i in range(1, 10010):
a[i] = a[i] + a[i - 1]
T = int(input())
for i in range(T):
n = int(input())
print(2 * a[n] + 1)
标签:柱子,移动,转移,问题,汉诺塔,盘子,柱上,多柱 来源: https://www.cnblogs.com/ChekunChuang/p/15486906.html