~ 如何用C++自制一个日麻游戏 ~(二)听牌判断算法 § 1 判断听牌(附带C#实现)

导入

—— 什么环节只要用算法判断一次,就能知道是否听牌立直、还差什么牌就可以荣和自摸?
—— 只要在缺一张手牌(如1、4、7、10、13张时)的情况下判断是否听牌、听哪些牌,就可以为上面的复杂判断提供基础。
网上大部分方法会用大量遍历查表等方法,解决这个问题这也就是我探索新方法的初衷

正题

为了简明地探讨这个问题,我先举一个已经和牌的例子:
在这里插入图片描述
如果未立直被点北风,那就是个很惨的役牌1番40符233
为了更好看清,我分为了4个面子和1个雀头,这时如果拿走一张牌让他变成听牌的形式,那就有3种情况:
首先下一个定义:几张连续或相同的牌,我称为 1 块(Block),上面例子中就用空格分开了块

  1. 拿走顺子的边张后,还剩下 5 块
    2
  2. 拿走顺子的中张,变成 6 块(这是听牌情况下,块数最多的情况)
    3
  3. 拿走雀头的一张,还剩 5 块
    4

此外,和牌时还有更复杂的复合形式,即有一块里既有雀头又有面子,这里举个简单的例子:
5
他如果缺一张一万或四万时,其中有一块不完整的便既含有雀头也含有面子:
6
这些便是所有情况的基本形式,我把他分为3大类,5小类,这时出现了预告里的图片,这五类我会依次介绍:
类型

  • 面子完整型(3n):顾名思义,只含有面子(即刻子或顺子)的块,牌数为3n,可以直接判断:
    7
  • 雀头完整型(3n+2):包含一个雀头,虽然不是3的倍数,但依然较完整,去对(去掉一个对子)就可以判断出缺(听)的牌:
    8
  • 半不完整型(3n+1):即嵌(坎)张,所以在听牌时都会成对出现,所以取中张(两块中间的那张)就可以了:
    9
  • 面子不完整型(3n+2):即面子完整型缺一张牌(但不会形成两块),听牌时会和雀头完整型一起出现,形成多面听或者双碰,用遍历(在该块的范围内遍历,遍历的次数较少)的方法可以找出:
    (根据不同牌型,遍历 不同的牌数 ~ 不同的牌数+2 次就可以)(至少3次、至多9次)
    10
  • 全不完整型(3n+4):最复杂的一种,即雀头完整型缺一张牌,因为不知道缺在雀头还是在面子上,所以只能用遍历(遍历的次数也较少)后再去对来处理:
    11

综上,所有牌型都可以分为如上五种情况处理,可以算是一种归类或者剪枝(?)
接下来便可以写代码了,首先得先按数量分成几块(Block),才能进行更深层的操作:

代码实现

首先,这个算法是针对每一家的,所以写在 Opponent 类下面,并在旁边创建新 Block 类:
(由于听的牌并不是特指牌山里的某一张,所以这里的 chong 不是指针)

//main.hpp
class Opponent
{
public:
	bool ting{ false };		//是否听牌
	vector<Tile> chong; 		//可以和的牌(听的牌)(铳)
	void ting_judge();		//听牌判断(在摸牌前判断)
...
public:		//下面是会用到的函数
	_int_ operator<<(Tile* card)		//(进张)摸牌
	{
		_int_ ru{ 0 };
		while (ru < tile.size() && *card >* tile[ru])		//找到进张插入的位置
			++ru;
		tile.insert(tile.begin() + ru, card);		//插入牌
		return ru;		//返回该牌的位置
	}
	__forceinline Tile*& operator[](_int_ num) { return tile[num]; }		//直接访问牌元素
	_int_ link(_int_ num)		//(前张牌序号)两张手牌间关系
	{		//理论上不会越界,但是以防万一,用try块保护
		try { return *tile.at((_ull_)num + 1) - *tile.at(num); }
		catch (out_of_range) { return 100; }		//(尽量大的数)
	}
}
//ting.hpp
class Block { ... }
void Opponent::ting_judge() { ... }

开始写 ting_judge() 函数里的内容:
首先清空听牌的状态

ting = false;
chong.clear();		//清空铳牌
...

然后是特殊牌型的判断(国士、七对):
由于算法很简单也很多样,我就不做详细介绍,只有大致介绍:

  • 国士:这里我的牌对应数字的定义(详见(一)§ 3)有一些优势:牌的序号*8就是对应的幺九牌,此外用 que 和 duo 两个 bool 型变量便可以轻松实现
  • 七对:由于日麻没有龙七对的役种,只好逐张判断,一般情况下偶数序号牌和下一张是相同的,而奇数的和下张不是相同,如果有单张会用 dan 的 bool 型变量记录(按照cpp习惯数组第一个序号是0)
...
if (fulu.empty())		//如果没有副露(特殊牌型判断)
	{		//国士牌型判断
		bool que{ false };		//是否缺了某张幺九牌
		bool duo{ false };		//是否多了某张幺九牌
		_int_ que_tile{ 0 };		//缺的幺九牌
		for (_int_ i{ 0 }; i < 13; ++i)		//判断十三张幺九牌的拥有情况
			if (*tile[i] == (i + que - duo) * 8);		//如果刚好位置映射
			else if (*tile[i] == (i + que - duo - 1) * 8)		//如果和上张映射幺九牌一样
			{
				if (duo == true)		//如果已经有一个多的牌
					goto guoshi_end;
				else duo = true;		//记录有多牌
			}
			else if (*tile[i] == (i + que - duo + 1) * 8)		//如果和下张映射幺九牌一样
			{
				if (que == true)		//如果已经有一个缺牌
					goto guoshi_end;		//不是国士
				else que = true;		//记录有缺牌
				que_tile = i * 8;		//记录缺的牌
			}
			else goto guoshi_end;		//有不是幺九即无听
		if (duo == true)		//若有多张
		{
			if (que == true)
				chong.emplace_back(que_tile);		//记听一面
			else chong.emplace_back(96);		//记听一面(中)(中在最后不会被que记录)
		}
		else for (_int_ i{ 0 }; i < 13; ++i)		//若不缺张
			chong.emplace_back(i * 8);		//记听十三面
		ting = true;
		return;
	guoshi_end:

		//七对牌型判断
		bool dan{ false };		//多出来的单张
		_int_ dan_tile{ 0 };		//单张牌位置
		for (_int_ i{ 0 }; i < 12; ++i)		//判断关系
			if ((i + dan) % 2 ^ _int_(bool(link(i))))		//如果偶数位关系对应不是相同,或奇数位不是其他
			{		//直接异或运算无法排除龙七对
				if (link(i) == 0)		//如果这个错误关系是相同
					goto qidui_end;		//龙七对
				else if (dan == true)		//如果已经有单牌了
					goto qidui_end;		//不是七对子
				else
				{
					dan = true;		//记录单张
					dan_tile = *tile[i];		//记录序号
				}
			}
		if (dan == false)		//如果没查到单张
			dan_tile = *tile[12];		//那单张就是最后一个
		chong.emplace_back(dan_tile);		//记听一面
		ting = true;
		//有可能复合二杯口,故听牌后不退出
	}
qidui_end:
...

接下来是判断完整型:
首先写 Block 类:
其成员拥有3个,在代码片中有注释;通常在创建新 block 时就已经确定了其中 loc 的值,所以写在构造函数

class Block		//块(其中牌关系只有相同或连续)
{
public:
	_int_ loc{ 0 };		//块内首张牌的序号(大概可以写成const,但没必要)
	_int_ len{ 1 };		//块内牌数(至少一张)
	bool type{ true };		//类型(真(3n)为完整型(由整数个面子组成),假为不完整型(含有雀头、不完整的面子))
	Block(_int_ _loc) : loc{ _loc } {}
	...
}

然后写 block 的判断:
3n张牌的初步被判断为完整型(true),其他为不完整型(false)
不难发现,在如下这种听牌情况时,听牌块数达到了最多的6块,不完整(包括雀头完整型)的块数达到了最多的3块:
3
而且在找到下一块的开头时,也会得到上一块的总长度,所以把上一块收尾和下一块的开头写在同一个循环体内
于是我们写如下代码:

...
_int_ block_err{ 0 };		//不完整的块数(最多3个)
vector<Block> block;		//块(最多6个)
block.reserve(6);
block.emplace_back(0);		//***第一块的开始***
for (_int_ i{ 0 }; i < tile.size() - 1; ++i)
	if (link(i) > 1)		//当关系不是相同或连续
	{
		block.back().len = i - block.back().loc + 1;		//记录上一块的长度
		block.back().type = !(block.back().len % 3);		//筛选完整型Lv.1
		block_err += !block.back().type;		//记录
		if (block.size() == 6 - fulu.size() || block_err == 4)		//若块序号达到(6 - 副露数)或有4个不整型
			return;		//无听
		block.emplace_back(i + 1);		//括号里是块内首张牌的序号
	}
block.back().len = (_int_)tile.size() - block.back().loc;		//记录上一块的长度
block.back().type = !(block.back().len % 3);		//筛选完整型Lv.1
block_err += !block.back().type;		//记录 ***最后一块的结束***
if (block_err == 4)		//若有4个不完整型
	return;		//无听
...

注意:emplace_back() 固然很好用,但是要看清类型。如 vector< T > a,括号可以认为是 T 的构造函数,里面只能填 T 类型的对象(符合拷贝构造函数)或者填写 T 构造函数的参数列表;如果是 vector< T* > a,那只能填 T* 类型的对象(拷贝构造函数)。否则会在 vector 或 xmemory 头文件报错 C2440,很难排查!
下面就要写重要的判断面子完整型函数(block_complete):

类型判断

block_complete

导入

为了更好看清每块的内部结构,我需要继续细分:
定义:块(Block)内所有相同的牌分为 1 组(Group)
如此,例如:
12
示意图:整张图都是属于一个块的,每一列都是一个组
示意图
然后想象自己是程序,从最边的第0组开始,一组一组地判断:
先杠刻子:如果遇到3个没杠掉的圈,3个一起杠掉;
再杠顺子:剩下的没杠掉的如果不满3个,在本组杠掉1个,下组和下下组也杠掉1个(也是共杠三个);
如果这组要杠掉1个,而下组或下下组不够的杠了,说明不是完整型,反之如果刚好杠完就是完整型。
拿上图举例:
第一次 ——
第二次 ——
第三次 ——
第四次 ——
杠
判断出这是面子完整型了,很简单吧?


如果是如下的牌型呢?
13
第一次 ——
第二次 ——
第三次 ——
杠2
杠到第四次时,发现四万有2张,理应杠掉五万和六万各两张,但是不够了,所以这不是面子完整型


这种方法可以正确分离所有的类型,除了三连刻无法识别成三条顺子:
14
但是就听牌来说,这并不会影响到是否听牌、听哪些牌的判断,而且之后改进也十分容易

代码实现

根据原理,实现这个并不难(写在 Block 类下):
注:“ * ”星号框起来的部分是为了以后对接“去对”的操作,现在并没有什么用 ╮(╯▽╰)╭
其中 block_card 的 bool 型容器记录每张牌是属于刻子还是顺子的,被当做刻(前面被竖着杠掉的)记做 flase ,当做顺(被斜杠杠掉的)记做 true

bool block_complete(Opponent& op, _int_ quetou_loc = -1)		//(判断的牌组,雀头的序号(-1为没有雀头))筛选完整型Lv.2
{
	class Group		//组
	{
	public:
		Group(_int_ _loc_) :loc_{ _loc_ } {}		//构造
		const _int_ loc_{ 0 };		//组内首张牌的序号
		_int_ len_{ 1 };		//组内牌数
		_int_ confirmed{ 0 };		//已确认(顺/刻)的牌数量
	};

	vector<Group> group;
	group.emplace_back(loc);
	_int_ i{ loc };
	for (; i < loc + len - 1; ++i)		//判断块内每个关系
		if (op.link(i) == 1)		//当关系是连续
		{
				group.back().len_ = i + 1 - group.back().loc_;
				group.emplace_back(i + 1);		//记录多一个组
		}
	group.back().len_ = i + 1 - group.back().loc_;
	const _int_ group_num{ (_int_)group.size() };		//包含的组数

	vector<bool> block_card(len, true);		//判断块内每张牌,若该牌属于顺子即记为顺(true),否则记为刻(flase)
	//*******************************************************************
	if (quetou_loc != -1)		//有雀头
	{
		++++group[quetou_loc].confirmed;		//雀头处有两个刻
		block_card[(_ull_)group[quetou_loc].loc_ - loc] = false;
		block_card[(_ull_)group[quetou_loc].loc_ - loc + 1] = false;
	}
	//*******************************************************************
	for (_ull_ i{ 0 }; i < group_num; ++i)		//每次循环记录一个组
	{
		switch (group[i].len_ - group[i].confirmed)		//该组牌数
		{
		case 0:
			continue;		//开始下一循环
			break;
		case 1:		//都是顺
			if (group_num > i + 2)
			{
				++group[i + 1].confirmed;		//确定后面几组有2张是顺
				++group[i + 2].confirmed;
				continue;
			}
			break;
		case 2:		//都是顺
			if (group_num > i + 2)
			{
				++++group[i + 1].confirmed;		//确定后面几组有2张是顺
				++++group[i + 2].confirmed;
				continue;
			}
			break;
		case 4:		//3刻1顺
			if (group_num > i + 2)
			{
				++group[i + 1].confirmed;		//确定后面几组有2张是顺
				++group[i + 2].confirmed;
				block_card[(_ull_)group[i].loc_ - loc] = false;		//三张是刻
				block_card[(_ull_)group[i].loc_ - loc + 1] = false;
				block_card[(_ull_)group[i].loc_ - loc + 2] = false;
				continue;
			}
			break;
		case 3:		//都是刻
			block_card[(_ull_)group[i].loc_ - loc] = false;		//三张是刻
			block_card[(_ull_)group[i].loc_ - loc + 1] = false;
			block_card[(_ull_)group[i].loc_ - loc + 2] = false;
			continue;
			break;
		default:		//可能是负数
			break;
		}
		type = false;		//不完整型
		return false;
	}
	return true;
}
...
const _int_ block_num{ (_int_)block.size() };		//块数
	for (_int_ num{ 0 }; num < block_num; ++num)
		if (block[num].type == true)		//只判断通过完整型Lv.1的
			if (block[num].block_complete(*this) == false)		//筛选完整型Lv.2,若不完整
				return;		//无听
...

其他不完整型判断

导入

有了 block_complete() 函数,剩下的一切都很明朗了:只要想办法往面子完整型上凑就好了
之前说了如果是听牌的牌型,不完整型(err_block)最多只能有3个,那分别有1、2、3个时,会有特征吗?
答案是有,而且有很明显的分类

  • 只有 1 个时:该不完整型一定是全不完整型,例:
    15
  • 有 2 个时:会有一个雀头完整型和一个面子不完整型,例:
    16
  • 有 3 个时:会有一个雀头完整型和两个半不完整型,如:
    17

所以可以用一个switch语句,来讨论这三种情况:

  • 由于只有半不完整型会用取中张的方法,所以就不用特意写个函数了
  • 遍历有两种模式,一种遍历后直接判断是否完整(面子不完整型),一种遍历后还要去对,所以参数列表里还有个 const bool 类型
class Block
{
public:
	...
	bool block_qudui(Opponent& op) { ... }		//(判断的牌组)雀头完整型
	bool block_bianli(Opponent& op, const bool mode) { ... }		//(判断的牌组,假为面子不完整型,真为全不完整型)
	
};
void Opponent::ting_judge()
{
	...
	switch (block_err)
	{
	case 1:		//有一块不完整型(一块全不完整型(3n+4))
	{
		for (_int_ err_block{ 0 }; err_block < block_num; ++err_block)
			if (block[err_block].type == false)		//是不完整型
			{
				if (block[err_block].block_bianli(*this, true))
					break;
				else return;		//无听
			}
		break;
	}
	case 2:		//有两块不完整型(一块面子不完整型(3n+2),一块雀头完整型(3n+2))
	{
		_int_ err_block_1{ 0 };
		for (; err_block_1 < block_num; ++err_block_1)
			if (block[err_block_1].type == false)		//是不完整型
				break;		//记录
		_int_ err_block_2{ err_block_1 + 1 };		//第二个不完整型
		for (; err_block_2 < block_num; ++err_block_2)
			if (block[err_block_2].type == false)		//是不完整型
				break;		//记录
		if (err_block_2 >= block_num)		//如果后者的数字大于等于组数
			return;		//无听
		bool temp = false;
		//去对和遍历顺序不能互换,因为如果遍历成功会直接写入铳牌,而去对成功不会写入铳牌,因为&&的短路所以去对写在前面
		if (blocks[err_block_2].block_ignore_pair(*this) && blocks[err_block_1].block_traversal(*this, false)		//满足任意一个,但是两条语句都要执行
			| blocks[err_block_1].block_ignore_pair(*this) && blocks[err_block_2].block_traversal(*this, false))		//按位或不会出现短路
			break;		//听牌
		else return;		//无听
	}
	case 3:		//有三块不完整型(两块半不完整型(3n+1),一块雀头完整型(3n+2))
	{
		_int_ err_block{ 0 }; 
		for (; err_block < block_num; ++err_block)
			if (block[err_block].type == false)		//是不完整型
				if (block[err_block].len % 3 == 1)		//如果牌数是3的倍数+1
					break;		//记录err_loc
		Tile temp_chong{ *tile[(_ull_)block[err_block].loc + block[err_block].len - 1] + 1 };		//中间隔的牌(临时记录铳牌)
		if (temp_chong != *tile[block[(_ull_)err_block + 1].loc] - 1)		//如果它和下一张牌不连续
			return;		//无听
		for (_int_ quetou_block{ 0 }; quetou_block < block_num; ++quetou_block)
			if (block[quetou_block].type == false)		//是不完整型
				if (block[quetou_block].len % 3 == 2)		//如果牌数是3的倍数+2(雀头)
					if (block[quetou_block].block_qudui(*this) == false)		//如果不是雀头块
						return;		//无听
		Opponent temp_op;		//临时用来判断的牌组
		Block temp_block{ 0 };
		temp_block.len = block[err_block].len + 1 + block[(_ull_)err_block + 1].len;		//这两个不完整型总长度+一张中间隔的牌
		_int_ i{ 0 };
		for (; i < block[err_block].len; ++i)
			temp_op[i] = tile[(_ull_)block[err_block].loc + i];		//复制该不完整型
		temp_op[i] = &temp_chong;		//记录中间的牌
		for (; i < temp_block.len; ++i)
			temp_op[i] = tile[(_ull_)block[err_block].loc + i];		//复制该不完整型
		if (temp_block.block_complete(temp_op) == true)		//如果该牌组完整
			chong.emplace_back(temp_chong);		//记听一面
		break;
	}
	}
	ting = true;		//听牌
}

有了上面的解释,这段代码应该不难理解,现在改写遍历和去对的算法了:

  • 遍历:就是在所在块上,加上任意一张,在不形成新块的情况下使它成为完整型,例如:
    18
    所以说遍历次数比 不同牌的数量 多 0-2 次(至少3次、至多9次)就可以,在上图情况下需要遍历不同的牌数 2+2 次,而如果块中含有老头牌或是字牌块,遍历次数能减少1-2次
  • 去对:找到所有的对子,每次去掉一个,查看他是否完整;这里直接传递给 block_complete(),让他的 block_card 直接认为那个对子是刻子的一部分,就可以排除对子了

代码实现

这段代码写在 Block 类下,也不是很冗长:

bool block_qudui(Opponent& op)		//(判断的牌组)雀头完整型
{
	for (_int_ i{ loc }, temp_group_num{ 0 }; i < loc + len - 1; ++i)
	{
		if (op.link(i) == 1)		//当关系是连续
			++temp_group_num;		//组数加一
		else if (block_complete(op, temp_group_num) == true)		//当关系是相同,判断是否是雀头完整型
			return true;		//该组是雀头完整型
	}
	return false;		//不完整
}
bool block_bianli(Opponent& op, const bool mode)		//(判断的牌组,假为面子不完整型,真为全不完整型)
{
	_int_ first{ *op[loc] - 1 };		//可能的首张牌
	if (*op[loc] % 16 == 0 || *op[loc] / 8 > 5)		//如果首张是一万、筒、索或字牌
		++first;		//first没有前一张,加回op[loc]
	_int_ last = *op[loc + len - 1] + 1;		//可能的末张牌
	if (*op[loc + len - 1] % 16 == 8 || *op[loc + len - 1] / 8 > 5)		//如果末张是九万、筒、索或字牌
		--last;		//last没有后一张,减回op[loc]
	Block temp_block{ 0 };		//新牌组对应的块
	temp_block.len = len + 1;
	_int_ temp_tile{ first };
	_int_ temp{ 0 };
	Opponent temp_op;
	for (_int_ i{ 0 }; i < last - first + 1; ++i, ++temp_tile)		//每张牌都插入尝试一次(遍历)
	{
		for (_int_ j{ loc }; j < loc + len; ++j)		//重新复制所有牌
			temp_op.tile.emplace_back(op[j]);
		temp = temp_op << new Tile(temp_tile);		//插入尝试的牌
		if (mode && temp_block.block_qudui(temp_op) 		//(真)全不完整型且遍历、去对后完整
			|| !mode && temp_block.block_complete(temp_op))		//(假)面子不完整型且遍历后完整
			op.chong.emplace_back(temp_tile);
		delete temp_op[temp];
		temp_op.tile.clear();
	}
	if (op.chong.empty())		//可能本来它就不为空,不过在这里不影响(将来算符时可能改动)
		return false;
	else return true;

以上听牌的算法就写完了,总计 270 多行,个人实验可以判断出日麻里所有听的牌(不考虑振听、空听情况下),大家可以自己实验一下2333

完整代码

#ifndef TING_HPP
#define TING_HPP

class Block		//块(其中牌关系只有相同或连续)
{
public:
	_int_ loc{ 0 };		//块内首张牌的序号
	_int_ len{ 1 };		//块内牌数(至少一张)
	bool type{ true };		//类型(真(3n)为完整型(由整数个面子组成),假为不完整型(含有雀头、不完整的面子))
	Block(_int_ _loc) : loc{ _loc } {}
	bool block_complete(Opponent& op, _int_ quetou_loc = -1)		//(判断的牌组,雀头的序号(-1为没有雀头))筛选完整型Lv.2
	{
		class Group		//组
		{
		public:
			Group(_int_ _loc_) :loc_{ _loc_ } {}		//构造
			const _int_ loc_{ 0 };		//组内首张牌的序号
			_int_ len_{ 1 };		//组内牌数
			_int_ confirmed{ 0 };		//已确认(顺/刻)的牌数量
		};

		vector<Group> group;
		group.emplace_back(loc);
		_int_ i{ loc };
		for (; i < loc + len - 1; ++i)		//判断块内每个关系
			if (op.link(i) == 1)		//当关系是连续
			{
				group.back().len_ = i + 1 - group.back().loc_;
				group.emplace_back(i + 1);		//记录多一个组
			}
		group.back().len_ = i + 1 - group.back().loc_;
		const _int_ group_num{ (_int_)group.size() };		//包含的组数

		vector<bool> block_card(len, true);		//判断块内每张牌,若该牌属于顺子即记为顺(true),否则记为刻(flase)
		if (quetou_loc != -1)		//有雀头
		{
			++++group[quetou_loc].confirmed;		//雀头处有两个刻
			block_card[(_ull_)group[quetou_loc].loc_ - loc] = false;
			block_card[(_ull_)group[quetou_loc].loc_ - loc + 1] = false;
		}
		for (_ull_ i{ 0 }; i < group_num; ++i)		//每次循环记录一个组
		{
			switch (group[i].len_ - group[i].confirmed)		//该组牌数
			{
			case 0:
				continue;		//开始下一循环
				break;
			case 1:		//都是顺
				if (group_num > i + 2)
				{
					++group[i + 1].confirmed;		//确定后面几组有2张是顺
					++group[i + 2].confirmed;
					continue;
				}
				break;
			case 2:		//都是顺
				if (group_num > i + 2)
				{
					++++group[i + 1].confirmed;		//确定后面几组有2张是顺
					++++group[i + 2].confirmed;
					continue;
				}
				break;
			case 4:		//3刻1顺
				if (group_num > i + 2)
				{
					++group[i + 1].confirmed;		//确定后面几组有2张是顺
					++group[i + 2].confirmed;
					block_card[(_ull_)group[i].loc_ - loc] = false;		//三张是刻
					block_card[(_ull_)group[i].loc_ - loc + 1] = false;
					block_card[(_ull_)group[i].loc_ - loc + 2] = false;
					continue;
				}
				break;
			case 3:		//都是刻
				block_card[(_ull_)group[i].loc_ - loc] = false;		//三张是刻
				block_card[(_ull_)group[i].loc_ - loc + 1] = false;
				block_card[(_ull_)group[i].loc_ - loc + 2] = false;
				continue;
				break;
			default:		//可能是负数
				break;
			}
			type = false;		//不完整型
			return false;
		}
		return true;
	}
	bool block_qudui(Opponent& op)		//(判断的牌组)雀头完整型
	{
		for (_int_ i{ loc }, temp_group_num{ 0 }; i < loc + len - 1; ++i)
		{
			if (op.link(i) == 1)		//当关系是连续
				++temp_group_num;		//组数加一
			else if (block_complete(op, temp_group_num) == true)		//当关系是相同,判断是否是雀头完整型
				return true;		//该组是雀头完整型
		}
		return false;		//不完整
	}
	bool block_bianli(Opponent& op, const bool mode)		//(判断的牌组,假为面子不完整型,真为全不完整型)
	{
		_int_ first{ *op[loc] - 1 };		//可能的首张牌
		if (*op[loc] % 16 == 0 || *op[loc] / 8 > 5)		//如果首张是一万、筒、索或字牌
			++first;		//first没有前一张,加回op[loc]
		_int_ last = *op[loc + len - 1] + 1;		//可能的末张牌
		if (*op[loc + len - 1] % 16 == 8 || *op[loc + len - 1] / 8 > 5)		//如果末张是九万、筒、索或字牌
			--last;		//last没有后一张,减回op[loc]
		Block temp_block{ 0 };		//新牌组对应的块
		temp_block.len = len + 1;
		_int_ temp_tile{ first };
		_int_ temp{ 0 };
		Opponent temp_op;
		for (_int_ i{ 0 }; i < last - first + 1; ++i, ++temp_tile)		//每张牌都插入尝试一次(遍历)
		{
			for (_int_ j{ loc }; j < loc + len; ++j)		//重新复制所有牌
				temp_op.tile.emplace_back(op[j]);
			temp = temp_op << new Tile(temp_tile);		//插入尝试的牌
			if (mode && temp_block.block_qudui(temp_op) 		//(真)全不完整型且遍历、去对后完整
				|| !mode && temp_block.block_complete(temp_op))		//(假)面子不完整型且遍历后完整
				op.chong.emplace_back(temp_tile);
			delete temp_op[temp];
			temp_op.tile.clear();
		}
		if (op.chong.empty())		//可能本来它就不为空,不过在这里不影响(将来算符时可能改动)
			return false;
		else return true;
	}
};
void Opponent::ting_judge()
{
	ting = false;
	chong.clear();		//清空铳牌

	if (fulu.empty())		//如果没有副露(特殊牌型判断)
	{		//国士牌型判断
		bool que{ false };		//是否缺了某张幺九牌
		bool duo{ false };		//是否多了某张幺九牌
		_int_ que_tile{ 0 };		//缺的幺九牌
		for (_int_ i{ 0 }; i < 13; ++i)		//判断十三张幺九牌的拥有情况
			if (*tile[i] == (i + que - duo) * 8);		//如果刚好位置映射
			else if (*tile[i] == (i + que - duo - 1) * 8)		//如果和上张映射幺九牌一样
			{
				if (duo == true)		//如果已经有一个多的牌
					goto guoshi_end;
				else duo = true;		//记录有多牌
			}
			else if (*tile[i] == (i + que - duo + 1) * 8)		//如果和下张映射幺九牌一样
			{
				if (que == true)		//如果已经有一个缺牌
					goto guoshi_end;		//不是国士
				else que = true;		//记录有缺牌
				que_tile = i * 8;		//记录缺的牌
			}
			else goto guoshi_end;		//有不是幺九即无听
		if (duo == true)		//若有多张
		{
			if (que == true)
				chong.emplace_back(que_tile);		//记听一面
			else chong.emplace_back(96);		//记听一面(中)(中在最后不会被que记录)
		}
		else for (_int_ i{ 0 }; i < 13; ++i)		//若不缺张
			chong.emplace_back(i * 8);		//记听十三面
		ting = true;
		return;
	guoshi_end:

		//七对牌型判断
		bool dan{ false };		//多出来的单张
		_int_ dan_tile{ 0 };		//单张牌位置
		for (_int_ i{ 0 }; i < 12; ++i)		//判断关系
			if ((i + dan) % 2 ^ _int_(bool(link(i))))		//如果偶数位关系对应不是相同,或奇数位不是其他
			{		//直接异或运算无法排除龙七对
				if (link(i) == 0)		//如果这个错误关系是相同
					goto qidui_end;		//龙七对
				else if (dan == true)		//如果已经有单牌了
					goto qidui_end;		//不是七对子
				else
				{
					dan = true;		//记录单张
					dan_tile = *tile[i];		//记录序号
				}
			}
		if (dan == false)		//如果没查到单张
			dan_tile = *tile[12];		//那单张就是最后一个
		chong.emplace_back(dan_tile);		//记听一面
		ting = true;
		//有可能复合二杯口,故听牌后不退出
	}
	qidui_end:


	_int_ block_err{ 0 };		//不完整的块数(最多3个)
	vector<Block> block;		//块(最多6个)
	block.reserve(6);
	block.emplace_back(0);		//第一块
	for (_int_ i{ 0 }; i < tile.size() - 1; ++i)
		if (link(i) > 1)		//当关系不是相同或连续
		{
			block.back().len = i - block.back().loc + 1;		//记录上一块的长度
			block.back().type = !(block.back().len % 3);		//筛选完整型Lv.1
			block_err += !block.back().type;		//记录
			if (block.size() == 6 - fulu.size() || block_err == 4)		//若块序号达到(6 - 副露数)或有4个不完整型
				return;		//无听
			block.emplace_back(i + 1);		//括号里是块内首张牌的序号
		}
	block.back().len = (_int_)tile.size() - block.back().loc;		//记录上一块的长度
	block.back().type = !(block.back().len % 3);		//筛选完整型Lv.1
	block_err += !block.back().type;		//记录
	if (block_err == 4)		//若有4个不完整型
		return;		//无听
	const _int_ block_num{ (_int_)block.size() };		//块数
	for (_int_ num{ 0 }; num < block_num; ++num)
		if (block[num].type == true)		//只判断通过完整型Lv.1的
			if (block[num].block_complete(*this) == false)		//筛选完整型Lv.2,若不完整
				return;		//无听
	switch (block_err)
	{
	case 1:		//有一块不完整型(一块全不完整型(3n+4))
	{
		for (_int_ err_block{ 0 }; err_block < block_num; ++err_block)
			if (block[err_block].type == false)		//是不完整型
			{
				if (block[err_block].block_bianli(*this, true))
					break;
				else return;		//无听
			}
		break;
	}
	case 2:		//有两块不完整型(一块面子不完整型(3n+2),一块雀头完整型(3n+2))
	{
		_int_ err_block_1{ 0 };
		for (; err_block_1 < block_num; ++err_block_1)
			if (block[err_block_1].type == false)		//是不完整型
				break;		//记录
		_int_ err_block_2{ err_block_1 + 1 };		//第二个不完整型
		for (; err_block_2 < block_num; ++err_block_2)
			if (block[err_block_2].type == false)		//是不完整型
				break;		//记录
		if (err_block_2 >= block_num)		//如果后者的数字大于等于组数
			return;		//无听
		bool temp = false;
		//去对和遍历顺序不能互换,因为如果遍历成功会直接写入铳牌,而去对成功不会写入铳牌,因为&&的短路所以去对写在前面
		if (blocks[err_block_2].block_ignore_pair(*this) && blocks[err_block_1].block_traversal(*this, false)		//满足任意一个,但是两条语句都要执行
			| blocks[err_block_1].block_ignore_pair(*this) && blocks[err_block_2].block_traversal(*this, false))		//按位或不会出现短路
			break;		//听牌
		else return;		//无听
	}
	case 3:		//有三块不完整型(两块半不完整型(3n+1),一块雀头完整型(3n+2))
	{
		_int_ err_block{ 0 }; 
		for (; err_block < block_num; ++err_block)
			if (block[err_block].type == false)		//是不完整型
				if (block[err_block].len % 3 == 1)		//如果牌数是3的倍数+1
					break;		//记录err_loc
		Tile temp_chong{ *tile[(_ull_)block[err_block].loc + block[err_block].len - 1] + 1 };		//中间隔的牌(临时记录铳牌)
		if (temp_chong != *tile[block[(_ull_)err_block + 1].loc] - 1)		//如果它和下一张牌不连续
			return;		//无听
		for (_int_ quetou_block{ 0 }; quetou_block < block_num; ++quetou_block)
			if (block[quetou_block].type == false)		//是不完整型
				if (block[quetou_block].len % 3 == 2)		//如果牌数是3的倍数+2(雀头)
					if (block[quetou_block].block_qudui(*this) == false)		//如果不是雀头块
						return;		//无听
		Opponent temp_op;		//临时用来判断的牌组
		Block temp_block{ 0 };
		temp_block.len = block[err_block].len + 1 + block[(_ull_)err_block + 1].len;		//这两个不完整型总长度+一张中间隔的牌
		_int_ i{ 0 };
		for (; i < block[err_block].len; ++i)
			temp_op[i] = tile[(_ull_)block[err_block].loc + i];		//复制该不完整型
		temp_op[i] = &temp_chong;		//记录中间的牌
		for (; i < temp_block.len; ++i)
			temp_op[i] = tile[(_ull_)block[err_block].loc + i];		//复制该不完整型
		if (temp_block.block_complete(temp_op) == true)		//如果该牌组完整
			chong.emplace_back(temp_chong);		//记听一面
		break;
	}
	}
	ting = true;		//听牌
}

#endif

附带C#代码实现

using System.Collections.Generic;
using System.Linq;

namespace Cs.Classes
{
    /// <summary>
    /// 块(其中牌关系只有相同或连续)
    /// </summary>
    public class Block
    {
        /// <summary>
        /// 块内首张牌的序号
        /// </summary>
        public int Loc { get; }
        /// <summary>
        /// 块内牌数(至少一张)
        /// </summary>
        public int Len { get; set; } = 1;
        /// <summary>
        /// 类型(真(3n)为完整型(由整数个面子组成),假为不完整型(含有雀头、不完整的面子))
        /// </summary>
        public bool Type { get; set; } = true;


        public Block(int loc) => Loc = loc;
        /// <summary>
        /// 筛选完整型Lv.2
        /// </summary>
        /// <param name="op">判断的牌组</param>
        /// <param name="eyesLoc">雀头的序号(-1为没有雀头)</param>
        public bool BlockComplete(Opponent op, int eyesLoc = -1)
        {
            var group = new List<Group> { new(Loc) };
            for (var i = Loc; i < Loc + Len - 1; ++i)       //判断块内每个关系
                if (op.Link(i) == 1)        //当关系是连续
                {
                    group.Last().Len = i + 1 - group.Last().Loc;
                    group.Add(new (i + 1));       //记录多一个组
                }
            group.Last().Len = Loc + Len - group.Last().Loc;
            var groupNum = group.Count;        //包含的组数
            var blockCard = new bool[Len];     //判断块内每张牌,若该牌属于顺子即记为顺(true),否则记为刻(flase)
            for (var i = 0; i < Len; ++i)
                blockCard[i] = true;
            if (eyesLoc != -1)       //有雀头
            {
                ++group[eyesLoc].Confirmed;      //雀头处有两个刻
                ++group[eyesLoc].Confirmed;
                blockCard[group[eyesLoc].Loc - Loc] = false;
                blockCard[group[eyesLoc].Loc - Loc + 1] = false;
            }
            for (var i = 0; i < groupNum; ++i)     //每次循环记录一个组
            {
                switch (group[i].Len - group[i].Confirmed)      //该组牌数
                {
                    case 0:
                        continue;       //开始下一循环
                    case 1:     //都是顺
                        if (groupNum > i + 2)
                        {
                            ++group[i + 1].Confirmed;       //确定后面几组有2张是顺
                            ++group[i + 2].Confirmed;
                            continue;
                        }
                        break;
                    case 2:     //都是顺
                        if (groupNum > i + 2)
                        {
                            ++group[i + 1].Confirmed;       //确定后面几组有2张是顺
                            ++group[i + 1].Confirmed;
                            ++group[i + 2].Confirmed;
                            ++group[i + 2].Confirmed;
                            continue;
                        }
                        break;
                    case 4:     //3刻1顺
                        if (groupNum > i + 2)
                        {
                            ++group[i + 1].Confirmed;       //确定后面几组有2张是顺
                            ++group[i + 2].Confirmed;
                            blockCard[group[i].Loc - Loc] = false;     //三张是刻
                            blockCard[group[i].Loc - Loc + 1] = false;
                            blockCard[group[i].Loc - Loc + 2] = false;
                            continue;
                        }
                        break;
                    case 3:     //都是刻
                        blockCard[group[i].Loc - Loc] = false;     //三张是刻
                        blockCard[group[i].Loc - Loc + 1] = false;
                        blockCard[group[i].Loc - Loc + 2] = false;
                        continue;
                    default:        //可能是负数
                        break;
                }
                Type = false;       //不完整型
                return false;
            }
            return true;
        }
        /// <summary>
        /// 去对(雀头完整型)
        /// </summary>
        /// <param name="op">判断的牌组</param>
        /// <returns>是否听牌</returns>
        public bool BlockIgnorePair(Opponent op)
        {
            for (int i = Loc, tempGroupNum = 0; i < Loc + Len - 1; ++i)
            {
                if (op.Link(i) == 1)        //当关系是连续
                    ++tempGroupNum;       //组数加一
                else if (BlockComplete(op, tempGroupNum))        //当关系是相同,判断是否是雀头完整型
                    return true;        //该组是雀头完整型
            }
            return false;       //不完整
        }
        /// <summary>
        /// 遍历
        /// </summary>
        /// <param name="op">判断的牌组</param>
        /// <param name="mode">假为面子不完整型,真为全不完整型</param>
        /// <returns>是否听牌</returns>
        public bool BlockTraversal(Opponent op, bool mode)
        {
            var first = op[Loc].Val - 1;        //可能的首张牌
            if ((op[Loc].Val & 15) == 0 || op[Loc].Val >> 3 > 5)      //如果首张是一万、筒、索或字牌
                ++first;        //first没有前一张,加回op[loc]
            var last = op[Loc + Len - 1].Val + 1;        //可能的末张牌
            if ((op[Loc + Len - 1].Val & 15) == 8 || op[Loc + Len - 1].Val >> 3 > 5)      //如果末张是九万、筒、索或字牌
                --last;     //last没有后一张,减回op[loc]
            var tempBlock = new Block(0) { Len = Len + 1 };      //新牌组对应的块
            var tempTile = first;
            var tempOp = new Opponent();
            for (var i = 0; i < last - first + 1; ++i, ++tempTile)     //每张牌都插入尝试一次(遍历)
            {
                for (var j = Loc; j < Loc + Len; ++j)       //重新复制所有牌
                    tempOp.Hands.Add(new Tile(op[j].Val));
                tempOp.TileIn(new Tile(tempTile));      //插入尝试的牌
                if (mode && tempBlock.BlockIgnorePair(tempOp)         //(真)全不完整型且遍历、去对后完整
                    || !mode && tempBlock.BlockComplete(tempOp))     //(假)面子不完整型且遍历后完整
                    op.ReadyHands.Add(new Tile(tempTile));
                tempOp.Hands.Clear();
            }
            return op.ReadyHands.Count != 0;       //可能本来它就不为空,不过在这里不影响(将来算符时可能改动)
        }
    }
}
*****
using System.Collections.Generic;
using System.Linq;

namespace Cs.Classes
{
    public class Opponent
    {
        /// <summary>
        /// 手牌区
        /// </summary>
        public List<Tile> Hands { get; } = new(14);

        /// <summary>
        /// 副露区
        /// </summary>
        public List<Meld> Melds { get; } = new(4);

        /// <summary>
        /// 牌河(舍张)
        /// </summary>
        public List<Tile> Discards { get; } = new(30);

        /// <summary>
        /// 可以和的牌
        /// </summary>
        public List<Tile> ReadyHands { get; } = new(13);

        /// <summary>
        /// 是否听牌
        /// </summary>
        public bool IsReadyHand { get; private set; }

        /// <summary>
        /// 打牌
        /// </summary>
        /// <param name="index">打的牌的序号</param>
        /// <param name="now">now</param>
        public void TileOut(int index, ref Tile now)
        {
            now = Hands[index];
            Discards.Add(Hands[index]); //放入牌河
            Hands.RemoveAt(index); //删除元素
        }

        /// <summary>
        /// 摸牌
        /// </summary>
        /// <param name="card">进张</param>
        /// <returns>插入牌的位置</returns>
        public int TileIn(Tile card)
        {
            var ru = 0;
            while (ru < Hands.Count && card.Val > Hands[ru].Val) //找到进张插入的位置
                ++ru;
            Hands.Insert(ru, card); //插入牌
            return ru;
        }

        /// <summary>
        /// 直接访问牌元素
        /// </summary>
        public Tile this[int index]
        {
            get => Hands[index];
            set => Hands[index] = value;
        }

        /// <summary>
        /// 两张手牌间关系
        /// </summary>
        /// <param name="num">前张牌序号</param>
        public int Link(int num)
        {
            try
            {
                return Hands[num + 1].Val - Hands[num].Val;
            }
            catch (System.Exception)
            {
                return int.MaxValue;
            } //(尽量大的数)
        }
        
        /// <summary>
        /// 听牌判断(在摸牌前判断)
        /// </summary>
        public void ReadyHandJudge()
        {
            IsReadyHand = false;
            ReadyHands.Clear(); //清空铳牌

            if (Melds is { Count: 0 }) //如果没有副露(特殊牌型判断)
            {
                //国士牌型判断
                var que = false; //是否缺了某张幺九牌(0或1)
                var duo = false; //是否多了某张幺九牌(0或1)
                var queTile = 0; //缺的幺九牌
                for (var i = 0; i < 13; ++i) //判断十三张幺九牌的拥有情况
                    if (Hands[i].Val == (i + (que ? 1 : 0) - (duo ? 1 : 0)) << 3) ; //如果刚好位置映射
                    else if (Hands[i].Val == (i + (que ? 1 : 0) - (duo ? 1 : 0) - 1) << 3) //如果和上张映射幺九牌一样
                    {
                        if (duo) //如果已经有一个多的牌
                            goto ThirteenOrphans;
                        else duo = true; //记录有多牌
                    }
                    else if (Hands[i].Val == (i + (que ? 1 : 0) - (duo ? 1 : 0) + 1) << 3) //如果和下张映射幺九牌一样
                    {
                        if (que) //如果已经有一个缺牌
                            goto ThirteenOrphans; //不是国士
                        else que = true; //记录有缺牌
                        queTile = i << 3; //记录缺的牌
                    }
                    else goto ThirteenOrphans; //有不是幺九即无听

                if (duo) //若有多张,记听一面或记听一面(中)(中在最后不会被que记录)
                    ReadyHands.Add(que ? new Tile(queTile) : new Tile(96));
                else for (var i = 0; i < 13; ++i) //若不缺张
                    ReadyHands.Add(new Tile(i << 3)); //记听十三面
                IsReadyHand = true;
                return; 
                ThirteenOrphans: //七对牌型判断
                var dan = false; //多出来的单张
                var danTile = 0; //单张牌位置
                for (var i = 0; i < 12; ++i) //判断关系
                    if (((i + (duo ? 1 : 0)) % 2 ^ (Link(i) > 0 ? 1 : 0)) > 0) //如果偶数位关系对应不是相同,或奇数位不是其他
                    {
                        //直接异或运算无法排除龙七对
                        if (Link(i) == 0) //如果这个错误关系是相同
                            goto SevenPairs; //龙七对
                        else if (dan) //如果已经有单牌了
                            goto SevenPairs; //不是七对子
                        else
                        {
                            dan = true; //记录单张
                            danTile = Hands[i].Val; //记录序号
                        }
                    }

                if (!dan) //如果没查到单张
                    danTile = Hands[12].Val; //那单张就是最后一个
                ReadyHands.Add(new Tile(danTile)); //记听一面
                IsReadyHand = true;
                //有可能复合二杯口,故听牌后不退出
            }

            SevenPairs:
            var blockErr = 0; //不完整的块数(最多3个)
            var block = new List<Block>(6) { new Block(0) }; //第一块(最多6块)
            for (var i = 0; i < Hands.Count - 1; ++i)
                if (Link(i) > 1) //当关系不是相同或连续
                {
                    block.Last().Len = i - block.Last().Loc + 1; //记录上一块的长度
                    block.Last().Type = block.Last().Len % 3 <= 0; //筛选完整型Lv.1
                    if (!block.Last().Type) //记录
                        ++blockErr;
                    if (block.Count == 6 - Melds.Count || blockErr == 4) //若块序号达到(6 - 副露数)或有4个不完整型
                        return; //无听
                    block.Add(new Block(i + 1)); //括号里是块内首张牌的序号
                }

            block.Last().Len = Hands.Count - block.Last().Loc; //记录上一块的长度
            block.Last().Type = block.Last().Len % 3 <= 0; //筛选完整型Lv.1
            if (!block.Last().Type) //记录
                ++blockErr;
            if (blockErr == 4) //若有4个不完整型
                return; //无听
            if (block.Where(each => each.Type) //只判断通过完整型Lv.1的
                .Any(each => !each.BlockComplete(this))) //筛选完整型Lv.2,若不完整
                return; //无听
            switch (blockErr)
            {
                case 1: //有一块不完整型(一块全不完整型(3n+4))
                    {
                        foreach (var errBlock in block.Where(errBlock => !errBlock.Type)) //是不完整型
                            if (errBlock.BlockTraversal(this, true))
                                break;
                            else return; //无听
                        break;
                    }
                case 2: //有两块不完整型(一块面子不完整型(3n+2),一块雀头完整型(3n+2))
                    {
                        var errBlock1 = 0;
                        for (; errBlock1 < block.Count; ++errBlock1)
                            if (!block[errBlock1].Type) //是不完整型
                                break; //记录
                        var errBlock2 = errBlock1 + 1; //第二个不完整型
                        for (; errBlock2 < block.Count; ++errBlock2)
                            if (!block[errBlock2].Type) //是不完整型
                                break; //记录
                        if (errBlock2 >= block.Count) //如果后者的数字大于等于组数
                            return; //无听
                        //去对和遍历顺序不能互换,因为如果遍历成功会直接写入铳牌,而去对成功不会写入铳牌,因为&&的短路所以去对写在前面
                        if ((block[errBlock1].BlockTraversal(this, false) && block[errBlock2].BlockIgnorePair(this)) //满足任意一个,但是两条语句都要执行
                            | (block[errBlock2].BlockTraversal(this, false) && block[errBlock1].BlockIgnorePair(this))) //按位或不会出现短路
                            break; //听牌
                        else return; //无听
                    }
                case 3: //有三块不完整型(两块半不完整型(3n+1),一块雀头完整型(3n+2))
                    {
                        var errBlock = 0;
                        for (; errBlock < block.Count; ++errBlock)
                            if (!block[errBlock].Type) //是不完整型
                                if (block[errBlock].Len % 3 == 1) //如果牌数是3的倍数+1
                                    break; //记录errLoc
                        var tempReadyHands = new Tile(Hands[block[errBlock].Loc + block[errBlock].Len - 1].Val + 1); //中间隔的牌(临时记录铳牌)
                        if (tempReadyHands.Val != Hands[block[errBlock + 1].Loc].Val - 1) //如果它和下一张牌不连续
                            return; //无听
                        if (block.Where(eyesBlock => !eyesBlock.Type) //是不完整型
                            .Where(eyesBlock => eyesBlock.Len % 3 == 2) //如果牌数是3的倍数+2(雀头)
                            .Any(eyesBlock => !eyesBlock.BlockIgnorePair(this))) //如果不是雀头块
                            return; //无听
                        var tempOp = new Opponent(); //临时用来判断的牌组
                        var tempBlock = new Block(0) { Len = block[errBlock].Len + 1 + block[errBlock + 1].Len }; //这两个不完整型总长度+一张中间隔的牌
                        var i = 0;
                        for (; i < block[errBlock].Len; ++i)
                            tempOp[i] = Hands[block[errBlock].Loc + i]; //复制该不完整型
                        tempOp[i] = tempReadyHands; //记录中间的牌
                        for (; i < tempBlock.Len; ++i)
                            tempOp[i] = Hands[block[errBlock].Loc + i]; //复制该不完整型
                        if (tempBlock.BlockComplete(tempOp)) //如果该牌组完整
                            ReadyHands.Add(tempReadyHands); //记听一面
                        break;
                    }
            }
            IsReadyHand = true; //听牌
        }
    }
}

规则参考

[1] 日麻百科
[2] 资深麻友
[3] 雀魂麻将
[4] 雀姬麻将

原创文章,转载请标明出处,如有谬误欢迎各位指正
欢迎交流麻将算法,QQ:2639914082
  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值