麻将向听数的计算与编程实现
九条可怜酱
2022年12月11日 02:13

首先说明一下本文采用的牌表示方式:

万:一二三四五六七八九

索:123456789

饼:①②③④⑤⑥⑦⑧⑨

字:东南西北白发中

向听数表示至少需要替换多少张牌才能进入听牌状态

特别的,本文中的向听数=-1表示和牌

为了计算向听数,我们首先需要这么几个参数:

G3:面子(顺子或刻子)的总数。-- group 3

G2:搭子(差一张牌就能变成面子)的总数(不能是脏搭子)。-- group 2

DG2:脏搭子(成搭子的两张牌都处于爆满状态)的数量。-- dirty group 2

P:雀头(两张一样的牌)的数量,只能是0或1-- pair

DN:脏数牌(孤立的数牌,并处于爆满状态)的数量。-- dirty number

DZ:脏字牌(孤立的字牌,并处于爆满状态的数量。-- dirty kanji

R:剩余的牌的数量。-- remaining

N:手牌的数量,取值为2、5、8、11、14,且N=3*G3+2*(G2+DG2+P)+DN+DZ+R。

K:K=(N-2)/3,取值为0、1、2、3、4。

爆满状态:

例如11119,可以拆成[111, 1, 9],由于不存在第5张1,就称1处于爆满状态,那个单张1就叫做脏数牌同理11112222,可以拆成[111, 222, 12],对于12这个搭子,12都处于爆满状态,所以称12是脏搭子

关于雀头:

对于两张相同的牌,应优先把它当成雀头,而不是搭子。

关于脏搭子:

脏搭子只能是顺子搭子,而不能是刻子搭子因为如果在遍历的过程中发现了能成为刻子的脏搭子,那么应该将它归类为2个脏牌(脏数牌或脏字牌),毕竟它已经不可能成为刻子了。

关于脏数牌和脏字牌:

严格来说这不是字面意思,数牌也可能成为脏字牌。例如11119,且自己杠了2,手牌可以拆成[111, 1, 9],那么此时这个单张1应该认为是脏字牌,因为它不可能成为顺子了。

下面来开始计算向听数。

  1. 令s=-1。

  2. 首先我们要确保G3 + G2 + DG2 ≤ K,如果不满足这个条件,那么进行以下步骤:

    1. 优先转移G2到R,G2每减1那么R加2,直到满足条件或G2=0终止循环。

    2. 若条件依然不满足,转移DG2到DN,DG2每减1那么DN加2,直到满足条件终止循环。

  1. 将所有的DG2转移到G2,即G2 += DG2,DG2 = 0

  2. 如果P=0,那么我们要凑一个雀头出来,进行如下步骤:

    1. 我们的目标是尽量用最少的步数凑出雀头来,同时希望尽量消耗脏牌,因为后面脏牌的存在会导致凑面子需要花费很多步数。

    2. 如果R>0:

      1. 如果DZ>0,那么DZ -= 1,R -= 1,s += 1,跳转到5

      2. 如果DN>0,那么DN -= 1,R -= 1,s += 1,跳转到5

      3. R -= 2,s += 1,跳转到5

  1. 如果DZ>0(此时说明R=0):

    1. 如果DZ≥2,那么DZ -= 2,s += 2,跳转到5

    2. DZ -= 1,DN -= 1,s += 1,跳转到5

  1. DN -= 2,跳转到5。(可以保证这里DN≥2)

  1. R += DN,DZ -= G2 + 2 * R。

  2. 如果DZ>0,那么s += DZ / 3。

  3. 返回2 * (K - G3) - G2 + s,所返回的值即向听数


下面将用C#代码实现这一算法。

代码块
C#
自动换行
复制代码
static int Syanten(int G3, int G2, int DG2, int P, int DN, int DZ, int N = 14) {
    int s = -1;
    int R = N - 3 * G3 - 2 * (G2 + DG2 + P) - DN - DZ;
    int K = N / 3;

    if (G3 + G2 + DG2 > K) {
        int t = K - G3 - DG2;
        if (t <= 0) {
            R += 2 * G2;
            DN -= 2 * t;
        } else {
            R += 2 * t;
        }
        G2 = K - G3;
    } else {
        G2 += DG2;
    }

    if (P == 0) {
        if (R > 0) {
            if (DZ > 0) {
                R--;
                DZ--;
            } else if (DN > 0) {
                R--;
                DN--;
            } else {
                R -= 2;
            }
            s++;
        } else {
            if (DZ > 0) {
                if (DZ >= 2) {
                    DZ -= 2;
                } else {
                    DZ--;
                    DN--;
                }
            } else {
                DN -= 2;
            }
            s += 2;
        }
    }

    R += DN;
    DZ -= G2 + 2 * R;
    if (DZ > 0) {
        s += DZ / 3;
    }

    return 2 * (K - G3) - G2 + s;
}
复制成功

测试代码:

代码块
C#
自动换行
复制代码
// 125689一二五六八九①②        125689s125689m12p
Assert(Syanten(G3: 0, G2: 7, DG2: 0, P: 0, DN: 0, DZ: 0) == 4);

// 114477一一四四七七①①        114477s114477m11p
Assert(Syanten(G3: 0, G2: 6, DG2: 0, P: 1, DN: 0, DZ: 0) == 3);

// 11114477一一四四七七         11114477s114477m
Assert(Syanten(G3: 1, G2: 4, DG2: 0, P: 1, DN: 1, DZ: 0) == 2);

// 11112222一一四四七七         11112222s114477m
Assert(Syanten(G3: 2, G2: 2, DG2: 1, P: 1, DN: 0, DZ: 0) == 1);

// 11112222一一一一四四         11112222s111144m
Assert(Syanten(G3: 3, G2: 0, DG2: 1, P: 1, DN: 1, DZ: 0) == 0);

// 11112222一一一一四五         11112222s111145m
Assert(Syanten(G3: 3, G2: 1, DG2: 1, P: 0, DN: 1, DZ: 0) == 1);

// 11114444一一一一四五         11114444s111145m
Assert(Syanten(G3: 3, G2: 1, DG2: 0, P: 0, DN: 3, DZ: 0) == 2);

// 11114589一二五六八九         11114589s125689m
Assert(Syanten(G3: 1, G2: 5, DG2: 0, P: 0, DN: 1, DZ: 0) == 3);

// 11114444一二五六八九         11114444s125689m
Assert(Syanten(G3: 2, G2: 3, DG2: 0, P: 0, DN: 2, DZ: 0) == 2);

// 11112222一二五六八九         11112222s125689m
Assert(Syanten(G3: 2, G2: 3, DG2: 1, P: 0, DN: 0, DZ: 0) == 2);

// 11114444一四七①④⑦         11114444s147m147p
Assert(Syanten(G3: 2, G2: 0, DG2: 0, P: 0, DN: 2, DZ: 0) == 4);

// 东东东东南南南南西西西西北北     11112222333344z
Assert(Syanten(G3: 3, G2: 0, DG2: 0, P: 1, DN: 0, DZ: 3) == 2);

// 东东东东南南南南西西西西北白     11112222333345z 
Assert(Syanten(G3: 3, G2: 0, DG2: 0, P: 0, DN: 0, DZ: 3) == 2);

// 一一一一东东东东南南南南西西     1111m1111222233z
Assert(Syanten(G3: 3, G2: 0, DG2: 0, P: 1, DN: 1, DZ: 2) == 1);

// 一一一一东东东东南南南南西北     1111m1111222234z
Assert(Syanten(G3: 3, G2: 0, DG2: 0, P: 0, DN: 1, DZ: 2) == 2);

// 111222333444东南            111222333444s12z
Assert(Syanten(G3: 4, G2: 0, DG2: 0, P: 0, DN: 0, DZ: 0) == 0);

// 111222333444①②            111222333444s12p
Assert(Syanten(G3: 4, G2: 1, DG2: 0, P: 0, DN: 0, DZ: 0) == 0);

// 111222333444①①            111222333444s11p
Assert(Syanten(G3: 4, G2: 0, DG2: 0, P: 1, DN: 0, DZ: 0) == -1);

// 二五八②⑤⑧258东南西北白      258m258p258s12345z
Assert(Syanten(G3: 0, G2: 0, DG2: 0, P: 0, DN: 0, DZ: 0) == 8);

// 11122                      11122s
Assert(Syanten(G3: 1, G2: 0, DG2: 0, P: 1, DN: 0, DZ: 0, N: 5) == -1);

// 11112                      11112s
Assert(Syanten(G3: 1, G2: 1, DG2: 0, P: 0, DN: 0, DZ: 0, N: 5) == 0);

// 11114                      11114s
Assert(Syanten(G3: 1, G2: 0, DG2: 0, P: 0, DN: 1, DZ: 0, N: 5) == 0);

// 12569                      12569s
Assert(Syanten(G3: 0, G2: 2, DG2: 0, P: 0, DN: 0, DZ: 0, N: 5) == 1);

// 11东南西                    11s123z
Assert(Syanten(G3: 0, G2: 0, DG2: 0, P: 1, DN: 0, DZ: 0, N: 5) == 1);

// 东南西北白                   12345z
Assert(Syanten(G3: 0, G2: 0, DG2: 0, P: 0, DN: 0, DZ: 0, N: 5) == 2);

// 11                         11s
Assert(Syanten(G3: 0, G2: 0, DG2: 0, P: 1, DN: 0, DZ: 0, N: 2) == -1);

// 12                         12s
Assert(Syanten(G3: 0, G2: 1, DG2: 0, P: 0, DN: 0, DZ: 0, N: 2) == 0);

// 14                         14s
Assert(Syanten(G3: 0, G2: 0, DG2: 0, P: 0, DN: 0, DZ: 0, N: 2) == 0);


static void Assert(bool value, [System.Runtime.CompilerServices.CallerArgumentExpression(nameof(value))] string? expr = null) {
    ConsoleColor oldColor = Console.ForegroundColor;
    Console.ForegroundColor = value ? ConsoleColor.Green : ConsoleColor.Red;
    Console.WriteLine($"{(value ? "√" : "×")} {expr}");
    Console.ForegroundColor = oldColor;
}
复制成功

代码输出: