题目大意:给定一副不全的麻将,从中摸取 14 张,求和牌的概率,牌型包括一般牌型/七対子/国士無双
这傻逼题我从昨天中午写到现在……
答案是分数形式,我们求出和牌的方案数和总方案数 C14n ,约分一下即可
比较好的一件事情就是 C14n≈4.25∗1018 ,刚好不爆long long
由于和牌牌型分为三类,所以一般的思路是分别计算三类牌型的概率,然后加起来,但是这样不对,因为一般牌型和七対子是有交集的,叫做二盃口……
一盃口:1番门前役,牌型中存在两幅同色同数的顺子,称作一盃口
例:1m 1m 2m 2m 3m 3m 5m 5m 5m 7m 7m 7m 9m 9m
牌型中的1m 1m 2m 2m 3m 3m即为一盃口
二盃口:3番门前役,牌型中存在两幅一盃口,称作二盃口
例:1m 1m 2m 2m 3m 3m 5m 5m 6m 6m 7m 7m 9m 9m
容易发现如果二盃口中没有重叠(即不存在某张牌出现了四张,像1m 1m 2m 2m 3m 3m 3m 3m 4m 4m 5m 5m 7m 7m这样的形式)那么二盃口既满足七対子的形式又满足一般牌型的形式,要从答案中减去
故答案=国士無双+七対子+一般牌型-不重叠的二盃口
国士無双:DP
七対子:DP
二盃口:DP/爆枚,如果用DP的话注意同种牌型的不同解释方式,例如1m 1m 2m 2m 3m 3m 4m 4m 5m 5m 6m 6m 7m 7m有三种解释方式,但是只应被统计一次
然后就是这该死的一般牌型了……
容易想到朴素的DP: fi,j,k,cnt1,cnt2 表示当前在第 i 张牌,凑成了 j 组面子, k=0/1 表示有/无将牌,此时第 i 张牌还剩 cnt1 张未匹配,第 i−1 张牌还剩 cnt2 张未匹配的方案数
但是这样DP出来的结果完全不对,因为同一牌型可能有不同解释方式,像3m 3m 3m 4m 4m 4m 5m 5m 5m 6m 6m 8m 8m 8m可以解释成(3m 3m 3m)(4m 4m 4m)(5m 5m 5m)(8m 8m 8m)(6m 6m)或(3m 4m 5m)(4m 5m 6m)(4m 5m 6m)(8m 8m 8m)(3m 3m),并且一种牌型的解释方式可能纷繁复杂,我们没办法像七対子 ∩ 一般牌型 = 不重叠的二盃口那样容斥
然后我考虑了下搜索,搜索的好处就是可以哈希判重,很好地回避了这个问题,而且如果只搜面子和将牌的话搜索量不会太大
然后我测了下,一般牌型的牌型数量是1000W左右,加上搜索以及哈希表的一大坨常数,MLE+TLE到死,极限数据单组4s,玩个卵
然后我想了下meet-in-the-meedle,发现还是没有什么好办法回避多种解释方式的问题
DP时间复杂度优秀,但是有BUG;搜索可以避免这个BUG,但是T到死,那么我们考虑结合一下这俩算法
我们把连在一起的牌称作一个连通块,例如1m 1m 2m 2m 3m 3m 5m 5m 6m 6m 7m 7m 8m 8m这组牌就可以分成{1m 1m 2m 2m 3m 3m}{5m 5m 6m 6m 7m 7m 8m 8m}两个连通块
容易发现同种牌型不同解释方式的问题只会在连通块内部出现,那么我们可以搜索连通块,然后用DP把连通块组合起来
考虑到连通块的长度最大为 9 ,因此搜索量不会超过 49=262144
然后就好办了
连通块中一张牌可能出现 1,2,3,4 次,我们用一个四进制数表示
搜索得到 cnti,j,k,sta 表示长度为 k ,状态为 sta 的连通块,其中包含 i 组面子和 j 组将牌的方案数
然后对 cnti,j,k 跑一个子集和变换得到 gi,j,k,sta 表示从 k 张连续的,状态为 sta 的牌中选出一个长度为 k ,包含 i 组面子和 j 组将牌的连通块的方案数
然后……然后直接DP就行了
时间复杂度很玄学但是跑的很快- -
/* - - 1m 2m 3m 4m 5m 6m 7m 8m 9m - 1s 2s 3s 4s 5s 6s 7s 8s 9s - 1p 2p 3p 4p 5p 6p 7p 8p 9p - 1c - 2c - 3c - 4c - 5c - 6c - 7c */
#include <map>
#include <string>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
int cnt[50],used[50];
long long C[140][20];
map<string,int> pos;
long long g[5][2][10][1<<18];
bool flag[10][1<<18];
void DFS(int i,int j,int k,int max_val,int hash)
{
int temp=((1<<2*max_val)-1)/3;
if(max_val && !flag[max_val][hash-temp])
flag[max_val][hash-temp]=true,g[j][k][max_val][hash-temp]++;
if(i>9) return ;
int l;
//啥也不凑
if(used[i]) DFS(i+1,j,k,max_val,hash);
//只凑顺子
if(i<=7)
for(l=1;l<=4-used[i] && l<=4-j;l++)
{
used[i]+=l;used[i+1]+=l;used[i+2]+=l;
DFS(i+1,j+l,k,i+2,hash+(l<<(i-1)*2)+(l<<(i)*2)+(l<<(i+1)*2));
used[i]-=l;used[i+1]-=l;used[i+2]-=l;
}
//将牌+顺子
if(!k)
{
if(used[i]<=4-2)
{
used[i]+=2;
DFS(i+1,j,k+1,max(max_val,i),hash+(2<<(i-1)*2));
used[i]-=2;
}
if(i<=7)
for(l=1;l<=4-2-used[i] && l<=4-j;l++)
{
used[i]+=l+2;used[i+1]+=l;used[i+2]+=l;
DFS(i+1,j+l,k+1,i+2,hash+(l+2<<(i-1)*2)+(l<<(i)*2)+(l<<(i+1)*2));
used[i]-=l+2;used[i+1]-=l;used[i+2]-=l;
}
}
//刻子+顺子
if(j!=4)
{
if(used[i]<=4-3)
{
used[i]+=3;
DFS(i+1,j+1,k,max(max_val,i),hash+(3<<(i-1)*2));
used[i]-=3;
}
if(i<=7)
for(l=1;l<=4-3-used[i] && l<=4-1-j;l++)
{
used[i]+=l+3;used[i+1]+=l;used[i+2]+=l;
DFS(i+1,j+l+1,k,i+2,hash+(l+3<<(i-1)*2)+(l<<(i)*2)+(l<<(i+1)*2));
used[i]-=l+3;used[i+1]-=l;used[i+2]-=l;
}
}
}
void Pretreatment()
{
int i,j,k,l1,l2,l3;
C[0][0]=1;
for(i=1;i<=136;i++)
{
C[i][0]=1;
for(j=1;j<=14;j++)
C[i][j]=C[i-1][j]+C[i-1][j-1];
}
pos["1m"]=3;
pos["2m"]=4;
pos["3m"]=5;
pos["4m"]=6;
pos["5m"]=7;
pos["6m"]=8;
pos["7m"]=9;
pos["8m"]=10;
pos["9m"]=11;
pos["1s"]=13;
pos["2s"]=14;
pos["3s"]=15;
pos["4s"]=16;
pos["5s"]=17;
pos["6s"]=18;
pos["7s"]=19;
pos["8s"]=20;
pos["9s"]=21;
pos["1p"]=23;
pos["2p"]=24;
pos["3p"]=25;
pos["4p"]=26;
pos["5p"]=27;
pos["6p"]=28;
pos["7p"]=29;
pos["8p"]=30;
pos["9p"]=31;
pos["1c"]=33;
pos["2c"]=35;
pos["3c"]=37;
pos["4c"]=39;
pos["5c"]=41;
pos["6c"]=43;
pos["7c"]=45;
DFS(1,0,0,0,0);
for(i=0;i<=4;i++)
for(j=0;j<=1;j++)
for(k=1;k<=9&&k<=i*3+j;k++)
for(l1=0;l1<k*2;l1+=2)
for(l2=(1<<k*2)-1;~l2;l2--)
{
int temp=(l2>>l1)&3;
for(l3=0;l3<temp;l3++)
g[i][j][k][l2]+=g[i][j][k][l2^(temp<<l1)^(l3<<l1)]*C[temp+1][l3+1];
}
}
long long Calculate1()//国士無双
{
static const char s[][10]={"","1m","9m","1s","9s","1p","9p","1c","2c","3c","4c","5c","6c","7c"};
static long long f[14][2];//f[i][j]表示当前选到第i张牌,有/无对子的方案数
int i;
memset(f,0,sizeof f);f[0][0]=1;
for(i=1;i<=13;i++)
{
f[i][0]=f[i-1][0]*C[cnt[pos[s[i]]]][1];
f[i][1]=f[i-1][0]*C[cnt[pos[s[i]]]][2]+f[i-1][1]*C[cnt[pos[s[i]]]][1];
}
return f[13][1];
}
long long Calculate2()//七対子
{
static long long f[46][8];//f[i][j]表示当前在第i张牌,凑成了j对的方案数
int i,j;
memset(f,0,sizeof f);f[0][0]=1;
for(i=1;i<=45;i++)
{
f[i][0]=f[i-1][0];
for(j=1;j<=7;j++)
f[i][j]=f[i-1][j]+f[i-1][j-1]*C[cnt[i]][2];
}
return f[45][7];
}
long long Calculate3()//一般牌型
{
static long long f[46][5][2];
//f[i][j][k]表示当前在第i张牌,凑成了j组面子,有/无将牌的方案数
int i,j,k,l1,l2,l3;
memset(f,0,sizeof f);f[0][0][0]=1;
for(i=1;i<=45;i++)
for(j=0;j<=4;j++)
for(k=0;k<=1;k++)
{
f[i][j][k]=f[i-1][j][k];
int temp=0;
for(l1=1;l1<=9 && cnt[i-l1+1];l1++)
{
temp^=cnt[i-l1+1]-1<<(l1-1)*2;
for(l2=0;l2<=j;l2++)
for(l3=0;l3<=k;l3++)
f[i][j][k]+=f[i-l1-1][l2][l3]*g[j-l2][k-l3][l1][temp];
}
}
return f[45][4][1];
}
long long Calculate4()//无重张的二盃口
{
static long long f[46][3][2];
//f[i][j][k]表示当前在第i张牌,凑出了j副一杯口,有/无将牌的方案数
int i,j,k,l;
memset(f,0,sizeof f);f[0][0][0]=1;
for(i=1;i<=45;i++)
for(j=0;j<=2;j++)
for(k=0;k<=1;k++)
{
f[i][j][k]=f[i-1][j][k];
long long temp=1;
for(l=1;l<=7;l++)
{
temp*=C[cnt[i-l+1]][2];
if(!temp) break;
switch(l)
{
case 1:if(k)f[i][j][k]+=f[i-l-1][j][k-1]*temp;break;
case 3:if(j)f[i][j][k]+=f[i-l-1][j-1][k]*temp;break;
case 4:if(j&&k)f[i][j][k]+=f[i-l-1][j-1][k-1]*temp;break;
case 6:if(j>=2)f[i][j][k]+=f[i-l-1][j-2][k]*temp;break;
case 7:if(j>=2&&k)f[i][j][k]+=f[i-l-1][j-2][k-1]*temp;break;
}
}
}
return f[45][2][1];
}
int main()
{
int T,n,i;
char p[10];
Pretreatment();
for(cin>>T;T;T--)
{
memset(cnt,0,sizeof cnt);
scanf("%d",&n);
for(i=1;i<=n;i++)
scanf("%s",p),cnt[pos[p]]++;
long long a=Calculate1()+Calculate2()+Calculate3()-Calculate4();
long long b=C[n][14];
long long gcd=__gcd(a,b);
a/=gcd;b/=gcd;
printf("%lld/%lld\n",a,b);
//cout<<_<<endl;
}
return 0;
}