在河内塔问题是递归的一个经典问题。给您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[]包含每个堆栈的标签的,因此该函数将如下所示:
K
char[]
void HanoiK(int nDisks, int kStacks, char labels[]) { ... }
我知道帧斯图尔特算法,这是最有可能的优化,但没有得到证实,和它给你的 号码 的移动。但是,我对严格的递归解决方案感兴趣,该解决方案遵循3钉和4钉的递归解决方案的模式,这意味着它可以打印实际的移动。
至少对我来说,Wikipedia上提出的Frame- Stewart算法的伪代码是相当抽象的,而且我还没有成功地将其翻译成可打印动作的代码。我会接受该参考实现(对于random k),或更详细的伪代码。
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]
HanoiK 2 [1; 2]
这是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的步骤进行了注释,如下所示:
那有意义吗?