麻雀の手牌を一雀頭四面子に分解する簡単な方法

## はじめに

同じような記事はありますが、結局、実例と実際に動くプログラムで説明した方がわかりやすいと思うので記事にしてみます。

## 手書きで計算するとどうなるのか

実際の例として次の並びを考えてみます。

１２３４５６７１２赤５６７１２

(１)　赤牌を普通の牌に変える

１２３４５６７１２５６７１２

(２)　昇順にソートする

１１１２２２３４５５６６７７

(３-１)　刻子、対子、順子の順に元の並びの左端から左への移動を試していく

☆左端からというのは元の並びの一番左の数字を必ず移動しなければならないということ
☆この時、対子を左へ２個移動するのは禁止
☆この時、順子を左へ移動した直後に、同じ数字で始まる刻子や対子を左へ移動するのは禁止

１１１　　　　　　　　　　　　　　　　←　＊＊＊２２２３４５５６６７７

(３-２)　刻子、対子、順子の順に元の並びの左端から左への移動を試していく

１１１　２２２　　　　　　　　　　　　←　＊＊＊＊＊＊３４５５６６７７

(３-３)　刻子、対子、順子の順に元の並びの左端から左への移動を試していく

１１１　２２２　３４５　　　　　　　　←　＊＊＊＊＊＊＊＊＊５６６７７

(３-４)　刻子、対子、順子の順に元の並びの左端から左への移動を試していく

１１１　２２２　３４５　５６７　　　　←　＊＊＊＊＊＊＊＊＊＊＊６＊７

(４-４)　移動できなくなったとき一雀頭四面子できていれば完成してるので記録する

完成していないので何もしない

(５-４)　まだ試していないパターンがあれば一つ前の状態に戻して(３-４)の続きからやる

左側の最後が５６７なので順子まですべて試したので何もしない

(６-３)　まだ試していないパターンがなければ一つ前の状態に戻して(５-３)へ行く

１１１　２２２　３４５　　　　　　　　←　＊＊＊＊＊＊＊＊＊５６６７７

(５-３)　まだ試していないパターンがあれば一つ前の状態に戻して(３-３)の続きからやる

左側の最後が３４５なので順子まですべて試したので何もしない

(６-２)　まだ試していないパターンがなければ一つ前の状態に戻して(５-２)へ行く

１１１　２２２　　　　　　　　　　　　←　＊＊＊＊＊＊３４５５６６７７

(５-２)　まだ試していないパターンがあれば一つ前の状態に戻して(３-２)の続きからやる

左側の最後が２２２なので刻子まで試して対子の２２と順子の２３４は試していないので一つ前の状態に戻して(３-２)の続きからやる

１１１　　　　　　　　　　　　　　　　←　＊＊＊２２２３４５５６６７７

(３-２)　刻子、対子、順子の順に元の並びの左端から左への移動を試していく

１１１　２２　　　　　　　　　　　　　←　＊＊＊＊＊２３４５５６６７７

(３-３)　刻子、対子、順子の順に元の並びの左端から左への移動を試していく

１１１　２２　２３４　　　　　　　　　←　＊＊＊＊＊＊＊＊５５６６７７

(３-４)　刻子、対子、順子の順に元の並びの左端から左への移動を試していく

☆この時、対子を左へ２個移動するのは禁止なので５５の移動は試さない

１１１　２２　２３４　５６７　　　　　←　＊＊＊＊＊＊＊＊＊５＊６＊７

(３-５)　刻子、対子、順子の順に元の並びの左端から左への移動を試していく

１１１　２２　２３４　５６７　５６７　←　＊＊＊＊＊＊＊＊＊＊＊＊＊＊

(４-５)　移動できなくなったとき一雀頭四面子できていれば完成してるので記録する

完成したので「１１１　２２　２３４　５６７　５６７」を記録する

(５-５)　まだ試していないパターンがあれば一つ前の状態に戻して(３-５)の続きからやる

左側の最後が５６７なので順子まですべて試したので何もしない

(６-４)　まだ試していないパターンがなければ一つ前の状態に戻して(５-４)へ行く

