[ZJOI2019]麻将(动态规划,自动机)

题面

洛谷

题解

先做一点小铺垫,对于一堆牌而言,我们只需要知道这\(n\)张牌分别出现的次数就行了,即我们只需要知道一个长度为\(n\)的串就可以了。

首先考虑如何判断一副牌是不是能胡。

出现了七对牌的情况很容易特判处理掉,只需要考虑第一种情况。

那么我们考虑\(dp\)来判断,设\(f[i][j][k][0/1]\)表示的当前考虑到了这个字符串的第\(i\)位,即考虑到了第\(i\)种牌,\(i-1,i,i+1\)的对子要用\(j\)次,\(i,i+1,i+2\)的对子要用\(k\)次,是否已经出现了一个对子。而这个\(dp\)值表示的是能够留下的最大的面子数量。不难发现\(j,k\)都不会超过\(2\)。

那么转移的时候相当于读进来当前的\(i\)有多少个,假设是\(x\),接下来\(x\)减去\(j+k\)组成顺子,然后枚举一下以多少\(x\)为开头组成顺子。这里再枚举一下是否用当前的\(x\)组成对子或者刻字。

我们把第一维丢掉,只考虑剩下的\(18\)个元素和最大的可能对子数,并且强制\(dp\)值不超过\(4\),最大对子数不超过\(7\)。这样子就会存在大量重复的状态,打表可得状态只有不到\(2100\)种。

那么我们可以提前把所有状态全部预处理出来,预处理对于当前的一个状态,插入后面一种牌\(x\)张的结果,这样子就构成了一个自动机,那么我们只需要从头到尾把一种状态插入进去就可以知道有没有胡牌。

那么此时我们只需要知道抽了\(i\)张之后还未胡牌的概率,全部累加就是答案。

考虑在自动机上\(dp\),设\(f[i][p][k]\)表示当前考虑到第\(i\)种牌,且当前在自动机的\(p\)位置上,前面一共抽了\(k\)张牌且还没有胡的方案数。

