2012-01-11 5 views
4

GetEnumerator()の再帰バージョンを作成する方法に関するアドバイスをいただけますか? よく知られているTowers of Hanoi problemは、私が実際に抱えている問題に匹敵する例として役立ちます。高さnのディスクのスタックのためのすべての動きを表示する単純なアルゴリズムは次のとおりです。C#GetEnumerator()の再帰バージョンを作成する方法

void MoveTower0 (int n, Needle start, Needle finish, Needle temp) 
{ 
    if (n > 0) 
    { 
    MoveTower0 (n - 1, start, temp, finish); 
    Console.WriteLine ("Moving disk from {0} to {1}", start, finish); 
    MoveTower0 (n - 1, temp, finish, start); 
    } 
} 

私は実際にIEnumerableを実装するクラスのHanoiTowerMovesを設定されてやりたい、それが次のようにすべての動きを反復処理するために私を可能にします:

foreach (Move m in HanoiTowerMoves) Console.WriteLine (m); 

GetEnumeratorメソッド()の実装に向けた最初のステップは、MoveTowerパラメータを取り除くように思われます。これは、スタックを使用して簡単に行うことができます。私はクラスMoveを導入して、パラメータを単一の変数に結合しました。次のように

class Move 
{ 
    public int N { private set; get; } 
    public Needle Start { private set; get; } 
    public Needle Finish { private set; get; } 
    public Needle Temp { private set; get; } 

    public Move (int n, Needle start, Needle finish, Needle temp) 
    { 
    N = n; 
    Start = start; 
    Finish = finish; 
    Temp = temp; 
    } 

    public override string ToString() 
    { 
    return string.Format ("Moving disk from {0} to {1}", Start, Finish); 
    } 
} 

今MoveTowerを書き換えることができます。

void MoveTower1() 
{ 
    Move m = varStack.Pop(); 

    if (m.N > 0) 
    { 
    varStack.Push (new Move (m.N - 1, m.Start, m.Temp, m.Finish)); 
    MoveTower1(); 
    Console.WriteLine (m); 
    varStack.Push (new Move (m.N - 1, m.Temp, m.Finish, m.Start)); 
    MoveTower1(); 
    } 
} 

次のようにこのバージョンが呼び出される必要があります。

varStack.Push (new Move (n, Needle.A, Needle.B, Needle.Temp)); 
MoveTower1(); 

反復可能なバージョンに向けた次のステップは、クラスを実装することです:

class HanoiTowerMoves : IEnumerable<Move> 
{ 
    Stack<Move> varStack; 
    int n; // number of disks 

    public HanoiTowerMoves (int n) 
    { 
    this.n = n; 
    varStack = new Stack<Move>(); 
    } 

    public IEnumerator<Move> GetEnumerator() 
    { 
    // ???????????????????????????? } 

    // required by the compiler: 
    IEnumerator IEnumerable.GetEnumerator() 
    { 
    return GetEnumerator(); 
    } 
} 

今私にとって大きな疑問は、GetEnumerator()の本体はどのように見えますか? 誰かが私のためにこの謎を解くことができますか?

以下は作成したコンソールアプリケーションのProgram.csのコードです。

using System; 
using System.Collections.Generic; 
using System.Collections; 

/* Towers of Hanoi 
* =============== 
* Suppose you have a tower of N disks on needle A, which are supposed to end up on needle B. 
* The big picture is to first move the entire stack of the top N-1 disks to the Temp needle, 
* then move the N-th disk to B, then move the Temp stack to B using A as the new Temp needle. 
* This is reflected in the way the recursion is set up. 
*/ 

namespace ConsoleApplication1 
{ 
    static class main 
    { 
    static void Main (string [] args) 
    { 
     int n; 
     Console.WriteLine ("Towers of Hanoi"); 

     while (true) 
     { 
     Console.Write ("\r\nEnter number of disks: "); 

     if (!int.TryParse (Console.ReadLine(), out n)) 
     { 
      break; 
     } 

     HanoiTowerMoves moves = new HanoiTowerMoves (n); 
     moves.Run (1); // algorithm version number, see below 
     } 
    } 
    } 

    class Move 
    { 
    public int N { private set; get; } 
    public Needle Start { private set; get; } 
    public Needle Finish { private set; get; } 
    public Needle Temp { private set; get; } 