１１１　２２　２３４　５６７　　　　　←　＊＊＊＊＊＊＊＊＊５＊６＊７

(５-４)　まだ試していないパターンがあれば一つ前の状態に戻して(３-４)の続きからやる

左側の最後が５６７なので順子まですべて試したので何もしない

(６-３)　まだ試していないパターンがなければ一つ前の状態に戻して(５-３)へ行く

１１１　２２　２３４　　　　　　　　　←　＊＊＊＊＊＊＊＊５５６６７７

(５-３)　まだ試していないパターンがあれば一つ前の状態に戻して(３-３)の続きからやる

左側の最後が２３４なので順子まですべて試したので何もしない

(６-２)　まだ試していないパターンがなければ一つ前の状態に戻して(５-２)へ行く

１１１　２２　　　　　　　　　　　　　←　＊＊＊＊＊２３４５５６６７７

(５-２)　まだ試していないパターンがあれば一つ前の状態に戻して(３-２)の続きからやる

左側の最後が２２なので対子まで試して順子の２３４は試していないので一つ前の状態に戻して(３-２)の続きからやる

１１１　　　　　　　　　　　　　　　　←　＊＊＊２２２３４５５６６７７

(３-２)　刻子、対子、順子の順に元の並びの左端から左への移動を試していく

１１１　２３４　　　　　　　　　　　　←　＊＊＊＊２２＊＊５５６６７７

☆この時、順子を左へ移動した直後に、同じ数字で始まる刻子や対子を左へ移動するのは禁止、左側の最後が２３４なので２で始まる右側の２２は移動できないのでこれ以上移動できるものがない

従って(３-３)ではなく(４-２)へ進む

(４-２)　移動できなくなったとき一雀頭四面子できていれば完成してるので記録する

完成していないので何もしない

(５-２)　まだ試していないパターンがあれば一つ前の状態に戻して(３-２)の続きからやる

左側の最後が２３４なので順子まですべて試したので何もしない

(６-１)　まだ試していないパターンがなければ一つ前の状態に戻して(５-１)へ行く

１１１　　　　　　　　　　　　　　　　←　＊＊＊２２２３４５５６６７７

(５-１)　まだ試していないパターンがあれば一つ前の状態に戻して(３-１)の続きからやる

左側の最後が１１１なので刻子まで試して対子の１１と順子の１２３は試していないので一つ前の状態に戻して(３-１)の続きからやる

　　　　　　　　　　　　　　　　　　　←　１１１２２２３４５５６６７７

(３-１)　刻子、対子、順子の順に元の並びの左端から左への移動を試していく

１１　　　　　　　　　　　　　　　　　←　＊＊１２２２３４５５６６７７

(３-２)　刻子、対子、順子の順に元の並びの左端から左への移動を試していく

１１　１２３　　　　　　　　　　　　　←　＊＊＊＊２２＊４５５６６７７

(４-２)　移動できなくなったとき一雀頭四面子できていれば完成してるので記録する

完成していないので何もしない

(５-２)　まだ試していないパターンがあれば一つ前の状態に戻して(３-２)の続きからやる

左側の最後が１２３なので順子まですべて試したので何もしない

(６-１)　まだ試していないパターンがなければ一つ前の状態に戻して(５-１)へ行く

１１　　　　　　　　　　　　　　　　　←　＊＊１２２２３４５５６６７７

(５-１)　まだ試していないパターンがあれば一つ前の状態に戻して(３-１)の続きからやる

左側の最後が１１なので対子まで試して順子の１２３は試していないので一つ前の状態に戻して(３-２)の続きからやる

　　　　　　　　　　　　　　　　　　　←　１１１２２２３４５５６６７７

(３-１)　刻子、対子、順子の順に元の並びの左端から左への移動を試していく

１２３　　　　　　　　　　　　　　　　←　＊１１＊２２＊４５５６６７７

(４-１)　移動できなくなったとき一雀頭四面子できていれば完成してるので記録する

完成していないので何もしない

(５-１)　まだ試していないパターンがあれば一つ前の状態に戻して(３-１)の続きからやる

左側の最後が１２３なので順子まですべて試したので何もしない

