冲击麻将是浙江省慈溪、余姚地区特有的麻将玩法。一共有136张牌,分为万牌,筒/饼牌,条/索牌,字牌/风头牌,不包括花牌。
胡法可以分为以下三类:
另外,游戏中还加入了“龙/财神/百搭”牌。即该种牌可以代替任何一种牌。
接下来,分别实现每种胡法的算法。用0—8表示一万—九万,9—17表示一筒—九筒,18—26表示一条—九条,27—33表示东南西北中发白。用一个数组表示手牌,dragon变量表示龙。
首先统计每张牌的个数,如果有大于1说明有对子,不能胡。然后看万筒条中有没有差小于3的即可。
function isAgari(paiArr, dragon) {
let cardsCount = [];
for (let i = 0; i < 34; ++i)
cardsCount.push(0);
for (let i = 0; i < paiArr.length; ++i)
if (paiArr[i] != dragon) // 不必统计龙
cardsCount[paiArr[i]]++;
let pai = []; // 给牌排好序
for (let i = 0; i < 34; ++i)
for (let j = 0; j < cardsCount[i]; ++j)
pai.push(i);
for (let i = 1; i < pai.length; ++i) { // 有相邻或间隔为1的万筒条,不能胡
if (Math.abs(pai[i] - pai[i - 1]) <= 2 &&
Math.floor(pai[i] / 9) == Math.floor(pai[i - 1] / 9) &&
pai[i] < 27)
return false;
}
return true;
}
这个更简单,只需要统计对子个数就好了,龙特殊处理一下就好了。
function isAgari(paiArr, dragon) {
let cardsCount = [], longCount = 0;
for (let i = 0; i < 34; ++i)
cardsCount.push(0);
for (let i = 0; i < paiArr.length; ++i)
if (paiArr[i] != dragon) // 不必统计龙
cardsCount[paiArr[i]]++;
else
longCount++;
let pairCount = 0, singleCount = 0; // 分别计算对子数和但张牌个数
for (let i = 0; i < cardsCount.length; ++i) {
pairCount += Math.floor(cardsCount[i] / 2);
singleCount += cardsCount[i] % 2;
}
if (singleCount <= longCount) // 龙把单个牌配成对后还有的多,两张龙也能成对
pairCount += singleCount + Math.floor((longCount - singleCount) / 2);
else // 把所有龙都把单张牌配对。
pairCount += longCount;
if (pairCount >= 7)
return true
return false;
}
如果没有龙,那么这个将是非常简单的。我们只要枚举对子,然后一次判断剩下牌是否为刻字与顺子的组合,只需一个dfs就可以了。但是,有龙的存在,如果我们枚举龙所对应的牌,效率将会非常低下。
因此,我们考虑换个思路。首先,万、筒、条、字牌是分别独立的。我们只需要分别考虑每一块牌是否可胡就行了。这样就分解成了4个子问题。并且这4个子问题规模不大,而且都有重复性,可以暴力枚举。为了优化时间效率,我们还可以把所有可以胡的牌型处理出来,然后只需要枚举龙的分配和对子的位置即可。
比如对于牌:[0,0,0,1,2,5,5,5,10,11,12,27,27,27]
。那么我们先拆成 [0,0,0,1,2,5,5,5]
、[10,11,12]
、[]
、[27,27,27]
四部分。然后分别判断每个部分是否可胡。不难发现,对子在第一部分的时候就是都可胡的。我们只需要枚举每部分分别在有无对子、有 $n$ 条龙的情况下是否可胡即可,而这数据量不大,完全可以先爆搜预处理,然后查表。
#include <cstdio>
#include <cstdlib>
using namespace std;
int cnt[10];
int q[10];
// Array(8),有对子/无对子+龙0123
inline bool check(int z)
{
if (z >= 10)
return true;
if (q[z] > 0)
{
bool flag;
if (z + 2 <= 9 && q[z + 1] > 0 && q[z + 2] > 0) // 可以抽顺子,枚举抽顺子
{
q[z] -= 1;
q[z + 1] -= 1;
q[z + 2] -= 1;
flag = check(z);
q[z] += 1;
q[z + 1] += 1;
q[z + 2] += 1;
if (flag)
return true;
}
if (q[z] >= 3) // 可以抽刻字,枚举抽刻字
{
q[z] -= 3;
flag = check(z);
q[z] += 3;
if (flag)
return true;
}
return false;
}
else
return check(z + 1);
}
inline bool check_dz(bool dz, int sum)
{
if (dz)
{
for (int i = 1; i <= 9; ++i) // 枚举哪张牌作为对子
{
if (q[i] < 2)
continue;
q[i] -= 2;
bool flag = check(1);
q[i] += 2;
if (flag)
return true;
}
}
else
{
if (check(1))
return true;
}
return false;
}
inline bool check_dragon(bool dz, int dragon, int sum)
{
if (dragon + sum > 14)
return false;
if (dragon > 0)
{
for (int i = 1; i <= 9; ++i) // 枚举龙所对应的牌
if (q[i] < 4)
{
q[i] += 1;
bool flag = check_dragon(dz, dragon - 1, sum);
q[i] -= 1;
if (flag)
return true;
}
return false;
}
else
{
return check_dz(dz, sum);
}
}
inline void dfs(int u, int sum) // 先枚举出所有牌型。用9个数字表示9种牌的个数。
{
if (u > 9)
{
if (sum)
putchar(',');
putchar('"');
for (int i = 1; i <= 9; ++i)
putchar('0' + cnt[i]);
putchar('"');
putchar(':');
putchar('[');
for (int j = 0; j < 8; ++j) // j=0~3表示有对子,j条龙;4~7表示无对子,j-5条龙.
{
for (int i = 1; i <= 9; ++i)
q[i] = cnt[i];
if (check_dragon(j <= 3, j % 4, sum))
printf("true");
else
printf("false");
if (j < 7)
putchar(',');
}
putchar(']');
// exit(0);
return;
}
for (int i = 0; i <= 4; ++i)
{
if (sum + i <= 14)
{
cnt[u] = i;
dfs(u + 1, sum + i);
}
}
}
int main()
{
freopen("data.js", "w", stdout); // 输出为js文件,方便js调用。
putchar('h');
putchar('=');
putchar('{');
dfs(1, 0);
putchar('}');
putchar('\n');
printf("module.exports={h}");
return 0;
}
上面是数字牌的情况,字牌也是类似的。不过我这里为了方便,字牌用了另一种记录方法,记录有4张、3张、2张、1张、0张的字牌分别由几种。因为字牌不能组成顺子,所以只需要判断刻字和对子即可。
inline void dfs(int u, int sum, int sum2)
{
if (u > 4)
{
if (tot)
putchar(',');
tot++;
putchar('"');
for (int i = 0; i <= 4; ++i)
putchar('0' + cnt[i]);
putchar('"');
putchar(':');
putchar('[');
for (int i = 0; i <= 1; ++i)
{
for (int j = 0; j < 4; ++j)
{
if (i + j)
putchar(',');
if (cnt[4] > 0)
printf("false");
else
{
int need = 2 * cnt[1] + 1 * cnt[2];
if (i == 0)
{
need -= 1;
if (need < 0)
need += 3;
}
if (need == j || need + 3 == j)
printf("true");
else
printf("false");
}
}
}
putchar(']');
}
else
{
for (int i = 0; i <= 7; ++i)
if (sum + i * u <= 14 && sum2 + i <= 7)
{
cnt[u] = i;
dfs(u + 1, sum + i * u, sum2 + i);
}
}
}
有了上面两个文件生成的数据,然后就可以判断了。
const distributeLong = [[[0, 0, 0, 0]],
[[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]],
[[2, 0, 0, 0], [0, 2, 0, 0], [0, 0, 2, 0], [0, 0, 0, 2], [1, 1, 0, 0], [1, 0, 1, 0], [1, 0, 0, 1], [0, 1, 1, 0], [0, 1, 0, 1], [0, 0, 1, 1]],
[[3, 0, 0, 0], [0, 3, 0, 0], [0, 0, 3, 0], [0, 0, 0, 3], [1, 2, 0, 0], [1, 0, 2, 0], [1, 0, 0, 2], [2, 1, 0, 0], [0, 1, 2, 0], [0, 1, 0, 2], [2, 0, 1, 0], [0, 2, 1, 0], [0, 0, 1, 2], [2, 0, 0, 1], [0, 2, 0, 1], [0, 0, 2, 1], [1, 1, 1, 0], [1, 1, 0, 1], [1, 0, 1, 1], [0, 1, 1, 1]]]
function isAgari(paiArr, dragon) {
let cardsCount = [], longCount = 0;
for (let i = 0; i < 34; ++i)
cardsCount.push(0);
for (let i = 0; i < paiArr.length; ++i)
if (paiArr[i] != dragon) // 不必统计龙
cardsCount[paiArr[i]]++;
else
longCount++;
let paiStr = ["", "", "", ""] // 计算四部分的牌型
let fengCount = [0, 0, 0, 0, 0]
for (let i = 0; i < 3; ++i)
for (let j = 0; j < 9; ++j)
paiStr[i] += cardsCount[i * 9 + j]
for (let i = 27; i < 34; ++i) {
fengCount[cardsCount[i]]++
}
for (let i = 0; i <= 4; ++i)
paiStr[3] += fengCount[i]
for (let i = 0; i < 4; ++i) {
for (let j in distributeLong[longCount]) { // 枚举4个部分龙的个数
flag = true;
try {
for (let k = 0; k < 4; ++k) { // k枚举对子的位置
if (k <= 2 && !h[paiStr[k]][(i != k) * 4 + distributeLong[longCount][j][k]]) {
flag = false;
break;
}
if (k == 3 && !p[paiStr[k]][(i != k) * 4 + distributeLong[longCount][j][k]]) {
flag = false;
break;
}
}
}
catch (e) {
flag = false
}
if (flag)
return true;
}
}
return false;
}