在知乎上看到一个问题,如何用程序判断麻将牌是否和牌。和牌的规则为:14张麻将牌当中,必须要有一个对子,即两张相同的牌,除去对子后,剩下的牌可以组合成”A, A, A”或者”A, A+1, A+2”两种模式的组合,其中A为某种花色的某张牌。
我的解法是:
首先定义一下麻将牌在程序中的表示方式:
把万,饼,条分别用连续的数字代表,比如:一万到九万用1到9表示,一饼到九饼用11到19表示,一条到九条用21到29表示。重要的是每一种牌内部数字是连续的,不同牌之间的数字不连续。对于风和中发白,每一种都用互相之间不连续的数字表示,比如中是31,发是33,白是35等等。
由于和牌中必需要有一个对子,所以我们的思路是,去除可能的对子,然后判断剩下的牌是否可以表示成“A A A”或者“A A+1 A+2 ”这两种模式的组合。
具体流程是:对输入的14张麻将牌序列进行排序,排序后,由低到高依次寻找对子,如果队列中没有对子,那么肯定不是和牌,程序结束。找到对子后,将这两张对子从序列中删除,判断其余的牌是否满足下面的条件,如果满足,则为和牌,结束,如果不满足,则在排序后的输入队列中寻找下一个对子,重复本过程,直到条件1满足或者搜索到序列结尾。
条件1:序列中的元素可以分为每3个元素一组,每组元素均为“A A A”或者“A A+1 A+2 ”的形式。
为了判断一个序列是否满足条件1,我们观察到以下几个性质:
性质1:排序后的麻将牌序列可以分割成若干子序列,其中每个子序列内部的元素是连续的,且不同序列间的元素不连续(连续指相邻元素的值相同或值相差1)。
性质2:排序后的麻将牌序列满足条件1的充分必要条件是,按性质1分割的每个子序列均满足条件1。
性质3:如果一个序列满足条件1,则按性质1分割后的子序列元素个数必定为3的整数倍。
于是,我们下一步要做的事情就是,将排序后的序列按照连续性分组,然后递归判断每组是否满足条件1,如果所有组都满足条件1,则分组前序列也满足条件1,否则不满足。
为了判断一个连续序列是否满足条件1,可以分几种情况考虑:
1. 如果序列中有3个元素,则直接判断是否属于条件1中的两种情况即可;
否则,设A为序列中的最小值,
2. 如果A的个数大于等于3(即3或4),则序列满足条件1的充分必要条件是序列去掉3个A后的剩余元素满足条件1;
3. 如果A的个数小于3(即1或2),则序列满足条件1的充分必要条件是序列去掉(A, A+1, A+2)后的剩余元素满足条件1;
时间复杂度:
由于牌的个数是常数14,且每轮迭代数据规模都会减小,因此,该算法时间复杂度为O(1)。
代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
static vector<int> result;
//for debug
void prtvct(const vector<int> &v, bool nxtline = true) {
for (auto it = v.begin(); it != v.end(); ++it) {
cout << *it << " ";
}
if (nxtline)
cout << endl;
}
bool erase_by_val(vector<int> &in, int val) {
for (auto it = in.begin(); it != in.end(); ++it) {
if (*it == val) {
in.erase(it);
return true;
}
}
return false;
}
void push_result(int a, int b, int c) {
result.push_back(a);
result.push_back(b);
result.push_back(c);
}
bool shrink(vector<int> &in) {
if (in[0] == in[1] && in[1] == in[2]) {
push_result(in[0], in[1], in[2]);
auto it = in.erase(in.begin());
it = in.erase(it);
in.erase(it);
return true;
} else {
if (in[in.size()-1] - in[0] < 2) {
return false;
}
}
int i, t;
for (i = 0, t = in[0]; i < 3; ++i, ++t) {
erase_by_val(in, t);
}
push_result(t-3, t-2, t-1);
return true;
}
void group(vector<int> &input, vector<vector<int> > &grp) {
vector<int> member;
if (input.begin() != input.end()) {
member.push_back(input[0]);
} else {
return;
}
for (auto it = input.begin()+1; it != input.end(); ++it) {
if (*it == *(it-1) || *it == *(it-1)+1) {
member.push_back(*it);
continue;
}
grp.push_back(member);
member.clear();
member.push_back(*it);
}
grp.push_back(member);
}
bool isLegal(vector<int> &input) {
vector<vector<int> > grp;
if (input.size() % 3 != 0) {
return false;
}
if (input.size() == 3) {
if (input[0] == input[1] && input[1] == input[2]) {
push_result(input[0], input[1], input[2]);
return true;
}
if (input[1] == input[0] + 1 && input[2] == input[1] + 1) {
push_result(input[0], input[1], input[2]);
return true;
}
return false;
}
group(input, grp);
// for each group
for (auto it = grp.begin(); it != grp.end(); ++it) {
if (shrink(*it) == false) {
return false;
}
if (isLegal(*it) == false) {
return false;
}
}
return true;
}
bool isHu(vector<int> &input) {
sort(input.begin(), input.end());
int i = 0;
while (i < input.size()-1) {
result.clear();
auto incopy = input;
if (input[i] == input[i+1]) {
result.push_back(input[i]);
result.push_back(input[i+1]);
auto it = incopy.erase(incopy.begin() + i);
incopy.erase(it);
// process
if (isLegal(incopy) == true) {
return true;
}
}
do { ++i; } while (i < input.size()-1 && input[i] == input[i-1]);
}
return false;
}
int main()
{
int n = 0;
vector<int> input;
cin >> n;
for (int i = 0; i < n; ++i) {
int a;
input.clear();
for (int j = 0; j < 14; ++j) {
cin >> a;
input.push_back(a);
}
if (isHu(input)) {
cout << "Hu: ";
prtvct(result);
} else {
cout << "Not Hu" << endl;
}
}
return 0;
}