河内的塔与K钉

时间:2010-08-31 08:05:18

标签: algorithm haskell f# recursion functional-programming

Towers of Hanoi问题是递归的典型问题。在其中一个上有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个钉子,所以我们添加另一个中间挂钩。当面临必须在任何一点选择哪个中间挂钩的问题时,我们将选择最左边的一个,以防1个以上是免费的。

我有这个问题的递归算法:

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[]) { ... }

我知道Frame-Stewart算法,它最有可能是最优的但未经过验证,并且可以为您提供数量的移动。但是,我对一个严格的递归解决方案感兴趣,该解决方案遵循3和4个挂钩的递归解决方案的模式,这意味着它打印实际的移动。

至少对我来说,维基百科上提出的Frame-Stewart算法的伪代码是相当抽象的,我没有成功地将其转换成打印动作的代码。我会接受它的参考实现(对于随机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]

7 个答案:

答案 0 :(得分:16)

这是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的移动序列。为了确定移动,我们按如下方式注释F-S的步骤:

  • 传统上,当呼叫河内时,目标被定义(不失一般性),因为将磁盘从第一个挂钩转移到第二个挂钩,使用所有剩余的挂钉进行临时存储。我们在递归时使用这个约定来定义分割和被征服的子问题的源,目的地和允许的存储。
  • 因此源peg是p1,目标peg是p2。所有剩余的钉子都可以作为临时存储,用于顶级河内问题。
  • 步骤1,“对于某些k,1&lt; = k&lt; n,将前k个磁盘转移到单个其他挂钩”:我们选择p3作为“一个其他挂钩”。
  • 因此“不打扰现在包含前k个磁盘的挂钩”(步骤2)意味着使用除p3之外的所有挂钩递归。即p1,p2,其余的超出p3。
  • “将前k个磁盘转移到目的地挂钩”(步骤3)表示从“其他挂钩”(p3)转移到p2。

这有意义吗?

答案 1 :(得分:2)

要解决河内之塔,您需要做的就是:

Frame Stewart算法并不那么复杂。基本上,您必须将一定数量的磁盘(例如,其中一半)移动到一些挂钩:将这些磁盘视为自己的独立磁盘。很容易为1或2个磁盘定义解决方案,并且将上半部分移动到目的地,将后半部分移动到最终需要的位置。

如果你想让它更易于编写(唯一的特殊情况变为1),你可以不断对其进行分段,但是没有大量的钉子,它将无法工作。

此外,如果k >= 3,您可以通过简单地忽略其余的钉子来解决它,就像河内的3个钉塔一样,尽管这不是最佳的。

答案 2 :(得分:2)

欣泽,拉尔夫。 功能珍珠:La Tour D'Hanoi http://www.comlab.ox.ac.uk/ralf.hinze/publications/ICFP09.pdf

  

这颗珍珠旨在展示   全麦和投射的思想   使用河内塔编程   拼图作为一个运行的例子。该   拼图有自己的美,我们   希望一路暴露。

答案 3 :(得分:1)

我想贡献我认为是 Frame-Stewart 算法中最优的 k。对于无法解决 Hanoi Towers 的情况,没有定义算法本身。

frameStewart :: Integer -> Integer -> Integer

frameStewart 1 _ = 1
frameStewart n 3 = (2^n) - 1
frameStewart n r
  | n < r     = 2*n - 1
  | otherwise = frameStewart k r + frameStewart (n-k) (r-1) + frameStewart k r
    where
      k = solve (2*n `div` r)
        where
          solve a 
            | a * (2*n - a) <= (n^2 - 2*a) = a
            | otherwise = solve (a-1)

wikipedia page表示n个圆盘减去2*n + 1的平方根,在特殊括号内求最接近的整数,+ 1等于k .我发现通过考虑 n = 4k = 2 来摆脱这些括号很有用,并操纵公式直到我得到代码中存在的不等式。仅此一项就解决了 4 个钉子的问题。

最后一步是确定用哪个值来解决该不等式,并认为它是 2*n 除以挂钩数 r。我认为它的结果与 Ben Houston 研究中的 5 和 6 个挂钩的表格相匹配,这些表格出现在 here

这不会给你一个移动列表,但是,只有最小数量。

results for 5 pegs variant of Tower of Hanoi results for 6 pegs variant of Tower of Hanoi

答案 4 :(得分:0)

