ハノイの塔 問題は、再帰の典型的な問題です。ディスクが1つあるペグが3つ与えられ、指定されたルールに従って、すべてのディスクを1つのペグから別のペグに移動する必要があります。また、最小数の移動でこれを行う必要があります。
この問題を解決する再帰アルゴリズムは次のとおりです。
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アルゴリズムについて知っています。これは最適である可能性が高いですが、証明されておらず、numberの動きを与えます。ただし、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]
などの解決策がない退化したケースを処理しないことに注意してください
Haskellでの実装は次のとおりです(update:は、r = 3のときにk = n-1にすることで3ペグのケースを処理しました):
-- 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のステップに次のように注釈を付けます。
それは理にかなっていますか?
ハノイの塔を解決するために必要なことは、次のとおりです。
フレームスチュワートアルゴリズムは実際にはそれほど複雑ではありません。基本的に、特定の数のディスク(たとえば、ディスクの半分)をペグに移動する必要があります。これらのディスクは、独自の別のタワーのように扱います。 1つまたは2つのディスクのソリューションを定義するのは簡単で、前半を目的地に移動し、後半を最終的に必要な場所に移動します。
書き込みを簡単にしたい(唯一の特殊なケースが1になる)場合は、継続的にセグメント化できますが、ペグの数が非常に多いと機能しません。
さらに、k >= 3
、残りのペグを無視するだけで、ハノイの3ペグタワーのように正確に解決できますが、最適ではありません。
ヒンズ、ラルフ。 Functional Pearl:La Tour D'Hanoi、 http://www.comlab.ox.ac.uk/ralf.hinze/publications /ICFP09.pdf
この真珠は、ハノイの塔パズルを実行例として使用して、全粒および射影プログラミングのアイデアを示すことを目的としています。パズルには独自の美しさがあるので、それを途中で公開したいと考えています。
ハノイの塔パズルは、1883年にフランスの数学者エドゥアールルーカスによってペンネームN.ルーカスデサイアムで西部世界に公開されました。ゲームに付随する「伝説」は、ベナレスでは、フォーハイ皇帝の治世中に、世界の中心を示すドームのあるインドの寺院(カシヴィシュワナート)があったと述べていました。ドーム内では、(ブラーミン)の司祭が黄金の円盤を3本のダイヤモンドの針先(すり切れた柱)の間で動かしました。神(ブラフマ)は、創造の時に64本の金の円盤を1本の針に置いた。 (ディスクはブラフマーからの不変の法則に従って移動され、ペグからペグに移されました)彼らが仕事を完了すると、宇宙は終わりになると言われていました。凡例はいくつかのサイトで異なりますが、一般的に一貫しています。 Brahmaによって設定された「法則」は次のとおりです。1)一度に移動できるディスクは1つだけです。2)小さいディスクにディスクを置くことはできません。3)一番上のディスクだけを取り出して別のディスクの上に置くことができます。ペグとそのディスクディスクのスタック全体が別のペグに移動されると、ゲームは終了します。 3ペグソリューションが存在することがすぐにわかりましたが、4ペグソリューションでは解決されませんでした。 1939年に、American Mathematical Monthlyは、mペグとnディスクを解決するためのコンテストを開催しました。 2年後、J。S.フレームとB. M.スチュワートによって2つの別々の(しかし後で証明された同等の)アルゴリズムが公開されました。どちらも正しいとはまだ証明されていませんが、一般的に正しいと見なされています。適切な解決策については、これ以上の進歩はありません。 *****これは3ペグの問題でのみ機能します***** n台のディスクのタワーの最小移動数は、次のような単純な再帰的な解決策で2n-1であることがすぐにわかりました。 、目標、および臨時雇用者。一時ペグを介して開始ペグから目標ペグにnペグを移動するには、次のようにします。n> 1の場合、(i)上位n-1ディスクを再帰的に、目標を介して開始から一時に移動します。 (ii)n番目のディスクを最初から最後まで移動します。 (iii)開始により、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は最初の数枚のnディスク。再帰的にnk = 2nk-1 + 1に似ています。そこからn = 2n-1を誘導できます。誘導による証明はあなたに任せます。*****基本的なフレーム/スチュワートアルゴリズム** *** mペグとnディスクの場合、0≤l <n(lは次のセットアップの手順を最小化する整数です)のようなlを選択します...•一番上のlディスクを開始ペグから中間ペグ;これはHk(l)の移動で実行できます(下部のn-lディスクは移動をまったく妨げないため)•Hk-1を使用して、開始ペグからゴールペグに下部n-lディスクを移動します(n-l)移動します(1つのペグが小さいディスクのタワーで占められているため、この段階では使用できません)。•元のlディスクを中間ペグからHk(l)移動の目標ペグに移動します。したがって、本質的にはHk(n)= 2Hk(l)+ Hk-1(n-1)-----> the l最小化*****パイのように簡単!!できません!***** 3ペグソリューションに対して動作することを確認するのは簡単です。 k = 2を使用; H2(0)= 0、H2(1)= 1、およびH2(n> 1)=∞を設定します。 k = 3の場合、l = n-1を設定できます。 (これによりH2(n-1)が有限になります)これにより、2n-1に等しいH3(n)= 2H3(n-1)+ H2(1)を書き込むことができます。言葉遊びのようですが、うまくいきます。 *****明確にするために少し異なる説明***** 4つの(またはそれ以上の)ペグに対しておそらく最適なソリューションを提供するFrame-Stewartアルゴリズムを以下に説明します:H(n、m)をmペグを使用して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`
開示:これらの情報源は、回答の確認と問題の履歴の一部として使用されました。検証に複数のサイトが使用されたため、原因がどこにあるかを正確に評価することは困難です...したがって、それらはすべて、履歴の多くの部分のソースです。