【ZJOI2019】【LOJ3042】【洛谷P5279】麻将(DP)(自动机)

本文深入探讨了麻将胡牌算法的实现,通过动态规划的方法计算胡牌概率。文章详细介绍了如何利用状态压缩和自动机来优化算法,以及如何处理面子和对子的计算。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

洛谷传送门

LOJ传送门


解析:

首先明确一点,我们要求的是这个东西:

∑ i i × P ( 胡 牌 最 小 寻 目 数 等 于 i ) \sum_{i}i\times P(胡牌最小寻目数等于i) ii×P(i)

其实稍微动一下脑子可以发现要求的其实是 ∑ i P ( 胡 牌 的 最 小 寻 目 数 大 于 i ) \sum_{i}P(胡牌的最小寻目数大于i) iP(i)

我们计算拿所有张数的牌时候无法胡牌的情况就行了。

然后,怎么判断能否胡牌?

首先看面子怎么算。接下来顺子指三张连着的牌,三条指三张相同的牌。

我们发现三组相同的顺子和三组连着的三条没有任何区别。

强行规定相同类型的顺子只能出现至多两组。

d p [ i ] [ j ] dp[i][j] dp[i][j]表示当前已经考虑了前 k k k种牌,第 k − 1 k-1 k1种牌开头的顺子至多有 i i i组,以第 k k k种牌开头的顺子至多有 j j j组的时候,所能组成的最多的面子数。(不记录 k k k,没有必要)。

现在考虑新增加了一种牌,有 b b b张,有很显然的转移,第 k − 1 k-1 k1种牌开头的顺子能够产生实际贡献了,需要加上,然后剩下的牌除了顺子里面的全部考虑变成三条。

转移封装起来,就是代码里面第 44 44 44行到第 51 51 51行的DP了。

现在考虑怎么算七对子。

显然压两种情况,考虑对子放在面子里面(不作为对子产生贡献),考虑对子拿出来(贡献对子数)。

将状态压成三个东西,对子塞进面子里面的dp状态,对子拿出来考虑的dp状态,最多能够组成的对子数。

所有不同的状态只有3956种。

然后建立自动机,表示当前状态下,下一种牌有 i i i张转移到的状态。

暴力DP,顺便滚动数组优化。

然后按照第二个式子,算每种情况下不胡的概率。


代码:

#include<bits/stdc++.h>
#define ll long long
#define re register
#define gc getchar
#define cs const

namespace IO{
	inline int getint(){
		re char c;
		while(!isdigit(c=gc()));re int num=c^48;
		while(isdigit(c=gc()))num=(num+(num<<2)<<1)+(c^48);
		return num;
	}
}
using namespace IO;

using std::cout;
using std::cerr;

struct node{
	int dp[3][3];
	node(){memset(dp,-1,sizeof dp);}
	
	int *operator[](int offset){return dp[offset];}
	cs int *operator[](int offset)cs{return dp[offset];}

};
cs node unit=node();

inline bool operator<(cs node &a,cs node &b){
	for(int re i=0;i<3;++i)
	for(int re j=0;j<3;++j)if(a[i][j]!=b[i][j])return a[i][j]<b[i][j];
	return false;
}

inline node operator+(cs node &a,cs node &b){
	node res=unit;
	for(int re i=0;i<3;++i)
	for(int re j=0;j<3;++j)
	res[i][j]=std::max(a[i][j],b[i][j]);
	return res;
}

inline node operator+(cs node &a,cs int &b){
	node res=unit;
	for(int re i=0;i<3&&i<=b;++i)
	for(int re j=0;j<3&&i+j<=b;++j)if(~a[i][j]){
		for(int re k=0;k<3&&i+j+k<=b;++k)
		res[j][k]=std::max(res[j][k],std::min(4,a[i][j]+i+(b-i-j-k)/3));
	}
	return res;
}

node starter(){
	node s=unit;
	s[0][0]=0;
	return s;
}

struct mj{
	node n,p;
	int c;
	mj():n(starter()),p(unit),c(0){}
	mj(cs node &_n,cs node &_p,cs int &_c):n(_n),p(_p),c(_c){}
	
	friend bool operator<(cs mj &a,cs mj &b){
		if(a.c!=b.c)return a.c<b.c;
		if(a.n<b.n)return true;
		if(b.n<a.n)return false;
		return a.p<b.p;
	}
	
	friend mj operator+(cs mj &a,cs int &b){
		if(b>=2)return mj(a.n+b,(a.n+(b-2))+(a.p+b),std::min(a.c+1,7));
		return mj(a.n+b,a.p+b,a.c);
	}
};

cs int SIZE=3957;

std::map<mj,int> id;
mj state[SIZE];
int tr[SIZE][5],tot;
bool ok[SIZE];

inline bool check(cs mj &state){
	if(state.c==7)return true;
	for(int re i=0;i<3;++i)
	for(int re j=0;j<3;++j)
	if(state.p[i][j]==4)return true;
	return false;
}

int dfs(cs mj &now){
	if(id.count(now))return id[now];
	id[now]=++tot;
	int t=tot;
	state[tot]=now;
	ok[tot]=check(now);
	for(int re i=0;i<=4;++i)
	tr[t][i]=dfs(now+i);
	return t;
}

cs int mod=998244353;
inline int add(int a,int b){return a+b>=mod?a+b-mod:a+b;}
inline void Inc(int &a,int b){(a+=b)>=mod?a-=mod:a;}
inline int dec(int a,int b){return a<b?a-b+mod:a-b;}
inline int mul(int a,int b){return (ll)a*b%mod;}
inline int quickpow(int a,int b,int res=1){
	while(b){
		if(b&1)res=mul(res,a);
		a=mul(a,a);
		b>>=1;
	}
	return res;
}

cs int N=101;

int n,ans;

int dp[2][N<<2][SIZE];
int used[N];
int c[5][5]={
	{1},
	{1,1},
	{1,2,1},
	{1,3,3,1},
	{1,4,6,4,1}
};

signed main(){
	dfs(mj());
	n=getint();
	for(int re i=1;i<=13;++i)++used[getint()],getint();
	dp[0][0][1]=1;int now=1;
	for(int re i=1;i<=n;++i,now^=1,memset(dp[now],0,sizeof dp[now]))
	for(int re j=0;j<=4*(i-1);++j)
	for(int re k=0;k<=tot;++k)if(dp[now^1][j][k])
	for(int re t=used[i];t<5;++t)
	Inc(dp[now][j+t][tr[k][t]],mul(dp[now^1][j][k],c[4-used[i]][t-used[i]]));
	now^=1;
	for(int re i=13;i<=n*4;++i){
		int sum=0,fail=0;
		for(int re j=1;j<=tot;++j){
			Inc(sum,dp[now][i][j]);
			if(!ok[j])Inc(fail,dp[now][i][j]);
		};
		Inc(ans,mul(fail,quickpow(sum,mod-2)));
	}
	cout<<ans<<"\n";
	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值