一尘不染

河内塔与K钉

algorithm

河内塔问题是递归的一个经典问题。给您3个钉,其中一个上有磁盘,并且必须按照给定的规则将所有磁盘从一个钉移动到另一个钉。您还必须以最少的移动次数执行此操作。

这是解决问题的递归算法:

void Hanoi3(int nDisks, char source, char intermed, char dest)
{
    if( nDisks > 0 )
    {
        Hanoi3(nDisks - 1, source, dest, intermed);
        cout << source << " --> " << dest << endl;
        Hanoi3(nDisks - 1, intermed, source, dest);
    }
}


int main()
{
    Hanoi3(3, 'A', 'B', 'C');

    return 0;
}

现在,想象同样的问题,只有4个钉子,所以我们添加了另一个中间钉子。当面对必须在任一点上选择哪个中间钉的问题时,我们将选择最左边的一个,以防多于一个空闲钉。

对于此问题,我具有以下递归算法:

void Hanoi4(int nDisks, char source, char intermed1, char intermed2, char dest)
{
    if ( nDisks == 1 )
        cout << source << " --> " << dest << endl;
    else if ( nDisks == 2 )
    {
        cout << source << " --> " << intermed1 << endl;
        cout << source << " --> " << dest << endl;
        cout << intermed1 << " --> " << dest << endl;
    }
    else
    {
        Hanoi4(nDisks - 2, source, intermed2, dest, intermed1);
        cout << source << " --> " << intermed2 << endl;
        cout << source << " --> " << dest << endl;
        cout << intermed2 << " --> " << dest << endl;
        Hanoi4(nDisks - 2, intermed1, source, intermed2, dest);
    }
}

int main()
{
    Hanoi4(3, 'A', 'B', 'C', 'D');

    return 0;
}

现在,我的问题是如何将这种递归方法推广到K钉子上?递归函数将收到一个char[]包含每个堆栈的标签的,因此该函数将如下所示:

void HanoiK(int nDisks, int kStacks, char labels[]) { ... }

我知道帧斯图尔特算法,这是最有可能的优化,但没有得到证实,和它给你的 号码
的移动。但是,我对严格的递归解决方案感兴趣,该解决方案遵循3钉和4钉的递归解决方案的模式,这意味着它可以打印实际的移动。

至少对我来说,Wikipedia上提出的Frame-
Stewart算法的伪代码是相当抽象的,而且我还没有成功地将其翻译成可打印动作的代码。我会接受该参考实现(对于random k),或更详细的伪代码。

我试图提出一种可以相应地对标签数组进行排列的算法,但是我没有运气让它起作用。任何建议表示赞赏。

更新:

用功能语言解决起来似乎容易得多。这是一个基于LarsH的Haskell解决方案的F#实现:

let rec HanoiK n pegs = 
    if n > 0 then 
        match pegs with
        | p1::p2::rest when rest.IsEmpty            
            ->  printfn "%A --> %A" p1 p2
        | p1::p2::p3::rest when rest.IsEmpty        
            ->  HanoiK (n-1) (p1::p3::p2::rest)
                printfn "%A --> %A" p1 p2
                HanoiK (n-1) (p3::p2::p1::rest)    
        | p1::p2::p3::rest when not rest.IsEmpty    
            ->  let k = int(n / 2)
                HanoiK k (p1::p3::p2::rest)
                HanoiK (n-k) (p1::p2::rest)
                HanoiK k (p3::p2::p1::rest)

let _ =
    HanoiK 6 [1; 2; 3; 4; 5; 6]

并且不将3钉作为边缘包:

let rec HanoiK n pegs = 
    if n > 0 then 
        match pegs with
        | p1::p2::rest when rest.IsEmpty            
            ->  printfn "%A --> %A" p1 p2
        | p1::p2::p3::rest     
            ->  let k = if rest.IsEmpty then n - 1 else int(n / 2) 
                HanoiK k (p1::p3::p2::rest)
                HanoiK (n-k) (p1::p2::rest)
                HanoiK k (p3::p2::p1::rest)

请注意,这无法处理没有解决方案的退化案例,例如 HanoiK 2 [1; 2]


阅读 306

收藏
2020-07-28

共1个答案

一尘不染

这是Haskell中的一个实现 (更新: 通过在r = 3时使k = n-1来处理3-peg情况):

-- hanoi for n disks and r pegs [p1, p2, ..., pr]
hanoiR :: Int -> [a] -> [(a, a)]

-- zero disks: no moves needed.
hanoiR 0 _ = []

-- one disk: one move and two pegs needed.
hanoiR 1 (p1 : p2 : rest) = [(p1, p2)] -- only needed for smart-alecks?

{-
-- n disks and 3 pegs -- unneeded; covered by (null rest) below.
hanoiR n [p1, p2, p3] =
    hanoiR (n - 1) [p1, p3, p2] ++
    [(p1, p2)] ++
    hanoiR (n - 1) [p3, p2, p1]
-}

-- n disks and r > 3 pegs: use Frame-Stewart algorithm
hanoiR n (p1 : p2 : p3 : rest) =
    hanoiR k (p1 : p3 : p2 : rest) ++
    hanoiR (n - k) (p1 : p2 : rest) ++
    hanoiR k (p3 : p2 : p1 : rest)
    where k
        | null rest   = n - 1
        | otherwise   = n `quot` 2

因此将其加载到GHCi中并输入

hanoiR 4 [1, 2, 3, 4]

即使用4个磁盘和4个钉子运行河内塔。您可以随意命名4个钉,例如

hanoiR 4 ['a', 'b', 'c', 'd']

输出:

[(1,2),(1,3),(2,3),(1,4),(1,2),(4,2),(3,1),(3,2),(1,2)]

即将顶部磁盘从钉1移至钉2,然后将顶部磁盘从钉1移至钉3,依此类推。

我对Haskell来说还很陌生,所以我必须承认我为自己的工作感到自豪。但是我可能会犯一些愚蠢的错误,因此欢迎您提供反馈。

从代码中可以看到,k的启发式只是floor(n / 2)。我没有尝试优化k,尽管n / 2似乎是一个不错的猜测。

我已经验证了4个磁盘和4个钉子的答案的正确性。在不编写模拟器的情况下,对我进行更多验证已经太晚了。(@ _ @)以下是一些结果:

ghci>  hanoiR 6 [1, 2, 3, 4, 5]
[(1,2),(1,4),(1,3),(4,3),(2,3),(1,4),(1,5),(1,2),
 (5,2),(4,2),(3,1),(3,4),(3,2),(4,2),(1,2)]
ghci>  hanoiR 6 [1, 2, 3, 4]
[(1,2),(1,4),(1,3),(4,3),(2,3),(1,2),(1,4),(2,4),(1,2),
 (4,1),(4,2),(1,2),(3,1),(3,4),(3,2),(4,2),(1,2)]
ghci>  hanoiR 8 [1, 2, 3, 4, 5]
[(1,3),(1,2),(3,2),(1,4),(1,3),(4,3),(2,1),(2,3),(1,3),(1,2),
 (1,4),(2,4),(1,5),(1,2),(5,2),(4,1),(4,2),(1,2),
 (3,2),(3,1),(2,1),(3,4),(3,2),(4,2),(1,3),(1,2),(3,2)]

这是否阐明了算法?

真的必不可少的是

hanoiR k (p1 : (p3 : (p2 : rest))) ++      -- step 1; corresponds to T(k,r)
hanoiR (n-k) (p1 : (p2 : rest)) ++         -- step 2; corresponds to T(n-k, r-1)
hanoiR k (p3 : (p2 : (p1 : rest)))         -- step 3; corresponds to T(k,r)

在这里,我们将Frame-Stewart算法的步骤1、2和3的移动顺序连接起来。为了确定移动,我们对FS的步骤进行了注释,如下所示:

  • 按照惯例,在调用hanoi时,目标(不失一般性)定义为将磁盘从第一个桩转移到第二个桩,并使用所有剩余的桩进行临时存储。在递归时,我们使用此约定来定义划分和征服的子问题的源,目标和允许的存储。
  • 因此,源钉为p1,目标钉为p2。对于最高级的河内问题,所有其余的钉都可用作临时存储。
  • 步骤1,“对于某个k,1 <= k <n,将前k个磁盘转移到另一个单独的桩上”:我们选择p3作为“另一个单独的桩钉”。
  • 因此,“不打扰现在包含前k个磁盘的钉”(步骤2)意味着使用除p3之外的所有钉进行递归。即p1,p2,其余部分则超出p3。
  • “将前k个磁盘转移到目标钉”(步骤3)表示从“另一个钉”(p3)转移到p2。

那有意义吗?

2020-07-28