tomohxxの日記

麻雀プログラミング

麻雀で学ぶアルゴリズム(1) 手牌/牌山生成(全幅)

先日このような記事を書きました。

tomohxx.hatenablog.com

Twitterで多くの人が興味をもってくれたようでうれしいです。そんなわけで麻雀をテーマにアルゴリズムを勉強していきたいと思います。

1回目のテーマは「手牌/牌山生成(全幅)」です。「全幅」の意味はすべての手牌/牌山を生成するという意味です。今回考える問題は以下のようになります。

萬子を M枚使ってできる牌の組み合わせをすべて生成せよ。

問題文の萬子には意味がなくて筒子でも索子でも字牌でもいいです。では解いていきます。 n萬の枚数を h_n \ (1 \le n \le 9)とします。今 n萬の枚数を考えていて、( n萬を含め) m枚牌を使わなければならないとします。まず n萬の枚数がとりうる最大値は {\rm min}(4, m)です。次に最小値を考えます。 n萬の枚数を決めた後は 4(9-n)枚が未使用になっています。 mがこの値より多いときはその差を最小値とする必要があります。よって n萬の枚数がとりうる最小値は {\rm max}(0, m-4(9-n))です。 n+1萬の枚数については m m-h_nに置き換えて同様に考えればよいです。この処理を再帰関数で実装します。添え字を1ずらしていることに注意してください。

#include <iostream>
#include <functional>
#include <vector>

void generate(int n, int m, std::function<void(const std::vector<int>&)> callback)
{
  static std::vector<int> h(9);

  if(n == 9){
    callback(h);
  }
  else{
    for(int i=std::max(0, m-4*(8-n)); i<=std::min(4, m); ++i){
      h[n] = i;
      generate(n+1, m-i, callback);
    }
  }
}

int main()
{
  int M;
  // 入力
  std::cin >> M;
  // M枚からなる組み合わせをすべて生成する
  generate(0, M, [](const std::vector<int>& h){
    // hを使った処理
    // 組み合わせを表示する例
    for(const auto& i : h){
      std::cout << i << ' ';
    }
    std::cout << '\n';
  });

  return 0;
}

最後にいくつか補足しておきます。

  • すべての組み合わせの数だけ知りたい場合はこれよりも良い方法が存在します。そのような問題は重複組み合わせと呼ばれます。
  • 全種類(萬子、筒子、索子、字牌)の牌が使えるとして組み合わせを生成することもできますが、実際のところその処理が必要になることはありません。