    public Move (int n, Needle start, Needle finish, Needle temp) 
    { 
     N = n; 
     Start = start; 
     Finish = finish; 
     Temp = temp; 
    } 

    public override string ToString() 
    { 
     return string.Format ("Moving disk from {0} to {1}", Start, Finish); 
    } 
    } 

    enum Needle { A, B, Temp } 

    class HanoiTowerMoves : IEnumerable<Move> 
    { 
    Stack<Move> varStack; 
    int n;   // number of disks 

    public HanoiTowerMoves (int n) 
    { 
     this.n = n; 
     varStack = new Stack<Move>(); 
    } 

    public void Run (int version) 
    { 
     switch (version) 
     { 
     case 0: // Original version 
      MoveTower0 (n, Needle.A, Needle.B, Needle.Temp); 
      break; 

     case 1: // No parameters (i.e. argument values passed via stack) 
      varStack.Push (new Move (n, Needle.A, Needle.B, Needle.Temp)); 
      MoveTower1(); 
      break; 

     case 2: // Enumeration 
      foreach (Move m in this) 
      { 
      Console.WriteLine (m); 
      } 

      break; 
     } 
    } 

    void MoveTower0 (int n, Needle start, Needle finish, Needle temp) 
    { 
     if (n > 0) 
     { 
     MoveTower0 (n - 1, start, temp, finish); 
     Console.WriteLine ("Moving disk from {0} to {1}", start, finish); 
     MoveTower0 (n - 1, temp, finish, start); 
     } 
    } 

    void MoveTower1() 
    { 
     Move m = varStack.Pop(); 

     if (m.N > 0) 
     { 
     varStack.Push (new Move (m.N - 1, m.Start, m.Temp, m.Finish)); 
     MoveTower1(); 
     Console.WriteLine (m); 
     varStack.Push (new Move (m.N - 1, m.Temp, m.Finish, m.Start)); 
     MoveTower1(); 
     } 
    } 

    public IEnumerator<Move> GetEnumerator() 
    { 
     yield break; // ???????????????????????????? 
    } 

    /* 
     void MoveTower1() 
     { 
     Move m = varStack.Pop(); 

     if (m.N > 0) 
     { 
      varStack.Push (new Move (m.N - 1, m.Start, m.Temp, m.Finish)); 
      MoveTower1(); 
      Console.WriteLine (m); ? yield return m; 
      varStack.Push (new Move (m.N - 1, m.Temp, m.Finish, m.Start)); 
      MoveTower1(); 
     } 
     } 
    */ 

    // required by the compiler: 
    IEnumerator IEnumerable.GetEnumerator() 
    { 
     return GetEnumerator(); 
    } 
    } 
} 
+1

私は、GetEnumeratorを再帰的に呼び出す点について理解していません。このメソッドを呼び出す再帰関数を書いてください。難しくありません。 –

答えて

12

あなたのアプローチはかなり良いですが、私はあなたが多少問題を過度に思っていると思います。一歩前に戻ってみましょう。あなたは再帰アルゴリズムを持っています:

void MoveTowerConsole (int n, Needle start, Needle finish, Needle temp) 
{ 
    if (n > 0) 
    { 
    MoveTowerConsole (n - 1, start, temp, finish); 
    Console.WriteLine ("Moving disk from {0} to {1}", start, finish); 
    MoveTowerConsole (n - 1, temp, finish, start); 
    } 
} 

アルゴリズムの出力は、コンソール出力の束です。代わりに、アルゴリズムの出力をにして、コンソールに出力される文字列のシーケンスを希望するとします。このような方法がどのように見えるかを考えてみましょう。

まず名前を変更します。第二に、その戻り値の型を無効にすることはできません。 IEnumerable<string>

IEnumerable<string> MoveTower(int n, Needle start, Needle finish, Needle temp) 
{ 
    if (n > 0) 
    { 
    MoveTower(n - 1, start, temp, finish); 
    Console.WriteLine ("Moving disk from {0} to {1}", start, finish); 
    MoveTower(n - 1, temp, finish, start); 
    } 
} 

これは正しいですか?いいえ、私たちは何も返さず、まだコンソールにダンプしています。 イテレータは何を得たいですか?我々が得るためにイテレータを望む:

  • 秒再帰的ステップのために必要なすべての動き

最初の再帰的ステップのために必要なすべての動き

