题面
题意
共有n种麻将牌,给出一开始的13张麻将牌,问期望摸上来几张牌后,与开始的13张牌组合后存在一个大小为14的能和的子集.
做法
因为要求期望的摸牌次数,我们可以将这个次数转化为
∑
i
=
13
4
∗
n
p
(
i
)
,
p
(
i
)
\sum_{i=13}^{4*n}p(i),p(i)
∑i=134∗np(i),p(i)表示总共有i张牌后仍然没和的概率,而
p
(
i
)
=
c
n
t
(
i
)
/
A
4
∗
n
−
13
i
−
13
,
c
n
t
(
i
)
p(i)=cnt(i)/A_{4*n-13}^{i-13},cnt(i)
p(i)=cnt(i)/A4∗n−13i−13,cnt(i)表示总共有i张牌后仍然没和的排列数量.
为了求这个值,我们需要记录此时已有的牌的状态来判断它是否能和.现在我们考虑如何判断一个集合是否能和.记
d
p
[
i
]
[
j
]
[
k
]
dp[i][j][k]
dp[i][j][k]表示当前考虑到第i种牌,以
i
−
2
i-2
i−2为开头的顺子有
j
j
j个,以
i
−
1
i-1
i−1为开头的顺子有
k
k
k个时的最大面子数,因为顺子数量不可能超过2(否则可以转化为3个刻子),因而后两维的大小都是3.
用上述dp可以记录一个集合中的最大面子数,这样一个集合是否能和就只要记录三个量:
c
t
ct
ct(当前对子数)
A
[
3
]
[
3
]
,
B
[
3
]
[
3
]
A[3][3],B[3][3]
A[3][3],B[3][3]分别表示此时无对子和有对子的最大面子数,这样的集合的种类其实很少.
然后考虑如何求
c
n
t
(
i
)
cnt(i)
cnt(i),可以设计第二个dp,
d
p
[
i
]
[
j
]
[
k
]
dp[i][j][k]
dp[i][j][k]表示当前考虑到第i种牌,此时牌的集合的状态为j,已经摸了k张牌的方案数,然后只要枚举第
i
+
1
i+1
i+1种牌摸了几张牌即可转移:
d
p
[
i
+
1
]
[
t
r
a
n
s
(
j
,
t
)
]
[
k
+
z
]
+
=
d
p
[
i
]
[
j
]
[
k
]
∗
A
4
−
y
l
[
i
+
1
]
z
−
y
l
[
i
+
1
]
∗
C
k
+
z
−
q
z
[
i
+
1
]
z
−
y
l
[
i
+
1
]
;
dp[i+1][trans(j,t)][k+z]+=dp[i][j][k]*A_{4-yl[i+1]}^{z-yl[i+1]}*C_{k+z-qz[i+1]}^{z-yl[i+1]};
dp[i+1][trans(j,t)][k+z]+=dp[i][j][k]∗A4−yl[i+1]z−yl[i+1]∗Ck+z−qz[i+1]z−yl[i+1];
z
z
z表示摸了几张第
i
+
1
i+1
i+1种牌,
y
l
[
i
]
yl[i]
yl[i]表示开始13张牌中有几张
i
i
i,
q
z
[
i
]
qz[i]
qz[i]为
y
l
[
i
]
yl[i]
yl[i]的前缀和.
为了节约空间,可以用滚动数组.
代码
#include<bits/stdc++.h>
#define ll long long
#define N 110
#define MN 400
#define MM 1610
#define M 998244353
using namespace std;
ll n,ans,cm,yl[N],qz[N],to[MM][5],jc[N*4],nj[N*4],dp[N][MM][4*N];
inline void Min(ll &u,ll v){if(v<u) u=v;}
inline void Max(ll &u,ll v){if(v>u) u=v;}
inline void Add(ll &u,ll v){u=(u+v)%M;}
struct Zt
{
ll dp[3][3];
Zt(){memset(dp,-1,sizeof(dp));}
void init(){memset(dp,-1,sizeof(dp));}
bool operator < (const Zt &u) const
{
ll i,j;
for(i=0; i<3; i++)
{
for(j=0; j<3; j++)
{
if(dp[i][j]!=u.dp[i][j])
return dp[i][j]<u.dp[i][j];
}
}
return 0;
}
bool operator != (const Zt &u) const
{
ll i,j;
for(i=0; i<3; i++)
{
for(j=0; j<3; j++)
{
if(dp[i][j]!=u.dp[i][j])
return 1;
}
}
return 0;
}
Zt tran(ll u)
{
ll i,j,k;
Zt res;
for(i=0; i<3; i++)
{
for(j=0; j<3; j++)
{
if(dp[i][j]==-1) continue;
for(k=0; k<=min(2ll,u-i-j); k++)
{
Max(res.dp[j][k],dp[i][j]+k+(u-i-j-k)/3);
Min(res.dp[j][k],4ll);
}
}
}
return res;
}
};
inline Zt max(Zt u,Zt v)
{
ll i,j;
Zt res;
for(i=0; i<3; i++)
{
for(j=0; j<3; j++)
{
res.dp[i][j]=max(u.dp[i][j],v.dp[i][j]);
}
}
return res;
}
struct Mj
{
Zt A,B;
ll cnt;
bool hu;
Mj(){cnt=hu=0;}
bool operator < (const Mj &u) const
{
if(cnt!=u.cnt) return cnt<u.cnt;
if(A!=u.A) return A<u.A;
return B<u.B;
}
Mj tran(ll u)
{
ll i,j,k;
Mj res;
res.B=B.tran(u);
res.cnt=cnt;
if(u>=2) res.cnt=min(res.cnt+1,7ll),res.B=max(res.B,A.tran(u-2));
res.A=A.tran(u);
if(res.cnt==7 || res.B.dp[0][0]==4) res.hu=1;
return res;
}
} st,mj[MM];
map<Mj,ll>mm;
inline ll A(ll u,ll v){return jc[u]*nj[u-v]%M;}
inline ll C(ll u,ll v){return jc[u]*nj[v]%M*nj[u-v]%M;}
inline ll po(ll u,ll v)
{
ll res=1;
for(; v;)
{
if(v&1) res=res*u%M;
u=u*u%M;
v>>=1;
}
return res;
}
inline ll in(Mj u)
{
mm[u]=++cm;
mj[cm]=u;
return cm;
}
ll dfs(Mj now)
{
if(mm.count(now)) return mm[now];
if(now.hu) return 0;
ll i,res=in(now);
for(i=0; i<=4; i++) to[res][i]=dfs(now.tran(i));
return res;
}
int main()
{
st.A.dp[0][0]=0;
ll i,j,k,z,p;
jc[0]=1;
for(i=1; i<=MN; i++) jc[i]=jc[i-1]*i%M;
nj[MN]=po(jc[MN],M-2);
for(i=MN-1; i>=0; i--) nj[i]=nj[i+1]*(i+1)%M;
dfs(st);
cin>>n;
for(i=1; i<=13; i++)
{
scanf("%lld%*d",&p);
yl[p]++;
}
for(i=1; i<=n; i++) qz[i]=qz[i-1]+yl[i];
dp[0][mm[st]][0]=1;
for(i=0; i<n; i++)
{
for(j=1; j<=cm; j++)
{
for(k=0; k<=4*i; k++)
{
if(!dp[i][j][k]) continue;
for(z=yl[i+1]; z<=4; z++)
{
ll t=to[j][z];
if(!t) continue;
Add(dp[i+1][t][k+z],dp[i][j][k]*A(4-yl[i+1],z-yl[i+1])%M*C(k+z-qz[i+1],z-yl[i+1])%M);
}
}
}
}
for(i=13; i<4*n; i++)
{
ll sum=0;
for(j=1; j<=cm; j++)
{
if(mj[j].hu) continue;
Add(sum,dp[n][j][i]);
}
sum=sum*po(A(4*n-13,i-13),M-2)%M;
Add(ans,sum);
}
cout<<ans;
}