判断一副麻将牌能否胡牌

问题描述

        如何判断一副麻将牌能否胡牌

问题分析

        为了简便,假定这副牌中只有万条筒三种好花色,且1条为“万能牌”。牌的数量为 17 张并已按万条筒且从小到大的顺序排好。当这副牌能够被拆成 1 个对子、若干个碰(三个一样的)以及若干个吃(三个花色连续)时,就说这副牌能胡。

        要能胡需要同时满足两个条件,1 :找到“将牌”;2 :在当前将牌下,剩余的牌要么形成碰,要么形成吃,不能落单。

问题抽象

        使用 10 进制 2 位数表示麻将的一张牌,个位数的值(1-9)表示1到9(万条筒),十位数的值表示花色,0:万,1:条,2:筒。例如,1 : 1 万,21 : 1 筒,17 :7条。

        令 牌 除以 10 的值为花色,牌 mod 10 的值为牌值,假定牌已按万条筒且从小到大的方式排列。

算法1描述

        1. 从这副牌中取出所有万能牌,放在一边

        2. 从剩下的普通牌中,从第 1 张牌开始,直至最后 1 张牌,找 “将牌” ;如果这张牌有 2 张及以上,直接去掉这张牌的 2 张,进行步骤 3;如果这张牌只有 1 张,且还有万能牌,那么去掉这牌的 1 张,去掉 1 张万能牌,进行步骤 3;其余情况,重复步骤 2 尝试将下一张牌作为“将牌”,如果直至最后 1 张牌都找不到“将牌”,那么这副牌不能胡牌,算法结束;

        3. 从剩下的普通牌中,从第 1 张牌开始,直至最后1张牌。尝试组合碰结构。如果这张牌的数量 >= 3 那么就取出这张牌 3 张放在一边。如果这张牌有剩余且牌的值 > 7,那么再尝试组合碰结构,不足 3 张用万能牌补充差值,如果万能牌不够用,那么说明当前“将牌”下无法胡牌,那么将牌堆复原到只取出万能牌时的状态,重复步骤 2 ,挑选下一张牌作为“将牌”。如果 如果这张牌有剩余且牌的值 <= 7,那么进行步骤 4.

        4. 如果这张牌有剩余,尝试组合出这张牌剩余数量的吃结构,吃结构的另外两张牌如果空缺,需要用万能牌充当。此时如果万能牌不够用,那么说明当前“将牌”下无法胡牌那么将牌堆复原到只取出万能牌时的状态。重复步骤 2 ,挑选下一张牌作为将牌。

        6. 如果这张牌没有剩余且没到最后 1 张牌重复步骤 3 ,如果处理完最后 1 张牌且这张牌没有剩余(此时这副牌全都被取出),那么这副牌能胡!

        注:对于每一张牌优先找碰结构,如果有剩余的牌再找这张牌开头的吃结构(即这张牌位于吃结构的左边)。由于优先将牌组合成碰结构,所以这张牌剩余多少张,就需要形成多少个吃结构,因为这样才能保证这张牌不能剩下!另外,对于牌值 >7 的牌,这里只会找这张牌的碰结构,因为它之前的牌一定被用完了,无法组合成想要的吃结构(这张牌位于中间或者右边)。

算法1实现

bool _Find_AAA_ABC(int allcards[30], int index, int& magicCount)
{
	for (int i = index; i < 30; ++i)
	{
		if(0 == allcards[i])
			continue;

		if (3 <= allcards[i])
			allcards[i] = allcards[i] - 3;//形成 碰

		if(0 == allcards[i])
			continue;

		auto val = i % 10;
		if(7 < val)//牌值 大于 7 只选择组合成碰结构
		{
			magicCount -= (3 - allcards[i]);
			allcards[i] = 0;
		}
		if (magicCount < 0)
			return false;

		if(allcards[i] == 0)
			continue;

		auto NeedChiCount = allcards[i];
		allcards[i] = 0;

		auto haveCount1 = allcards[i + 1];
		if(haveCount1 < NeedChiCount)
			magicCount -= (NeedChiCount - haveCount1);
		else
		{
			allcards[i + 1] -= NeedChiCount;
			allcards[i + 1] = 0;
		}
			

		if (magicCount < 0)
			return false;

		auto haveCount2 = allcards[i + 2];
		if (haveCount2 < NeedChiCount)
			magicCount -= (NeedChiCount - haveCount2);
		else
		{
			allcards[i + 2] -= NeedChiCount;
			allcards[i + 2] = 0;
		}

		if (magicCount < 0)
			return false;

	}
	return true;
}