  • 現在の移動
  • だから我々は、変更それらを得るためのアルゴリズム:

    IEnumerable<string> MoveTower(int n, Needle start, Needle finish, Needle temp) 
    { 
        if (n > 0) 
        { 
        foreach(string move in MoveTower(n - 1, start, temp, finish)) 
         yield return move; 
        yield return string.Format("Moving disk from {0} to {1}", start, finish); 
        foreach(string move in MoveTower(n - 1, temp, finish, start)) 
         yield return move; 
        } 
    } 
    

    これで完了です!そのように簡単です。再帰アルゴリズムを再帰的列挙子に変換するためにクラス全体を定義する必要はありません。コンパイラはあなたのためにその作業をさせてください。あなたが「移動」を列挙する方法にこれを変更したい場合は

    、その操作を行います。

    IEnumerable<Move> MoveTower(int n, Needle start, Needle finish, Needle temp) 
    { 
        if (n > 0) 
        { 
        foreach(Move move in MoveTower(n - 1, start, temp, finish)) 
         yield return move; 
        yield return new Move(start, finish); 
        foreach(Move move in MoveTower(n - 1, temp, finish, start)) 
         yield return move; 
        } 
    } 
    

    今、私は効率性に基づいて、このコードを批判ます。このように再帰的な列挙子を作ることによって、n個の列挙子の連鎖を構築することができます。次の項目を必要とするとき、トップの列挙子は、次の列挙子を呼び出して、下の列挙子をn深く呼び出します。ですから、各ステップは実際にはnステップを経て完了します。私はその理由のために再帰なしで問題を解決する傾向があります。

    練習:それはすべてので何再帰もしないように、上記の反復子ブロックを書き換えます。明示的なスタックを使用するソリューションは、正しい方向へのステップですが、それでも再帰は行われます。あなたは再帰が行われないようにそれを適応させることができますか?

    あなたはIEnumerable<Move>は、あなたが簡単な方法で上記のコードを適応させることができます実装するクラスを書く時に曲がっている場合は、次の

    class MoveIterator : IEnumerable<Move> 
    { 
        public IEnumerator<Move> GetEnumerator() 
        { 
         foreach(Move move in MoveTower(whatever)) 
          yield return move; 
        } 
    

    あなたが列挙子を返すメソッドを実装するために降伏リターンを使用することができます列挙型

  • +0

    1つ以上のソリューションを入力すると私より速くなります。 –

    +0

    こんにちはEric、 あなたのソリューションを提示する洞察とステップバイステップのために、ありがとうございます。実際には、問題を間違った方法で見て、どの方法を使うのかわからなくなってしまったのです。非常に有益で有益な情報! 実際には、2つのスタックを使用する非再帰的なソリューションがありました。下記の答えを参照してください。 もう一度おねがいします! Johnプール Amersfoort Netherland –

    +0

    残念ながら、私の元の問題では、再帰を削除するのは簡単ではありません。再帰呼び出しは複数レベルの条件付きステートメントに埋め込まれており、コードをステートエンジンに変換するとかなり読みにくくなります。だから、私は入れ子にされた列挙型ソリューションで生きなければならないと思います。パフォーマンスが受け入れられる限り問題はありません。 –

    0

    非再帰バージョン:

    // Non-recursive version -- state engine 
    //rta.Push (State.Exit); 
    //parameters.Push (new Move (n, Needle.A, Needle.B, Needle.Temp)); 
    //MoveTower3(); 
    
    enum State { Init, Call1, Call2, Rtrn, Exit } 
    
    { 
        ... 
    
        #region Non-recursive version -- state engine 
        static void MoveTower3() 
        { 
        State s = State.Init; 
        Move m = null; 
    
        while (true) 
         switch (s) 
         { 
         case State.Init: 
          m = moveStack.Pop(); 
          s = (m.n <= 0) ? State.Rtrn : State.Call1; 
          break; 
         case State.Call1: 
          rta.Push (State.Call2); // where do I want to go after the call is finished 
          moveStack.Push (m); // save state for second call 
          moveStack.Push (new Move (m.n-1, m.start, m.temp, m.finish)); // parameters 
          s = State.Init; 
          break; 
         case State.Call2: 
          m = moveStack.Pop(); // restore state from just before first call 
          Console.WriteLine (m); 
          rta.Push (State.Rtrn); 
          moveStack.Push (new Move (m.n-1, m.temp, m.finish, m.start)); 
          s = State.Init; 
          break; 
         case State.Rtrn: 
          s = rta.Pop(); 
          break; 
         case State.Exit: 
          return; 
         } 
        } 
        #endregion 
    
