游戏: 麻将判听
序言:
在北京读研已经有三个月了,第一次离家这么久,不想家是不可能的,尤其想念在家打麻将的欢乐时光。昨天晚上突然连续梦到跟好友在打麻将,心里实在有些痒飕飕,早上起来就特别想写个关于麻将的东西。
其实,今年暑期在家休息的时候就跟一个好友试着写过关于麻将判听的算法,当时完全属于瞎搞搞,各种百度,各种看不懂,然后又陷入深思。不过两个人大水比居然也在一两天里研究出了一个还算能用的递归算法,两个人当时也自豪了许久。
不过,毕竟当时没有基础,现在回想起来很多东西写的真心有些烂。趁着今天心血来潮,便再写一遍吧。
问题描述 :
方便起见,仅考虑清一色的情况(其实只要写出一种颜色,其他的都是依葫芦画瓢)。以“万”牌为例,共有九种牌,每种牌有四张。
考虑一副给定的牌型,判断该牌型是否已听牌。若听牌,则输出所听的牌,反之则输出未听牌。
注意:给定的是当前手牌,即牌数仅可能是1、4、7、10、13。
解题思路 :
首先我们要考虑对于牌型的存储方式。若采用枚举存储法,显然不利于我们进行增删改减。而计数法则是个不错的选择:不妨定义一个数组 Mahjong[10],其中 Mahjong[i] 表示 i “万”的个数,而Mahjong[0]可以缺省不用。
对于给定的牌型,我们难以将是否听牌的判断思路直接传达给计算机。不妨换个思路:遍历 1-9 “万”,尝试将之加入到给定的牌型中,然后判断是否能胡牌,若能够胡牌,则加入的这张牌便是其中一张听的牌。
一副已经胡的牌(只考虑手牌),其必然满足
n
∗
A
A
A
+
m
∗
A
B
C
+
D
D
=
3
k
+
1
(
n
,
m
,
k
∈
Z
且
0
≤
k
≤
4
)
n*AAA+m*ABC+DD= 3k+1 (n,m,k∈Z 且 0 ≤ k ≤ 4)
n∗AAA+m∗ABC+DD=3k+1(n,m,k∈Z且0≤k≤4)
其中, AAA表示刻子,ABC表示顺子,DD表示将牌。
显然,将牌是最为特殊的,其有且仅有一对。而刻子和顺子都可以有或者没有。一个直观的思路便是:先将将牌删掉,然后试着用递归的方法删刻子和顺子,若最终能够全部删干净,说明那张加入的牌是所听的牌。
基本思路就是这样,但是存在一个严峻的问题是,每次加入牌或是删除牌都是一种尝试的行为,不递归至末尾我们并不知道其是否奏效,因此我们必须在每次尝试失败之后将牌型还原至增删之前,即所谓“回溯法”。
C++代码:
#include <iostream>
#include <assert.h>
using namespace std;
int Mahjong[10];//计数存储数组
bool isting;//判听标志
bool ishu;//判胡标志
bool empty()//判断是否删空
{
int sum = 0;
for(int i = 1; i < 10; ++i)
sum += Mahjong[i];
return !sum;
}
void backtrack()
{
if(empty())
{
ishu = true;
return;
}
else
{
int i = 1;
while(!Mahjong[i]) ++i;
if(Mahjong[i+1] && Mahjong[i+2] && i <=7)
{
-- Mahjong[i];
-- Mahjong[i+1];
-- Mahjong[i+2];
backtrack();
++ Mahjong[i];
++ Mahjong[i+1];
++ Mahjong[i+2];
if(ishu) return;
}
if(Mahjong[i] >= 3)
{
Mahjong[i] -= 3;
backtrack();
Mahjong[i] += 3;
if(ishu) return;
}
}
}
int main()
{
int T;//测试次数
cin>>T;
while(T--)
{
memset(Mahjong, 0, sizeof(Mahjong));
int n;//输入牌数
cin>>n;
assert(n % 3 == 1 && n <=13);//假定牌数合理
for(int i = 0; i < n; ++i)
{
int curr;
cin>>curr;
assert(curr >= 1 && curr <=9);//假定牌张合理
++ Mahjong[curr];
assert(Mahjong[curr] <= 4);//每种牌不得超过4张
}
isting = false;
for(int i = 1; i <= 9; ++i)
{//遍历1-9,试着加入1张牌
++ Mahjong[i];
ishu = false;//置“胡”标志为否
for(int j = 1; j <= 9; ++j)
{//遍历1-9,试着取大于2张的做将
if(Mahjong[j] >= 2)
{
Mahjong[j] -= 2;//去麻将
backtrack();//入递归
Mahjong[j] += 2;//还原
if(ishu)
{
isting = true;
cout<<i<<" ";
if(Mahjong[i] == 5)
cout<<"(舍) ";
break;
}
}
}
-- Mahjong[i];//还原
}
cout<<endl;
if(!isting)
cout<<"未听..."<<endl;
}
}
测例:
3
7
1 2 3 4 5 6 7 //两头钓
4
1 1 2 2 //两面倒
13
1 1 1 2 3 4 5 6 7 8 9 9 9 //九宝莲灯
输出:
1 4 7
1 2
1 2 3 4 5 6 7 8 9
结语:
关于麻将判听的小算法就记录到这里。最近导师开始施加压力了,滚回去学苦逼的软件仿真了~