(６-０)　終了

結果：「１１１　２２　２３４　５６７　５６７」

```mermaid
---
title: 処理の流れ
---
stateDiagram-v2
    direction LR
    [*] --> (１)
    (１) --> (２)
    (２) --> (３)
    (３) --> (３)
    (３) --> (４)
    (４) --> (５)
    (５) --> (６)
    (５) --> (３)
    (６) --> (５)
    (６) --> [*]
```

右側の(-？)は探索の深さを表す
(３-？)から(３-？)に行くときに(３-？)の右側の？が増える
(５-？)から(６-？)に行くときに(６-？)の右側の？が減る
？が０になったら終了


## 上記のアルゴリズムのポイント

☆赤牌を普通の牌に変える
☆昇順にソートする

この二つはその方が簡単だからやっているだけです。一番小さい数字から始まる刻子や対子や順子を左に移動するという手順を守っていれば必須ではありません。


☆刻子、対子、順子の順に調べる

順番はどうでもいいですが、順番が決まっていることが重要です。さらに順に調べるということは状態を必要に応じて戻さなければなりません。
刻子を移動したときは、対子や順子を調べるより先に刻子を移動する前の状態に戻さないといけません。対子を移動したときは、順子を調べるより先に対子を移動する前の状態に戻さないといけません。


☆対子を左へ２個移動するのは禁止

雀頭は２つ必要ないので当然です。


☆元の並びの一番左の数字を必ず移動しなけれなならないということ

正確にいうと一番小さい数字です。ランダムな順で移動していると検索漏れが発生するので何らかの基準が必要です。


☆順子を左へ移動した直後に、同じ数字で始まる刻子や対子を左へ移動するのは禁止

重複して同じパターンを発見しないためです。

これがないと上記の例ですと

「１１１　２２　２３４　５６７　５６７」
「１１１　２３４　２２　５６７　５６７」

のように「２２」と「２３４」が入れ替わった２パターンができてしまいます。

ただし

☆元の並びの一番左の数字を必ず移動しなければならない

というルールなので

「２３４」と「５６７」が入れ替わったりする心配はありません。「５６７」を左に移動したときには右側には５以上の数字しかないので「５６７」の後に「２３４」がででくることは絶対にありません。あくまで先頭の数字が同じ部分の並びが入れ替わった複数のパターンがでてくる心配だけがあります。

全く同じ並びである「５６７」と「５６７」が入れ替わる心配も同じ理由でありません。一番左の「５」のからできた「５６７」と左から二番目の「５」のからできた「５６７」なら
前者が必ず左側にあるからです。

「２２２」と「２２」が入れ替わったりする心配もありません。それですと同じ牌が５つあることになって麻雀のルールに違反します。

したがって例えば２が先頭の部分に対しては、
- 「２２２　２３４」と「２３４　２２２」
- 「２２　２３４」と「２３４　２２」
- 「２２　２３４　２３４」と「２３４　２２　２３４」と「２３４　２３４　２２」

の３系統の重複だけ気にすればいいわけで「２３４」を左に移動した直後に「２２２」や「２２」を左に移動しないと決めておけば解決します。これを一般化すると

☆順子を左へ移動した直後に、同じ数字で始まる刻子や対子を左へ移動するのは禁止

となるわけです。

## サンプルコード

専門用語が多すぎて英語にすると読みにくくてやってられないので日本語識別子で書いています。

```c++:見本.h
#pragma once

#include <iostream>
#include <array>
#include <bitset>
#include <vector>
#include <algorithm>
#include <string>

struct 牌
{
    int 値{0};

    static constexpr int 赤牌 = 1 << 6;
    static constexpr int 番号部分 = (1 << 4) - 1;
    static constexpr int 萬子 = 0 << 4;
    static constexpr int 筒子 = 1 << 4;
    static constexpr int 索子 = 2 << 4;
    static constexpr int 字牌 = 3 << 4;
    static constexpr int 次の種類 = 1 << 4;

    constexpr int 種類を得る() const noexcept { return 値 & 字牌; }
    constexpr char 種類を文字で得る() const noexcept
    {
        switch (種類を得る()) {
        case 索子: return 's';
        case 筒子: return 'p';
        case 字牌: return 'z';
        default: return 'm';
        }
    }
    constexpr int 番号を得る() const noexcept { return 値 & 番号部分; }
    constexpr bool 赤牌である() const noexcept { return 値 & 赤牌; }
    constexpr bool 萬子である() const noexcept { return 種類を得る() == 萬子; }
    constexpr bool 筒子である() const noexcept { return 種類を得る() == 筒子; }
    constexpr bool 索子である() const noexcept { return 種類を得る() == 索子; }
    constexpr bool 字牌である() const noexcept { return 種類を得る() == 字牌; }
    constexpr bool 数牌である() const noexcept { return 種類を得る() != 字牌; }
    constexpr bool 風牌である() const noexcept { return 字牌である() && 番号を得る() <= 4; }
    constexpr bool 三元牌である() const noexcept { return 字牌である() && 番号を得る() >= 5; }
    constexpr bool operator ==(牌 牌2) const noexcept { return 値 == 牌2.値; }
    constexpr bool operator !=(牌 牌2) const noexcept { return 値 != 牌2.値; }
    constexpr bool operator <(牌 牌2) const noexcept
    {
        if ((~赤牌 & 値) < (~赤牌 & 牌2.値)) return true;
        if ((~赤牌 & 値) == (~赤牌 & 牌2.値) && 値 > 牌2.値) return true;
        return false;;
    }
    constexpr bool operator >(牌 牌2) const noexcept { return 牌2 < *this; }
    constexpr 牌 operator +(int 差分) const noexcept { return {値 + 差分}; }
    constexpr 牌 operator -(int 差分) const noexcept { return {値 - 差分}; }
};

constexpr 牌 萬子の(int 番号) { return {牌::萬子 | 番号}; }
constexpr 牌 筒子の(int 番号) { return {牌::筒子 | 番号}; }
constexpr 牌 索子の(int 番号) { return {牌::索子 | 番号}; }
constexpr 牌 字牌の(int 番号) { return {牌::字牌 | 番号}; }
constexpr 牌 赤牌の(牌 ある牌) { return {牌::赤牌 | ある牌.値}; }
constexpr 牌 非赤牌の(牌 ある牌) { return {~牌::赤牌 & ある牌.値}; }

constexpr 牌 無牌{0};
constexpr 牌 東{字牌の(1)}, 南{字牌の(2)}, 西{字牌の(3)}, 北{字牌の(4)};
constexpr 牌 白{字牌の(5)}, 發{字牌の(6)}, 中{字牌の(7)};

using 四牌 = std::array<牌, 4>;
using 十四牌 = std::array<牌, 14>;
using 十四ビット = std::bitset<14>;

constexpr size_t 未発見 = 14;

inline size_t 未使用牌を探す(const 十四牌& 手牌, const size_t 位置, const 十四ビット 使用中)
{
    size_t 未使用牌の位置 = 位置;
    while (未使用牌の位置 < 未発見 && 使用中.test(未使用牌の位置)) ++未使用牌の位置;
    return 未使用牌の位置;
}

inline size_t 牌を探す(const 十四牌& 手牌, const size_t 位置, const 十四ビット 使用中, const 牌 対象の牌)
{
    size_t 対象の位置 = 未使用牌を探す(手牌, 位置, 使用中);
    if (対象の位置 < 未発見) {
        if (手牌[対象の位置] == 対象の牌) return 対象の位置;
        if (手牌[対象の位置] > 対象の牌) return 未発見;
    }
    if (対象の位置 + 1 >= 未発見) return 未発見;
    return 牌を探す(手牌, 対象の位置 + 1, 使用中, 対象の牌);
}

struct 雀頭
{
    牌 代表牌{無牌};
};

inline std::ostream& operator <<(std::ostream& 出力, const 雀頭& 値)
{
    const 牌 代表牌 = 値.代表牌;
    const int 番号 = 代表牌.番号を得る();
    出力 << 番号 << 番号 << 代表牌.種類を文字で得る();

    return 出力;
}

struct 面子
{
    int 種類{順子};
    牌 代表牌{無牌};

    static constexpr int 順子 = 0;
    static constexpr int 刻子 = 1;
    static constexpr int 槓子 = 2;
};

constexpr 面子 順子の(牌 代表牌) { return {面子::順子, 代表牌}; }
constexpr 面子 刻子の(牌 代表牌) { return {面子::刻子, 代表牌}; }
constexpr 面子 槓子の(牌 代表牌) { return {面子::槓子, 代表牌}; }

inline std::ostream& operator <<(std::ostream& 出力, const 面子& 値)
{
    const int 種類 = 値.種類;
    const 牌 代表牌 = 値.代表牌;
    const int 番号 = 代表牌.番号を得る();
    if (種類 == 面子::順子) {
        出力 << 番号 << 番号 + 1 << 番号 + 2;
    } else {
        出力 << 番号 << 番号 << 番号;
        if (種類 == 面子::槓子) 出力 << 番号;
    }
    出力 << 代表牌.種類を文字で得る();

    return 出力;
}

struct 一雀頭四面子
{
    雀頭 一雀頭{無牌};
    std::array<面子, 4> 四面子{};
    size_t 面子の数{0};
    size_t 副露の数{0};
    size_t 暗槓の数{0};

    bool 雀頭が有る() const noexcept
    {
        return 一雀頭.代表牌 != 無牌;
    }

    bool 完成した() const noexcept
    {
        return 雀頭が有る() && 面子の数 >= 4;
    }

    bool 雀頭を設定する(const 十四牌& 手牌, const size_t 対象1, 十四ビット& 使用中) noexcept
    {
        if (雀頭が有る()) return false;

        const size_t 対象2 = 牌を探す(手牌, 対象1 + 1, 使用中, 手牌[対象1]);
        if (対象2 >= 未発見) return false;

        使用中.set(対象1);
        使用中.set(対象2);
        一雀頭 = {手牌[対象1]};

        return true;
    }

    bool 刻子を設定する(const 十四牌& 手牌, const size_t 対象1, 十四ビット& 使用中) noexcept
    {
        return 面子を設定する(手牌, 対象1, 使用中, 0);
    }

    bool 順子を設定する(const 十四牌& 手牌, const size_t 対象1, 十四ビット& 使用中) noexcept
    {
        return 面子を設定する(手牌, 対象1, 使用中, 1);
    }

private:
    bool 面子を設定する(const 十四牌& 手牌, const size_t 対象1, 十四ビット& 使用中, int 差分) noexcept
    {
        if (面子の数 >= 4) return false;

        const size_t 対象2 = 牌を探す(手牌, 対象1 + 1, 使用中, 手牌[対象1] + 差分);
        if (対象2 + 1 >= 未発見) return false;

        const size_t 対象3 = 牌を探す(手牌, 対象2 + 1, 使用中, 手牌[対象1] + 差分 * 2);
        if (対象3 >= 未発見) return false;

        auto& 新規の面子 = 四面子[面子の数];
        使用中.set(対象1);
        使用中.set(対象2);
        使用中.set(対象3);
        新規の面子 = {(差分 == 0) ? 面子::刻子 : 面子::順子, 手牌[対象1]};
        ++面子の数;

        return true;
    }
};

inline std::ostream& operator <<(std::ostream& 出力, const 一雀頭四面子& 値)
{
    if (値.雀頭が有る()) {
        出力 << 値.一雀頭;
    }
    for (size_t n = 0; n < 値.面子の数; ++n) {
        出力 << 値.四面子[n];
    }
    出力 << std::endl;

    return 出力;
}

inline void 一雀頭四面子に分解する(
    一雀頭四面子& 現状, std::vector<一雀頭四面子>& 分解後,
    const 十四牌& 手牌, const size_t 位置, 十四ビット 使用中, const 牌 直前の順子の先頭の牌)
{
    if (現状.完成した()) {
        分解後.push_back(現状);
        return;
    }

    一雀頭四面子 元の状況 = 現状;
    十四ビット 元の使用中 = 使用中;

    const auto 次の位置 = 未使用牌を探す(手牌, 位置, 使用中);
    if (次の位置 >= 未発見) return;

    bool 順子を分離済み = 手牌[次の位置] == 直前の順子の先頭の牌;
    if (!順子を分離済み && 現状.雀頭を設定する(手牌, 次の位置, 使用中)) {
        一雀頭四面子に分解する(現状, 分解後, 手牌, 次の位置 + 1, 使用中, 無牌);
        現状 = 元の状況;
        使用中 = 元の使用中;
    }
    if (!順子を分離済み && 現状.刻子を設定する(手牌, 次の位置, 使用中)) {
        一雀頭四面子に分解する(現状, 分解後, 手牌, 次の位置 + 1, 使用中, 無牌);
        現状 = 元の状況;
        使用中 = 元の使用中;
    }
    if (手牌[次の位置].数牌である() && 現状.順子を設定する(手牌, 次の位置, 使用中)) {
        一雀頭四面子に分解する(現状, 分解後, 手牌, 次の位置 + 1, 使用中, 手牌[次の位置]);
    }
}

inline void 一雀頭四面子に分解する(
    一雀頭四面子& 現状, std::vector<一雀頭四面子>& 分解後,
    const 十四牌& 手牌, const size_t 位置, 十四ビット 使用中)
{
    // 無理だとすぐに判断できる場合は分解を試さない
    int ブロックの長さ = 1;
    bool 対子有り = false;
    for (size_t n = 0; n < 13; ++n) {
        if (手牌[n + 1] == 手牌[n]) {
            対子有り = true;
            ++ブロックの長さ;
        } else if (手牌[n + 1] == 手牌[n] + 1) {
            ++ブロックの長さ;
        } else {
            // ブロックの長さを３で割って１余るならば
            // 雀頭と面子の組み合わせに分けることはできない
            if (ブロックの長さ % 3 == 1) return;
            // 次の牌は次のブロックの始まり
            ブロックの長さ = 1;
        }
    }
    // 対子が一つもなければ絶対に雀頭を作ることができない
    if (!対子有り) return;

    一雀頭四面子に分解する(現状, 分解後, 手牌, 位置, 使用中, 無牌);
}

inline std::vector<一雀頭四面子> 一雀頭四面子のリストを作成する(
    const 十四牌& 理牌済み赤無しの手牌, const std::vector<面子>& 副露面子, const size_t 暗槓の数 = 0)
{
    一雀頭四面子 現状{};
    std::vector<一雀頭四面子> 分解後{};
    size_t 位置{0};
    十四ビット 使用中 = {};

    for (size_t n = 0; n < 副露面子.size() && n < 4; ++n) {
        現状.四面子[n] = 副露面子[n];
    }
    現状.面子の数 = 副露面子.size();

    // 副露の数を記録しておかないと一盃口などの判定がしづらくなる
    現状.副露の数 = 現状.面子の数;
    // 副露の数と暗槓の数を記録しておかないと三暗刻や四暗刻などの判定がしづらくなる
    現状.暗槓の数 = 暗槓の数;

    一雀頭四面子に分解する(現状, 分解後, 理牌済み赤無しの手牌, 位置, 使用中);
    return 分解後;
}

// 読み込んだ牌の数を戻す
// mが萬子、pが筒子、sが筒子、zが字牌、0が赤牌、1～9は数牌ならそのまま、
// 字牌ならば1z/2z/3z/4z/5z/6z/7zがそれぞれ東/南/北/西/白/發/中を表現している
template <size_t N> inline size_t 読み込む(std::array<牌, N>& 牌のリスト, const std::string& 牌文字列)
{
    牌のリスト = {};
    size_t 位置{0};
    auto 追加する = [&牌のリスト, &位置](int 各牌) { 牌のリスト[位置] = {各牌}; ++位置; };

    int 種類 = 牌::萬子;
    auto 終端 = 牌文字列.crend();
    for (auto it = 牌文字列.crbegin(); it != 終端 && 位置 < N; ++it) {
        switch (*it) {
        case '0': if (種類 != 牌::字牌) 追加する(牌::赤牌 | 種類 | 5); break;
        case '1': 追加する(種類 | 1); break;
        case '2': 追加する(種類 | 2); break;
        case '3': 追加する(種類 | 3); break;
        case '4': 追加する(種類 | 4); break;
        case '5': 追加する(種類 | 5); break;
        case '6': 追加する(種類 | 6); break;
        case '7': 追加する(種類 | 7); break;
        case '8': if (種類 != 牌::字牌) 追加する(種類 | 8); break;
        case '9': if (種類 != 牌::字牌) 追加する(種類 | 9); break;
        case 'm': 種類 = 牌::萬子; break;
        case 'p': 種類 = 牌::筒子; break;
        case 's': 種類 = 牌::索子; break;
        case 'z': 種類 = 牌::字牌; break;
        default: break;
        }
    }

    std::reverse(牌のリスト.begin(), 牌のリスト.begin() + 位置);
    return 位置;
}

template <size_t N> inline size_t 赤を消して理牌する(
    std::array<牌, N>& 牌のリスト, const size_t 枚数 = N)
{
    size_t 赤ドラの数{0};
    for (size_t n = 0; n < 枚数; ++n) {
        auto& 各牌 = 牌のリスト[n];
        if (各牌.赤牌である()) {
            ++赤ドラの数;
            各牌 = 非赤牌の(各牌);
        }
    }
    std::sort(牌のリスト.begin(), 牌のリスト.begin() + 枚数, [](牌 左, 牌 右) { return 左.値 < 右.値; });

    return 赤ドラの数;
}

// このファイルのライセンス
/*
This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <http://unlicense.org/>
*/
```

以下に簡単なテストコードをあげておきます。

```c++:見本.cpp
#include "pch.h"

#include "見本.h"
#include <sstream>

static std::string 分割する(
    const std::string 牌姿, std::vector<std::string> 副露文字列 = {}, const size_t 暗槓の数 = 0)
{
    std::ostringstream 出力;
    出力 << "* " << 牌姿;
    for (const auto& 各文字列 : 副露文字列) {
        出力 << " / " << 各文字列;
    }

    十四牌 手牌{};

    const size_t 手牌の枚数 = 読み込む(手牌, 牌姿);
    size_t 赤ドラの数 = 赤を消して理牌する(手牌, 手牌の枚数);

    std::vector<面子> 副露面子;
    for (const auto& 各文字列 : 副露文字列) {
        四牌 各四牌{};
        const size_t 牌の枚数 = 読み込む(各四牌, 各文字列);
        赤ドラの数 += 赤を消して理牌する(各四牌, 牌の枚数);
        if (牌の枚数 == 3) {
            if (各四牌[0] != 各四牌[1]) {
                副露面子.push_back({面子::順子, 各四牌[0]});
            } else {
                副露面子.push_back({面子::刻子, 各四牌[0]});
            }
        } else if (牌の枚数 == 4) {
            副露面子.push_back({面子::槓子, 各四牌[0]});
        }
    }

    if (赤ドラの数 > 0) 出力 << " 赤" << 赤ドラの数;
    出力 << std::endl;

    std::vector<一雀頭四面子> リスト = 一雀頭四面子のリストを作成する(手牌, 副露面子, 暗槓の数);
    for (const auto& 各要素 : リスト) {
        出力 << 各要素;
    }
    出力 << std::endl;
    return 出力.str();
}

#if defined(_WIN32) && defined(__clang__)
#include "Windows.h"
inline void コンソールの文字コードを変更する() { SetConsoleOutputCP(CP_UTF8); }
#else
inline void コンソールの文字コードを変更する() {}
#endif

int main()
{
    コンソールの文字コードを変更する();

    std::cout
    << 分割する("12345671206712m")
    << 分割する("11123456678999s")
    << 分割する("11123455678999s")
    << 分割する("11123450999s", {"678m"})
    << 分割する("55554444776666m")
    << 分割する("33445566778899p")
    << 分割する("789s05566p", {"340s", "406m"})
    << 分割する("55544433322211m")
    << 分割する("55544433322211z");

    return 0;
}
```