        #region Enumeration 
        static IEnumerable<Move> GetEnumerable (int n) 
        { 
        Stack<Move> moveStack = new Stack<Move>(); 
        Stack<State> rta = new Stack<State>(); // 'return addresses' 
        rta.Push (State.Exit); 
        moveStack.Push (new Move (n, Needle.A, Needle.B, Needle.Temp)); 
        State s = State.Init; 
        Move m = null; 
    
        while (true) 
         switch (s) 
         { 
         case State.Init: 
          m = moveStack.Pop(); 
          s = (m.n <= 0) ? State.Rtrn : State.Call1; 
          break; 
         case State.Call1: 
          rta.Push (State.Call2); // where do I want to go after the call is finished 
          moveStack.Push (m); // save state for second call 
          moveStack.Push (new Move (m.n-1, m.start, m.temp, m.finish)); // parameters 
          s = State.Init; 
          break; 
         case State.Call2: 
          m = moveStack.Pop(); // restore state from just before first call 
          yield return m; 
          rta.Push (State.Rtrn); 
          moveStack.Push (new Move (m.n-1, m.temp, m.finish, m.start)); 
          s = State.Init; 
          break; 
         case State.Rtrn: 
          s = rta.Pop(); 
          break; 
         case State.Exit: 
          yield break; 
         } 
        } 
        #endregion 
    } 
    
    1

    あなたの非再帰的なソリューションは良いです - (基本的に、スタックとステートマシン)プッシュダウンオートマトンを構築する反復バージョンを構築するための標準的な技術で再帰的な解法。実際、これはイテレータブロックと非同期ブロックのコードを生成する方法と非常によく似ています。

    しかし、この特定のケースでは、スイッチと現在の状態でプッシュダウンオートマトンの重機を引き出す必要はありません。あなたはこれを行うことができます:

    IEnumerable<Move> MoveTowerConsole (int size, Needle start, Needle finish, Needle temp) 
    { 
        if (size <= 0) yield break; 
        var stack = new Stack<Work>(); 
        stack.Push(new Work(size, start, finish, temp)); 
        while(stack.Count > 0) 
        { 
        var current = stack.Pop(); 
        if (current.Size == 1) 
         yield return new Move(current.Start, current.Finish); 
        else 
        { 
         // Push the work in the *opposite* order that it needs to be done. 
         stack.Push(new Work(current.Size - 1, current.Temp, current.Finish, current.Start)); 
         stack.Push(new Work(1, current.Start, current.Finish, current.Temp)); 
         stack.Push(new Work(current.Size - 1, current.Start, current.Temp, current.Finish)); 
    
        } 
    } 
    

    あなたはすでにあなたが現在の再帰的ステップの後にやってする必要がある仕事を正確に知っているので、スタック上の作業の3つのビットを入れてスイッチの周りにバウンスする必要はありません。ただキューすべてを一度に所定のステップで処理してください。

    +0

    こんにちはエリック、ありがとう、あなたの非再帰的なソリューション!私はそれが私に少し魔法のように見えることを認めなければならない。私は大きなアイデアのあいまいな概念を持っています。あなたが待ち行列から重要ではないサイズ(すなわち> 1)の「仕事」を選ぶと、それは3つの部分で分割されます。そうでない場合、アルゴリズムは終了せず、それらをキューに戻します。 –

    +0

    ...些細なことに遭遇するたびに、それを返し、キューに何も残らないまでこれを繰り返します。 Workクラスは、再帰が使用されるときにスタック上で通常行われるすべてのローカル変数の単なるコンテナです。それはそれを見る正しい方法ですか?このように*再帰関数を*書き換えることは可能ですか?もっと多くの例があるのか​​、この件に関する記事への言及はありますか? ありがとう - ジョン –

    +0

    @ JohnPool:まず、私の解決策は間違っていた - 私はスタックではなく、キューを使用している必要があります。しかし、はい、それは基本的なアイデアです。すべての再帰関数を簡単にこのように書き換えることはできません。 –

    関連する問題