其他分享
首页 > 其他分享> > 多柱汉诺塔问题

多柱汉诺塔问题

作者:互联网

多柱汉诺塔问题

题意分析

题目链接 : 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])\)​ ,可能现在的规律不容易发现,但是我们可以考虑观察其差分数组,如下图

image

可以发现该数列为分组规律,可以分成若干组,从第一组开始,第 \(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