汉诺塔问题的一个变种

汉诺塔问题的一个变种

最近碰到的一个有意思的算法题。

问题定义

考虑简化的汉诺塔问题:有三个柱子1、2、3,每个柱子上只要最下面的盘子是最大的,其他盘子可以以任意顺序摆放。用尽量少的步骤将n个按顺序摆放的盘子从柱子1移到柱子3(仍为顺序摆放)。

问题分析

为了将所有盘子放到3上,需要三步:

  1. 将前n-1个盘子以某种顺序放到2上;
  2. 将第n个盘子放到3上;
  3. 将2上的盘子放到3上。

其中步骤1与步骤3的差别在于:步骤1中盘子是第一次从柱子1上移出,而且移出之前是排好序的;步骤3中盘子经过步骤1的摆放可能是乱序的。

下面仔细分析步骤1。假设我们现在要将盘子i从1移动到2,这时柱子2、3上没有比i更大的盘子(因为只有前i-1个被移出了),那么柱子2必须是空的。因此有递归表示:

f(将前i个盘子移动到2) = f(将前i-1个盘子移动到3) + (将i移到2) + g(把前i-1个盘子从3移到2)

其中f在移动过程中必须考虑操作的合法性,g则不需要,所以g可以达到步数下限i-1(直接挨个移动)。这时前面的递推式可以写为

f(将前i个盘子移动到2) = f(将前i-1个盘子移动到3) + 1 + i-1 = f(将前i-1个盘子移动到3) + i

由此推出f(将前n个盘子移动到2)的步数下限为:

n * (n + 1) / 2

以n=7为例,根据这种操作方式将前6个盘子移动到2,其排列方式为

| 7
| 6 4 2 1 3 5
|

到现在为止,我们找到了完成步骤1的最优方法,但还不能确认通过这种方法能够达到全局最优。

现在仔细分析步骤3。经过前两个步骤之后,我们需要将2上的n-1个盘子按顺序放到3上。这时n-1一定被压在最下面,为了将n-1移出,我们需要将2上的n-2个盘子移动到1上。其递归表示为:

h(将n个盘子从2移动到3) = k(将n-1个盘子从2移动到1) + (将n移到3) + h(将n-1个盘子从1移动到3)

其中k需要移动n-1个盘子,其操作步数的下限为n-1。那么这个下限何时能达到呢?显然是当n-1个盘子中最大的n-1正好在最上面的时候(这样可以先把它放到最下面)。即只要柱2上的盘子排列成如下形式就能达到步数下限:

|n n-2 n-4 ... n-3 n-1

我们发现这正好是完成步骤1后形成的状态,我们恰好可以同时在步骤1和步骤3中达到最优解。

这时步骤3的递推表达式成为:

h(将n个盘子从2移动到3上) = k(将n-1个盘子从2移动到1上) + 1 + n-1 = n + h(将n-1个盘子从1移动到3上)

由此推出h(将n个盘子从2移动到3上)的步骤下限为n * (n + 1) / 2。

综合以上,我们整个过程中需要步骤数位:

f(将前n-1个盘子移动到2) + (将第n个盘子放到3上) + h(将n-1个盘子从2移动到3) = n(n-1) + 1

编程实现

有两种实现方式:递归和非递归,其中递归方式的思路与上文所述完全一致。不过如果搞清楚了整个过程,用循环的方式写起来更简单而且更快。

递归方式代码:

#include <cstdio>
void mix(int m, int a)
{
    int i;
    if(m>0)
    {
        if(m>1)
            mix(m-1, 3-a);
        //cout<<0<<" "<<a<<endl;
        printf("0 %d\n", a);
        for(i = m-1; i > 0; i--)
            //cout<<3-a<<" "<<a<<endl;
            printf("%d %d\n", 3-a, a);
    }
}
void dump(int m, int a)
{
    int i;
    if(m>0)
    {
        for(i = m; i > 1; i--)
            //cout<<a<<" "<<1-a<<endl;
            printf("%d %d\n", a, 1-a);
        //cout<<a<<" "<<2<<endl;
        printf("%d 2\n", a);
        if(m > 1)
            dump(m-1, 1-a);
    }
}
int main()
{
    int n;
    int a,b;
    //cin>>n;
    scanf("%d", &n);
    mix(n-1,1);
    //cout<<0<<" "<<2<<endl;
    printf("0 2\n");
    dump(n-1,1);
    return 0;
}

非递归方式代码:

#include <iostream>
using namespace std;

int main()
{
    int n, i, j, empty;
    cin>>n;

    empty = 1 + n % 2;

    for(i = 0; i < n - 1; i ++)
    {
        cout<<"0 "<<empty<<endl;
        for(j = 0; j < i; j ++)
        {
            cout<<3 - empty<<' '<<empty<<endl;
        }
        empty = 3 - empty;
    }

    cout<<"0 2"<<endl;

    empty = 0;
    for(i = 0; i < n - 1; i ++)
    {
        for(j = 0; j < n - 1 - i - 1; j ++)
        {
            cout<<1 - empty<<' '<<empty<<endl;
        }
        cout<<1 - empty<<" 2"<<endl;
        empty = 1 - empty;
    }

    return 0;
}

有趣的是:知道了搬运方式之后,我们并不需要维护整个汉诺塔的状态。

    原文作者: 汉诺塔问题
    原文地址: https://blog.csdn.net/huangbo10/article/details/41250237
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