イベント駆動で組み合わせを列挙する


組み合わせの列挙を行いたいということはしばしば起こります。例えば 0, 1, 2, 3, 4 の 5 つの数字から 3 つを取り出す組み合わせというのは {0, 1, 2}, {0, 1, 3}, {0, 1, 4}, {0, 2, 3}, {0, 2, 4}, {0, 3, 4}, {1, 2, 3}, {1, 2, 4}, {1, 3, 4}, {2, 3, 4} の 10 通りあります。普通に書く場合は『Using Combinations to Improve Your Software Test Case Generation』のように書けばよいのですが,ここではあえてイベントを利用して書いてみたいと思います。

まず組み合わせを構成する要素を表すクラスを作成します。

public class Element
{
   private readonly int capacity;
   private int value;

   public Element(int capacity, int value)
   {
      this.capacity = capacity;
      this.value = value;
   }

   public int Value
   {
      get { return this.value; }
   }

   public event EventHandler Exceeded;
   public event EventHandler ValueReset;

   public void Succeed()
   {
      this.value++;
      if (this.value >= this.capacity)
      {
         OnExceeded(EventArgs.Empty);
      }
   }

   public void Reset(int value)
   {
      this.value = value;
      OnValueReset(EventArgs.Empty);
   }

   protected virtual void OnExceeded(EventArgs e)
   {
      if (Exceeded != null)
      {
         Exceeded(this, e);
      }
   }

   protected virtual void OnValueReset(EventArgs e)
   {
      if (ValueReset != null)
      {
         ValueReset(this, e);
      }
   }
}

最初に挙げた例のように,要素を 1 つずつカウントアップしていくのが簡単です。カウントアップして取りうる値の最大値になったら,前の要素をカウントアップして,自身はリセットします。この要素間の連絡をイベントが担うわけですね。

次に要素を管理するクラスを作成します。

public class Combination
{
   private readonly LinkedList<Element> elements;
   private bool hasMore;

   public Combination(int n, int k)
   {
      if (n < 0)
      {
         throw new ArgumentOutOfRangeException("n");
      }
      if (k < 0 || n < k)
      {
         throw new ArgumentOutOfRangeException("k");
      }
      this.elements = new LinkedList<Element>();
      while (--k >= 0)
      {
         var count = this.elements.Count;
         var element = new Element(n - k, count);
         var node = this.elements.AddLast(element);
         AddEvent(node);
      }
      this.hasMore = true;
   }

   public bool HasMore
   {
      get { return this.hasMore; }
   }

   public int[] Current
   {
      get { return this.elements.Select(e => e.Value).ToArray(); }
   }

   public void MoveNext()
   {
      this.elements.Last.Value.Succeed();
   }

   private void AddEvent(LinkedListNode<Element> node)
   {
      var element = node.Value;
      if (node.Previous == null)
      {
         element.Exceeded += (sender, e) => this.hasMore = false;
      }
      else
      {
         var previous = node.Previous.Value;
         element.Exceeded += (sender, e) =>
         {
            previous.Succeed();
            element.Reset(previous.Value + 1);
         };
         previous.ValueReset += (sender, e) =>
         {
            element.Reset(previous.Value + 1);
         };
      }
   }
}

LinkedList に追加する際に,追加する要素に前の要素と連絡をするためのイベントを追加します。実装を簡単にするために IEnumerator<T> と少し違うインターフェイスになっています。さらに言えば, Current を呼ぶたびに毎回新しい配列を作っていて微妙です。修正は容易ですが,あえてこの実装を採用することもないので,簡単のためにこうしています。

以下のように使います。

for (var c = new Combination(5, 3); c.HasMore; c.MoveNext())
{
   var values = c.Current;
   Console.WriteLine(string.Join(", ", values));
}

// output
// 0, 1, 2
// 0, 1, 3
// 0, 1, 4
// 0, 2, 3
// 0, 2, 4
// 0, 3, 4
// 1, 2, 3
// 1, 2, 4
// 1, 3, 4
// 2, 3, 4