1883年,法国数学家爱德华·卢卡斯(Edouard Lucas)以笔名N. Lucas de Siam向河流世界发布了“河内之塔”。 &#34;传奇&#34;伴随着游戏的是,在贝纳雷斯,在皇帝佛嗨统治期间,有一个印第安寺庙,其圆顶标志着世界的中心(Kashi Vishwanath)。在圆顶内,(婆罗门)牧师在3个钻石针尖(破旧的柱子)之间移动金色圆盘,高度为肘高,与蜜蜂的身体一样厚。在创造时,上帝(梵天)在一根针上放置了64个金盘。 (光盘根据Brahma的不变法则移动,将它们从一个钉子转移到另一个钉子)据说,当他们完成任务时,宇宙就会结束。图例在不同的网站上有所不同,但一般都是一致的。     &#39;法律&#39;由Brahma设定如下: 1)一次只能移动1张光盘 2)没有光盘可以放在较小的光盘上 3)只能移除顶部光盘,然后将其放置在另一个挂钉及其光盘的顶部 当整堆光盘移动到另一个挂钉时,游戏结束。很快发现3 peg溶液存在,但没有解决4 + peg溶液。     1939年,美国数学月刊举办了一场竞赛,以解决和制作光盘问题。两年后,J. S. Frame和B. M. Stewart发表了两个独立的(但后来证明相同的)算法。两者都尚未被证实是正确的,尽管它们通常被认为是正确的。在适当的解决方案上还没有进一步的进展。 *****这仅适用于3个挂钩问题*****     n个磁盘塔的最小移动次数很快显示为2n-1,简单的递归解决方案如下:标记三个桩的起点,目标和温度。通过temp peg将n pegs从起始栓钉移动到目标钉: 对于n> 1, (i)递归地将顶部n-1个磁盘从开始移动到临时目标。 (ii)将第n个磁盘从开始移动到目标。 (iii)通过start递归地将n-1个磁盘从temp移动到目标。 此解决方案需要2n-1个动作: (1)如果n = 1,则f(n)= 1 = 2n-1 (2)如果n> 1,f(n)= 2 *(2n-1-1)+ 1 = 2n-2 + 1 = 2n-1     解决它的简单方法...... 1,3,7,15,31是前几张光盘的解决方案。递归地类似于nk = 2nk-1 + 1。从那里我们可以诱导n = 2n-1。通过归纳证明我留给你。 *****基本的Frame / Stewart算法*****     对于m peg和n个圆盘,选择1使得0≤l<1。 n(而l是一个整数,可以最大限度地减少以下设置中的步骤)...... •将顶部l盘从起始桩移动到中间挂钉;这可以在Hk(l)移动中完成(因为底部n-1个盘根本不干扰移动)。 •使用Hk-1(n-l)移动将底部n-l个磁盘从起始栓钉移动到目标栓钉。 (由于一个钉子被一个较小的圆盘塔占用,因此在这个阶段不能使用它。) •将原始l个磁盘从中间挂钩移动到Hk(l)移动中的目标挂钩。 所以基本上它是Hk(n)= 2Hk(l)+ Hk-1(n-1)-----&gt; l最小化 *****非常简单!!不!*****     验证它是否适用于我们的3 peg解决方案应该很容易。 使用k = 2;我们设置H2(0)= 0,H2(1)= 1,H2(n> 1)=∞。 对于k = 3,我们可以设置l = n-1。 (它导致我们的H2(n-1)变得有限)这将允许我们写入H3(n)= 2H3(n-1)+ H2(1),其等于2n-1。这是一个文字游戏,但它的工作原理。 *****略有不同的描述有助于澄清*****     Frame-Stewart算法给出了四个(甚至更多)桩钉的最佳解决方案,如下所述: 将H(n,m)定义为使用m pegs传输n个磁盘所需的最小移动次数 该算法可以递归地描述: 1.对于一些l,1

`Option VBASupport 1
Option Explicit
Dim n as double
dim m as double
dim l as double
dim rx as double
dim rxtra as double
dim r as double
dim x as double
dim s1 as double
dim s2 as double
dim i as integer
dim a ()
dim b ()
dim c ()
dim d ()
dim aa as double
dim bb as double
dim cc as double
dim dd as double
dim total as double

Sub Hanoi
on error goto errorhandler

m=inputbox ("m# pegs=??")
n=inputbox ("n# discs=??")
x=-1
l=m-1
rx=1
s1=0
s2=0
aa=0

while n>rx
        x=x+1
        r=(l+x)/(x+1)
        rx=r*rx
wend
rx=1
for i=0 to x-1
        r=(l+i)/(i+1)
        rx=r*rx
        redim a (-1 to x)
        redim b (-1 to x)
        redim c (-1 to x)
        redim d (-1 to x)
            a(i)=rx
            b(i)=i
            bb=b(i)
            c(i)=rx-aa
            aa=a(i)
            cc=c(i)
            d(i)=cc*2^bb
            dd=d(i)
            s1=s1+dd
next

rxtra=n-aa
s2=rxtra*2^(bb+1)
total = 2*(s1+s2)-1
msgbox total

exit sub
errorhandler: msgbox "dang it!!"
'1, 3, 5, 9, 13, 17, 25, 33 first 8 answers for 4 peg
'16=161,25=577,32=1281,64=18433
End Sub`

披露:这些来源用于确认答案和问题的一些历史。由于多个网站用于验证,所以很难给出确切的信用,因此它们是历史上许多部分的所有来源。

答案 5 :(得分:-1)

Facebook k-peg N disc of hanoi可以通过BFS算法解决。如需解决方案,请访问http://www.woolor.com/InterviewMitra/41/facebook-k-peg-of-tower-of-hanoi-solution

答案 6 :(得分:-1)