bool isHu()
{
	vector<int> cards = { 21,28,3,1,7,9,15,11,
										27,26,13,13,13,
										21,21,1,2/*,25,25*/ };
	int allCards[30] = { 0 };
	for (auto& i : cards)
		allCards[i]++;

	auto magicCardCount = 0;
	if (allCards[11] > 0)
	{
		magicCardCount = allCards[11];
		allCards[11] = 0;
	}
	auto oldMagicCount = magicCardCount;
	for (int i = 1; i < 30; ++i)//O(29)
	{
		if(allCards[i] == 0)
			continue;

		bool isFind = true;
		auto jiangCount = allCards[i];
		if (allCards[i] >= 2)
			allCards[i] = allCards[i] - 2;
		else if (allCards[i] == 1)
		{
			if (magicCardCount <= 0)
				continue;
			allCards[i]--;
			magicCardCount--;
		}

		int tmpCards[30] = { 0 };
		std::memcpy(tmpCards, allCards, 30*sizeof(int));
		if (_Find_AAA_ABC(tmpCards, 1, magicCardCount))
			return true;
		else
		{
			//将牌还原
			allCards[i] = jiangCount;
			magicCardCount = oldMagicCount;
		}

	}
	return false;
}

算法1分析

        这基本是一个二次时间花费的算法,尽管这里的 N 比较小仅为 29。算法的时间复杂度为 O(29*29);该算法并不算高效,原因在于,该算法对万能牌的使用比较随意,万能牌可以和任何只有一张的牌组成将牌,并且在组合吃结构以及部分碰组合时,对于万能牌的花费数量同样不定,可以是 2 张,也可以是 1 张。

        具体来讲,当一副牌中有现成的“将牌”时,如果仍然花费一张万能牌去和一张单牌组成将牌,是不必要的浪费。同样原本可以花费 1 张就形成的碰或吃结构,却花费了 2 张也是一种浪费。

        基于以上的分析,可以认为 ,只要推迟万能牌的使用(充当其他牌),以及花费更少数量的万能牌形成组合,那么就能更快的判断一副牌能否胡牌。

算法2描述

        1.从这副牌中取出所有万能牌,放在一边

        2.对于剩余的普通牌,从第 1 张牌开始,直至最后 1 张牌。如果牌值 <=7 ,那么先组合出这张牌的所有吃结构并取出,如果这张牌有剩余,再将这张牌组合成碰结构并取出,这里组合吃和碰都不使用万能牌。

        3.对于剩余的普通牌,从第 1 张牌开始,直至最后 1 张牌,如果某张牌数量为 2,那么取出这两张牌。

        4.对于剩余的普通牌,从第 1 张牌开始,直至最后 1 张牌。优先考虑使用 1 张万能牌来组合碰、吃结构,如果这张牌此时既不能形成碰,也不能形成吃,那么在还没有找到“将牌”的前提下,才尝试用一张万能牌和这张牌组合成“将牌”。其次再尝试用 2 张万能牌只组合成碰结构(这里吃结构是不必要的),期间如果万能牌不够用那么这副牌不能胡,算法结束。

       5.如果这副牌(包括普通牌和万能牌)没有剩余,那么这副牌能胡。如果有剩余那么不能胡。

算法2实现

bool isHu2()
{
	vector<int> cards = { 21,28,3,3,1,11,11,1,
										27,26,13,14,15,
										21,21,1,2/*,25,25*/ };
	int allCards[30] = { 0 };
	for (auto& i : cards)
		allCards[i]++;
	auto card_count = cards.size();

	auto magicCardCount = 0;
	if (allCards[11] > 0)
	{
		magicCardCount = allCards[11];
		allCards[11] = 0;
	}

	card_count -= magicCardCount;
	//组合 AAA 、ABC ,不使用万能牌
	_Find_AAA_ABC2(allCards, 1, card_count);//2,5,8,11,14,17

	//组合 AA ,不使用万能牌
	bool isFindJiang = false;
	for(int i=1; i<30; ++i)
		if (2 == allCards[i])
		{
			card_count -= 2;
			isFindJiang = true;
			allCards[i] = 0;
			break;
		}

	if (0 == (magicCardCount + card_count))
		return true;

	//2,0;2,1;3,0; 3,2; 4,1;4,2;
	if (magicCardCount > card_count) //magicCardCount: 0~4,card_count: 1~13
		return true;

	for (int i = 1; i < 30; ++i)
	{
		if (0 == allCards[i])
			continue;

		if (2 == allCards[i])//优先花费 1 张万能牌形成 碰结构
		{
			allCards[i] = 0;
			card_count -= 2;
			magicCardCount -= 1;
			continue;
		}

		if (1 == allCards[i])//其次花费 1 张万能牌形成 吃结构
		{
			auto val = i % 10;
			if (1 <= allCards[i + 1] && 7 >= val)
			{
				allCards[i] = 0;
				allCards[i + 1] -= 1;
				magicCardCount -= 1;
				card_count -= 2;
				continue;
			}
			else if (1 <= allCards[i + 2] && 7 >= val)
			{
				allCards[i] = 0;
				allCards[i + 2] -= 1;
				magicCardCount -= 1;
				card_count -= 2;
				continue;
			}

			//既然形成不了吃结构,如果还没找到将牌,考虑组合成将牌
			if (!isFindJiang)
			{
				allCards[i] = 0;
				magicCardCount -= 1;
				card_count -= 1;
				isFindJiang = true;
				continue;
			}

			//最次考虑使用两张形成碰组合
			allCards[i] = 0;
			card_count -= 1;
			magicCardCount -= 2;
		}

	}

	return 0 == (card_count+magicCardCount);
}

void _Find_AAA_ABC2(int allcards[30], int index, size_t &card_count)
{
	for (int i = index; i < 30; ++i)
	{
		if (0 == allcards[i])
			continue;

		auto val = i % 10;
		if (7 >= val)
		{
			auto NeedChiCount = std::min(allcards[i], std::min(allcards[i + 1], allcards[i + 2]));
			if (NeedChiCount < 1)
				continue;

			allcards[i] -= NeedChiCount;
			allcards[i + 1] -= NeedChiCount;
			allcards[i + 2] -= NeedChiCount;
			card_count -= (3 * NeedChiCount);
		}

		if (3 <= allcards[i])
		{
			allcards[i] = allcards[i] - 3;
			card_count -= 3;
		}

	}
}}
}

算法2分析

        算法共计用了 3 个 最多 29 次的 for 循环,时间复杂度为 O(3*29),3 可以忽略。这是一个线性时间花费的算法。和算法 1 不同的是,算法 1 随意组合,当万能牌不够用时算法结束。算法 2 尽可能推迟万能牌的使用,并且要求万能牌花费越少越好,另外算法 2 不仅关注剩余万能牌的数量,同时时刻关注剩余的普通牌数量。

        

	if (magicCardCount > card_count) //magicCardCount: 0~4,card_count: 1~13
		return true;

        因为万能牌数量与剩余普通牌数量的和要么是 3n,要么是 3n+2,再加上万能牌最多有 4 张。因此当万能牌数量比剩余的普通牌数量多时,只能是以下几种情况:

        万能牌数量:2;剩余普通牌数量:0;

        万能牌数量:2;剩余普通牌数量:1;

        万能牌数量:3;剩余普通牌数量:0;

        万能牌数量:3;剩余普通牌数量:2;

        万能牌数量:4;剩余普通牌数量:1;

        万能牌数量:4;剩余普通牌数量:2;

        而且,这几种情况都是可以胡牌的,只要符合条件,就可以在这里终止算法。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值