转移的时候枚举这张牌用了多少次,用组合数带进去进行计算,通过自动机进行状态的转移。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<map>
using namespace std;
#define MOD 998244353
#define MAX 402
void add(int &x,int y){x+=y;if(x>=MOD)x-=MOD;}
inline int read()
{
int x=0;bool t=false;char ch=getchar();
while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
if(ch=='-')t=true,ch=getchar();
while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
return t?-x:x;
}
struct Data
{
int f[18],cnt;
void init(){memset(f,-1,sizeof(f));f[0]=cnt=0;}
bool check()
{
if(cnt>=7)return true;
for(int i=0;i<3;++i)
for(int j=0;j<3;++j)
if(f[9+i*3+j]>=4)return true;
return false;
}
}QwQ,ST[2100];
bool operator<(Data a,Data b)
{
if(a.cnt!=b.cnt)return a.cnt<b.cnt;
for(int i=0;i<18;++i)if(a.f[i]!=b.f[i])return a.f[i]<b.f[i];
return false;
}
Data Trans(Data a,int b)
{
Data c;c.init();c.cnt=min(a.cnt+(b>=2),7);
for(int i=0;i<3;++i)
for(int j=0;j<3;++j)
{
if(~a.f[i*3+j])
{
for(int k=0;k<3&&i+j+k<=b;++k)
c.f[j*3+k]=max(c.f[j*3+k],min(a.f[i*3+j]+i+(b-i-j-k>=3),4));
if(b>=2)
for(int k=0;k<3&&i+j+k<=b-2;++k)
c.f[9+j*3+k]=max(c.f[9+j*3+k],min(a.f[i*3+j]+i,4));
}
if(~a.f[9+i*3+j])
{
for(int k=0;k<3&&i+j+k<=b;++k)
c.f[9+j*3+k]=max(c.f[9+j*3+k],min(a.f[9+i*3+j]+i+(b-i-j-k>=3),4));
}
}
return c;
}
map<Data,int> M;int tot;
void Build(Data x)
{
if(x.check())return;
if(M.find(x)!=M.end())return;
ST[M[x]=++tot]=x;
for(int i=0;i<=4;++i)Build(Trans(x,i));
}
int jc[MAX],jv[MAX],inv[MAX];
int f[2][2100][MAX];
int C(int n,int m){return 1ll*jc[n]*jv[m]%MOD*jv[n-m]%MOD;}
int n,ans,s[MAX],tr[2100][5];
int main()
{
QwQ.init();Build(QwQ);
jc[0]=jv[0]=inv[0]=inv[1]=1;
for(int i=2;i<MAX;++i)inv[i]=1ll*inv[MOD%i]*(MOD-MOD/i)%MOD;
for(int i=1;i<MAX;++i)jc[i]=1ll*jc[i-1]*i%MOD;
for(int i=1;i<MAX;++i)jv[i]=1ll*jv[i-1]*inv[i]%MOD;
n=read();for(int i=1;i<=13;++i)s[read()]+=1,read();
for(int i=1;i<=tot;++i)for(int j=0;j<=4;++j)tr[i][j]=M[Trans(ST[i],j)];
f[0][1][0]=1;
for(int i=1,ss=0,nw=1,pw=0;i<=n;ss+=s[i],++i,nw^=1,pw^=1)
{
memset(f[nw],0,sizeof(f[nw]));
for(int j=1;j<=tot;++j)
for(int k=s[i];k<=4;++k)
{
if(!tr[j][k])continue;
int w=1ll*C(4-s[i],k-s[i])*jc[k-s[i]]%MOD;
for(int l=0;l<=n*4-k;++l)
if(f[pw][j][l])
add(f[nw][tr[j][k]][k+l],1ll*f[pw][j][l]*w%MOD*C(k+l-ss-s[i],k-s[i])%MOD);
}
}
for(int i=13,val=1;i<=n*4;val=1ll*val*inv[n*4-i]%MOD,++i)
{
int ret=0;
for(int j=1;j<=tot;++j)add(ret,f[n&1][j][i]);
add(ans,1ll*ret*val%MOD);
}
printf("%d\n",ans);
return 0;
}

[ZJOI2019]麻将(动态规划,自动机)的更多相关文章

  1. [ZJOI2019]麻将

    这是一道麻将自动机的模板题(雾 其实这是一道dp套dp借助自动机实现的麻将好题! 首先把期望转化一下,拆成sigema p(x>i) 现在要计算i张牌不胡的概率,也就等价于计算i张牌不胡的方案数 ...

  2. 【洛谷5279】[ZJOI2019] 麻将(“胡牌自动机”上DP)

    点此看题面 大致题意: 给你13张麻将牌,问你期望再摸多少张牌可以满足存在一个胡的子集. 似乎ZJOI2019Day1的最大收获是知道了什么是胡牌? 一个显然的性质 首先我们要知道一个显然的性质,即对 ...

  3. [ZJOI2019]麻将(DP+有限状态自动机)

    首先只需要考虑每种牌出现的张数即可,然后判断一副牌是否能胡,可以DP一下,令f[i][j][k][0/1]表示到了第i位,用j次i-1,i,i+1和k次i,i+1,i+2,是否出现对子然后最大的面子数 ...

  4. Luogu P5279 [ZJOI2019]麻将

    ZJOI2019神题,间接送我退役的神题233 考场上由于T2写挂去写爆搜的时候已经没多少时间了,所以就写挂了233 这里不多废话直接开始讲正解吧,我们把算法分成两部分 1.建一个"胡牌自动 ...

  5. 【题解】Luogu P5279 [ZJOI2019]麻将

    原题传送门 希望这题不会让你对麻将的热爱消失殆尽 我们珂以统计每种牌出现的次数,不需要统计是第几张牌 判一副牌能不能和,类似这道题 对于这题: 设\(f[i][j][k][0/1]\)表示前\(i\) ...

  6. 洛谷P5279 [ZJOI2019]麻将

    https://www.luogu.org/problemnew/show/P5279 以下为个人笔记,建议别看: 首先考虑如何判一个牌型是否含有胡的子集.先将牌型表示为一个数组num,其中num[i ...

  7. 洛谷 P5279 - [ZJOI2019]麻将(dp 套 dp)

    洛谷题面传送门 一道 dp 套 dp 的 immortal tea 首先考虑如何判断一套牌是否已经胡牌了,考虑 \(dp\)​​​​​.我们考虑将所有牌按权值大小从大到小排成一列,那我们设 \(dp_ ...

  8. 洛谷P5279 [ZJOI2019]麻将(乱搞+概率期望)

    题面 传送门 题解 看着题解里一堆巨巨熟练地用着专业用语本萌新表示啥都看不懂啊--顺便\(orz\)余奶奶 我们先考虑给你一堆牌,如何判断能否胡牌 我们按花色大小排序,设\(dp_{0/1,i,j,k ...

  9. 题解 洛谷 P5279 【[ZJOI2019]麻将】

    这题非常的神啊...蒟蒻来写一篇题解. Solution 首先考虑如何判定一副牌是否是 "胡" 的. 不要想着统计个几个值 \(O(1)\) 算,可以考虑复杂度大一点的. 首先先把 ...

随机推荐

  1. EFCore使用JSON_VALUE查询json对象的值

    EFCore使用JSON_VALUE查询json对象的值 Intro SqlServer 从2016开始支持 JSON 操作,可以使用 JSON_VALUE 查询 JSON 对象的某个属性值,更多介绍 ...

  2. Hadoop3新特性

    1.添加Classpath isolation,防止不同版本的jar包出现冲突. 2.支持Shell重写. 3.支持HDFS中的擦除编码[Erasure Encoding],默认的EC策略可以节省50 ...

  3. MongoDB 3.6版本关于bind_ip设置

    2017年下半年新发布的MongoDB 3.6版本在安全性上做了很大提升,主要归结为两点: 1.将将bind_ip 默认值修改为了localhost: 2. 在db.createUser()和 db. ...

  4. 一文把samba相关的都说清楚

    1.前言 samba源码都一样,配置也也一样,各个不同linux版本,唯一不同的是对服务的启动方式不同.下面以ubuntu14.4为例,说明. 2. 安装samba samba的安装,可以源码安装,大 ...

  5. python3 多线程的使用

    示例1: import threadingfrom time import sleep class forThread(threading.Thread): def __init__(self, ev ...

  6. zabbix忘记admin登录密码重置密码

    问题描述: 有时候忘记admin的密码了,因为账号太多 解决方案: 1.zabbix连接的是mysql数据库 [root@localhost /]# mysql -uroot -pAbc123 #-u ...

  7. SQLServer之添加聚集索引

    聚集索引添加规则 聚集索引按下列方式实现 PRIMARY KEY 和 UNIQUE 约束 在创建 PRIMARY KEY 约束时,如果不存在该表的聚集索引且未指定唯一非聚集索引,则将自动对一列或多列创 ...

  8. oc中的委托模式

    通过一个例子来理解委托模式 首先定义个协议 协议(protocol) :它可以声明一些必须实现的方法和选择实现的方法  (在java中称为接口) // // StudentDelegate.h // ...

  9. 在 Xshell 中 使用 hbase shell 进入后 无法删除

    在 Xshell 中 使用 hbase shell 进入后 无法删除 问题: 在hbase shell下,误输入的指令不能使用backspace和delete删除,使用过的人都知道,这是有多坑,有多苦 ...

  10. 记一个bug

    就在刚刚,测试叫我去看一个问题,有用户反应,在业务页面,出现了一部分重复的内容,而且点击按钮弹窗里,出现了只有个title,没有body的情况. 事情的现象就是这样.然后我就开始着手找原因了.首先声明 ...