首先说明一下本文采用的牌表示方式:
万:一二三四五六七八九
索: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应该认为是脏字牌,因为它不可能成为顺子了。
下面来开始计算向听数。
令s=-1。
首先我们要确保G3 + G2 + DG2 ≤ K,如果不满足这个条件,那么进行以下步骤:
优先转移G2到R,G2每减1那么R加2,直到满足条件或G2=0终止循环。
若条件依然不满足,转移DG2到DN,DG2每减1那么DN加2,直到满足条件终止循环。
将所有的DG2转移到G2,即G2 += DG2,DG2 = 0。
如果P=0,那么我们要凑一个雀头出来,进行如下步骤:
我们的目标是尽量用最少的步数凑出雀头来,同时希望尽量消耗脏牌,因为后面脏牌的存在会导致凑面子需要花费很多步数。
如果R>0:
如果DZ>0,那么DZ -= 1,R -= 1,s += 1,跳转到5。
如果DN>0,那么DN -= 1,R -= 1,s += 1,跳转到5。
R -= 2,s += 1,跳转到5。
如果DZ>0(此时说明R=0):
如果DZ≥2,那么DZ -= 2,s += 2,跳转到5。
DZ -= 1,DN -= 1,s += 1,跳转到5。
DN -= 2,跳转到5。(可以保证这里DN≥2)
R += DN,DZ -= G2 + 2 * R。
如果DZ>0,那么s += DZ / 3。
返回2 * (K - G3) - G2 + s,所返回的值即向听数。
下面将用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;
} 测试代码:
// 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;
} 代码输出:
