【洛谷5279】[ZJOI2019] 麻将(“胡牌自动机”上DP)
大致题意: 给你13张麻将牌,问你期望再摸多少张牌可以满足存在一个胡的子集。
似乎ZJOI2019Day1的最大收获是知道了什么是胡牌?
一个显然的性质
首先我们要知道一个显然的性质,即对于一副牌,我们仅需要考虑其每张牌的张数,而顺序是没有任何关系的。
因此,对于一副牌,我们可以将其转化为一个长度为\(n\),每个位置上为\(0\sim4\)的序列。
这样就方便操作了许多。
胡牌自动机
在前面性质的基础上,我们来考虑如何判断一副牌,即一个长度为\(n\)的序列是否能胡。
我们似乎可以建一个自动机(可以以其作用命名为胡牌自动机)去处理它。
建自动机的前奏:\(DP\)
那么我们该如何去建这个自动机呢?
考虑如果我们能得出一个判断一副牌是否能胡的\(DP\),然后把每个状态看作自动机的点,\(DP\)转移看作自动机的边,则一个自动机就建成了。
于是问题又变成了:如何用\(DP\)来判断一副牌是否能胡。
设\(f_{0/1,i,j,k}\)表示处理完前\(i\)种牌,还剩\(j\)组\((i-1,i)\)以及\(k\)张\(i\),且存在(\(1\))/不存在(\(0\))对子时最多的面子数。
由于\(j\ge3\)时,我们可以用\(3\)个\(i-1\)和\(3\)个\(i\)各自组成面子;\(k\ge3\)时,我们可以直接用\(3\)个\(i\)组成面子。
因此,\(0\le j,k\le2\)。
所以可以考虑建一个\(3*3\)的矩阵存下\(f_{0/1,i}\)的全部答案。
假设加入了\(x\)张牌,则我们进行如下几种转移:
- 将\(f_{0,i}\)从\(f_{0,i-1}\)加\(x\)张牌转移过来。
- 将\(f_{1,i}\)从\(f_{1,i-1}\)加\(x\)张牌转移过来。
- 如果\(x>1\),则将\(f_{1,i}\)从\(f_{0,i-1}\)加\(x-2\)张牌转移过来。
转移过程中,我们枚举若干张牌和之前的\((i-2,i-1)\)拼面子,保留若干组\((i-1,i)\)和若干张\(i\),然后拿剩下的牌尽可能地拼面子,这样即可进行转移。
根据定义,若\(f_{1,i}>3\),则这副牌就能胡了。
说到这里,或许你会发现,我们在这个\(DP\)中并没有考虑七对子的情况,这在后面会特判处理。
正式开始建自动机
接下来我们考虑如何将这个\(DP\)转化为自动机。
首先,我们确定一个初始状态(一张牌都没有)。
然后,以类似于\(BFS\)的方式,找到未处理过的节点,枚举新加入的牌数,然后通过\(DP\)转移的方式得出子节点的状态。
不难发现,前面的\(i\)在这里没有任何作用,可以不用记录。因为我们是从每个节点一步步转移的。
而\(0/1\)这个状态还是十分必要的,因此我们可以考虑,对自动机上每个节点开两个矩阵\(P_{0/1}\),来进行转移。
此外,由于前面提到过的七对子,我们再开一个变量\(t\)记录出现的对子个数。
则综上所述,一个节点是胡的,当且仅当其\(P_1\)中存在一个元素大于\(3\)或\(t\ge7\)。
而为了提高效率,我们可以把所有胡的节点全部压成一个节点,以其\(t=-1\)作为特殊标记即可。
对于其他节点,我们可以开个\(map\)判断一种节点是否已经出现过(注意要将\(t,P_{0/1}\)全部进行比较),出现过则直接连边,否则先新建节点然后再连边。
顺便提一句,这里的\(BFS\)只要按节点编号从小到大枚举即可,无须队列。
显然按此方式建出来的胡牌自动机形态是固定的,即对于任何数据长得都一样。
具体实现详见代码。
胡牌自动机上\(DP\)
现在,我们到了这道题的最后一个关键步骤,胡牌自动机上\(DP\)。
我们可以设\(g_i\)表示摸了\(i\)张牌后不胡的方案数,则答案就为:
\]
其中分子中的\(i!\)和\((4n-13-i)!\)表示这\(i\)张牌和剩下的\(4n-13-i\)张牌放的顺序任意,可以随便放;分母中的\((4n-13)!\)是总方案数,显然算期望必须要除;加\(1\)是因为我们选择\(i\)张牌后依然不能胡,需要\(+1\)。
然后考虑如何\(DP\)。
设\(f_{i,j,k}\)表示处理到第\(i\)张牌,共摸了\(j\)张牌,走到了胡牌自动机上的\(k\)号节点的方案数。
那么显然我们可以枚举一个摸的牌数\(t\)(\(0≤t≤4-a_i\),其中\(a_i\)为初始\(13\)张牌中\(i\)的张数),然后从\(f_{i,j,k}\)向\(f_{i+1,j+t,O_k.Son_{a_i+t}}\)转移,其中\(O_k.Son_{a_i+t}\)表示胡牌自动机上\(k\)号节点的第\(a_i+t\)个儿子。
还有\(4-a_i\)张牌中选\(t\)张牌的方案数\(C_{4-a_i}^t\)要记得乘上。
这个\(DP\)转移应该是比较显然,也比较简单的。
代码
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100
#define M 400
#define X 998244353
#define Gmax(x,y) (x<(y)&&(x=(y)))
#define Inc(x,y) ((x+=(y))>=X&&(x-=X))
#define Qinv(x) Qpow(x,X-2)
using namespace std;
int n,m,a[N+5],Fac[M+5],Inv[M+5];
I int Qpow(RI x,RI y) {RI t=1;W(y) y&1&&(t=1LL*t*x%X),x=1LL*x*x%X,y>>=1;return t;}
class HuAutomation//胡牌自动机
{
private:
#define SZ 2092//实测自动机大小
#define C(x,y) (1LL*Fac[x]*Inv[y]%X*Inv[(x)-(y)]%X)//组合数
#define Pos(x) (p.count(x)?p[x]:(O[p[x]=++tot]=x,tot))//求节点编号,若不存在则新建一个
#define Extend(x) for(j=0;j^5;++j) O[x].S[j]=Pos(O[x]+j);//扩展
class Mat//矩阵
{
private:
#define CM Con Mat&
#define Rp for(RI i=0,j;i^3;++i) for(j=0;j^3;++j)
#define S (i+j+k)
int f[3][3];
public:
I Mat() {Rp f[i][j]=-1;}I int* operator [] (CI x) {return f[x];}
I bool operator != (Mat o) Con {Rp if(f[i][j]^o[i][j]) return 1;return 0;}//不等于
I bool operator < (Mat o) Con {Rp if(f[i][j]^o[i][j]) return f[i][j]<o[i][j];}//比大小,用于map
I bool Check() Con {Rp if(f[i][j]>3) return 1;return 0;}//判断是否能胡
I void F5(Mat o,CI t)//更新
{
Rp if(~o[i][j]) for(RI k=0;k^3&&S<=t;++k)//i,j,k分别枚举用于拼面子、用于保留(i-1,i)、用于保留i和直接拼面子的牌数
Gmax(f[j][k],min(i+o[i][j]+(t-S)/3,4));//转移更新信息(要向4取min是因为大于4没有意义,同时提高效率)
}
#undef S
};
struct node//存储一个节点的信息
{
int t,S[5];Mat P[2];I node() {t=S[0]=S[1]=S[2]=S[3]=S[4]=0,P[0]=P[1]=Mat();}
I bool operator < (Con node& o) Con//用于map
{
return t^o.t?t<o.t:(P[0]!=o.P[0]?P[0]<o.P[0]:(P[1]!=o.P[1]?P[1]<o.P[1]:0));
}
I node operator + (CI x) Con//加上x张新牌
{
if(IsHu()) return Hu();node res;//如果已经胡了直接返回
res.P[0].F5(P[0],x),res.P[1].F5(P[1],x),x>1&&(res.P[1].F5(P[0],x-2),0),//进行转移
res.t=t+(x>1),res.IsHu()&&(res=Hu(),0);return res;//统计对子数,然后判断是否胡
}
I bool IsHu() Con {return !~t||t>=7||P[1].Check();}//已经胡或者七对子或者存在4个面子和1个对子
I node Hu() Con {node x;return x.t=-1,x;}//胡牌的特殊标记
}O[SZ+5];map<node,int> p;
I node Begin() {node x;return x.P[0][0][0]=0,x;}//初始状态
I node Hu() {node x;return x.t=-1,x;}//胡牌的特殊标记
public:
int tot,f[N+5][M+5][SZ+5];
I void Build()//建自动机
{
RI i,j;p[O[1]=Begin()]=1,p[O[2]=Hu()]=tot=2;//建立初始状态和胡牌状态
Extend(1);for(i=3;i<=tot;++i) Extend(i);//对除第2个(胡牌)以外的其他状态进行扩展
}
I void DP()//DP求解答案
{
for(RI i=f[0][0][1]=1,j,k,t;i<=n;++i) for(j=m;~j;--j)//枚举当前是第i张牌,共摸了j张牌
for(k=1;k<=tot;++k) if(f[i-1][j][k]) for(t=0;t<=4-a[i];++t)//枚举在胡牌自动机哪个节点上,以及现在摸的牌数
Inc(f[i][j+t][O[k].S[a[i]+t]],1LL*f[i-1][j][k]*C(4-a[i],t)%X);//转移,注意乘上组合数系数
}
}H;
I void CInit(CI x)//初始化
{
RI i;for(Fac[0]=i=1;i<=x;++i) Fac[i]=1LL*Fac[i-1]*i%X;//初始化阶乘
for(Inv[x]=Qinv(Fac[x]),i=x-1;~i;--i) Inv[i]=1LL*Inv[i+1]*(i+1)%X;//初始化阶乘逆元
}
#define Calc(x,y) Inc(ans,1LL*H.f[n][x][y]*Fac[i]%X*Fac[m-i]%X)//统计答案
int main()
{
RI i,j,x,y,ans=0;for(H.Build(),scanf("%d",&n),i=1;i<=13;++i) scanf("%d%d",&x,&y),++a[x];//读入数据+预处理
for(m=(n<<2)-13,CInit(m),H.DP(),i=1;i<=m;++i) for(Calc(i,1),j=3;j<=H.tot;++j) Calc(i,j);//统计答案,注意跳过2号节点
return printf("%lld",1LL*ans*Inv[m]%X+1),0;//输出答案,除以总状态数然后加1
}
【洛谷5279】[ZJOI2019] 麻将(“胡牌自动机”上DP)的更多相关文章
- 洛谷 P5279 - [ZJOI2019]麻将(dp 套 dp)
洛谷题面传送门 一道 dp 套 dp 的 immortal tea 首先考虑如何判断一套牌是否已经胡牌了,考虑 \(dp\).我们考虑将所有牌按权值大小从大到小排成一列,那我们设 \(dp_ ...
- 洛谷P5279 [ZJOI2019]麻将
https://www.luogu.org/problemnew/show/P5279 以下为个人笔记,建议别看: 首先考虑如何判一个牌型是否含有胡的子集.先将牌型表示为一个数组num,其中num[i ...
- 洛谷P5279 [ZJOI2019]麻将(乱搞+概率期望)
题面 传送门 题解 看着题解里一堆巨巨熟练地用着专业用语本萌新表示啥都看不懂啊--顺便\(orz\)余奶奶 我们先考虑给你一堆牌,如何判断能否胡牌 我们按花色大小排序,设\(dp_{0/1,i,j,k ...
- 洛谷 P2656 (缩点 + DAG图上DP)
### 洛谷 P2656 题目链接 ### 题目大意: 小胖和ZYR要去ESQMS森林采蘑菇. ESQMS森林间有N个小树丛,M条小径,每条小径都是单向的,连接两个小树丛,上面都有一定数量的蘑菇.小胖 ...
- 洛谷P5280 [ZJOI2019]线段树 [线段树,DP]
传送门 无限Orz \(\color{black}S\color{red}{ooke}\)-- 思路 显然我们不能按照题意来每次复制一遍,而多半是在一棵线段树上瞎搞. 然后我们可以从\(modify\ ...
- 洛谷 P5280 - [ZJOI2019]线段树(线段树+dp,神仙题)
题面传送门 神仙 ZJOI,不会做啊不会做/kk Sooke:"这八成是考场上最可做的题",由此可见 ZJOI 之毒瘤. 首先有一个非常显然的转化,就是题目中的"将线段树 ...
- 【洛谷3239_BZOJ4008】[HNOI2015] 亚瑟王(期望 DP)
题目: 洛谷 3239 分析: 卡牌造成的伤害是互相独立的,所以 \(ans=\sum f_i\cdot d_i\) ,其中 \(f_i\) 表示第 \(i\) 张牌 在整局游戏中 发动技能的概率.那 ...
- 洛谷 P4093 [HEOI2016/TJOI2016]序列 CDQ分治优化DP
洛谷 P4093 [HEOI2016/TJOI2016]序列 CDQ分治优化DP 题目描述 佳媛姐姐过生日的时候,她的小伙伴从某宝上买了一个有趣的玩具送给他. 玩具上有一个数列,数列中某些项的值可能会 ...
- 洛谷 P3580 - [POI2014]ZAL-Freight(单调队列优化 dp)
洛谷题面传送门 考虑一个平凡的 DP:我们设 \(dp_i\) 表示前 \(i\) 辆车一来一回所需的最小时间. 注意到我们每次肯定会让某一段连续的火车一趟过去又一趟回来,故转移可以枚举上一段结束位置 ...
随机推荐
- PIE SDK栅格RGB渲染
1. 功能简介 RGB色彩模式是一种颜色标准,是通过对红(R).绿(G).蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,RGB即是代表红.绿.蓝三个通道的颜色,这个标准几乎包 ...
- 使用 .NET Core CLI 创建 .NET Core 全局工具
https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=2&ch=&tn=baiduhome_pg& ...
- oracle trim不掉空白字符分享(转)
本文转自:http://www.2cto.com/database/201306/223558.html 问题背景:一个工商注册号,正常的用trim能解决的问题,但是这个case,trim后和肉眼看到 ...
- 我的博客已经迁移到csdn
博客已经迁移csdnhttp://blog.csdn.net/u013372900 博客园我很喜欢是源于他的可扩展性,可以自己去改,但遗憾的是博客园的速度似乎不是很给力.IT能有今天的 发展是源于无数 ...
- 图解CSS的padding,margin,border属性(详细介绍及举例说明)
图解CSS的padding,margin,border属性 W3C组织建议把所有网页上的对像都放在一个盒(box)中,设计师可以通过创建定义来控制这个盒的属性,这些对像包括段落.列表.标题.图片以及层 ...
- Oracle中查询关键字select--from--where--group by--having--order by执行顺序
select--from--where--group by--having--order by 这6个查询关键字的执行顺序: 1.from组装来自不同数据源的数据:2.where基于指定的条件对记录行 ...
- 跨域策略文件crossdomain.xml文件
使用crossdomain.xml让Flash可以跨域传输数据 一.crossdomain.xml文件的作用 跨域,顾名思义就是需要的资源不在自己的域服务器上,需要访问其他域服务器.跨域策略文件 ...
- Java流和文件
File类:java.io包下与平台无关的文件和目录 java可以使用文件路径字符串来创建File实例,文件路径可以是绝对路径,也可以是相对路径,默认情况下,相对路径是依据用户工作路径,通常就是运行J ...
- mvc 中Request[""]与Request.QueryString[""]
1.Request[""]与Request.QueryString[""]获取不到值时返回null: 2.Request[""]与Reque ...
- 08.StreamReader和StreamWrite的学习
StreamReader和StreamWrite是用来操作字符的 namespace _21.对StreamReader和StreamWriter的学习 { class Program { stati ...