Luogu P5279 [ZJOI2019]麻将
ZJOI2019神题,间接送我退役的神题233
考场上由于T2写挂去写爆搜的时候已经没多少时间了,所以就写挂了233
这里不多废话直接开始讲正解吧,我们把算法分成两部分
1.建一个“胡牌自动机”
首先我们发现这题不能转化为一般DP问题求解的最大瓶颈就是因为它的状态很诡异
但是我们细细一想,形如\(\{1,1,1,2,3,4,5,6,7,8,9,10,12,12\}\)和\(\{3,3,3,5,6,7,9,10,11,14,15,16,20,20\}\)的本质其实是一样的(都是三个顺子+一个刻子+一个雀头的形式,且相对大小分布相同)
那么考虑到一共只有\(13\)张牌,那么最后的有效集合数必然很少,如果我们可以构造出这些集合那么可以帮助DP了
如果你知道DP套DP的姿势,那么就会套路地明白内层的DP应该用自动机的转移形式,所以我们大致的目标就出现了:
建出一个自动机,其中每一个节点都是一个状态,然后每个节点向子节点连的边就是摸牌之后能转移到的状态
所以我们就先考虑怎么建这个自动机,还是考虑DP
我们设\(f_{0/1,i,j,k}\)表示目前处理完了前\(i\)种牌,还剩下\(j\)组\((i-1,i)\)以及\(k\)张\(i\),且是否(用\(0/1\)表示)存在雀头时最多的面子数
由于当\(j,k\ge 3\)时可以直接用\(i-1,i\)组成刻子,因此我们状态中的\(j,k\le 2\)
那么我们就开一个\(3\times3\)的矩阵表示状态,单个转移的时候枚举用于拼面子,与\((i-1,i)\)拼顺子,以及用来保留的个数,然后将多余的拿来拼刻子即可
然后考虑每次多出一个数值,我们直接讨论是否要留出一个雀头,分情况转移即可
我们发现这样并没有考虑七对子的情况,这个没关系,我们直接记录雀头的个数,特判了即可
因此剩下终止状态的判断就很简单了,直接把能胡的点作为终止节点结束即可
具体构造的过程可以用一个map
来去重,然后用BFS来扩展状态,这个具体看代码
2.期望DP
先说一句,前面由于自动机对于任意数据构造相同,所以总点数是固定的\(2092\),如前言所述不大
那么考虑DP求解最后的问题,我们用一个经典套路,将求\(\ge\)的化为求\(>\)然后最后加上等于的情况即可
具体到这道题上,其实就是求出\(i\)轮后不胡的方案数然后乘上贡献,最后的总答案加\(1\)即可
那么大致的DP方程就有了,我们设\(f_{i,j,k}\)表示选了前\(i\)种牌,一共用了\(j\)张,此时位于自动机上\(k\)号点的方案数
那么转移其实很简单,枚举第\(i\)种牌选的张数\(t\),\(f_{i,j,k}\)向\(f_{i+1,j+t,node_k.son_{a_i+t}}\)转移,注意不要忘记乘上组合数\(C_{4-a_i}^t\)
求出\(f\)数组后最后的答案统计就十分简单了,令\(m=4n-13\):
\]
这个很好理解,因为前后都可以随便选,因此不再赘述
综上,我们得到了一个复杂度上界为\(2092\cdot n^2\)的优秀做法,因为常数很小且跑不满因此足以通过
CODE
#include<cstdio>
#include<cstring>
#include<map>
#define RI register int
#define CI const int&
using namespace std;
const int N=105,MX=2100,mod=998244353;
inline void maxer(int& x,CI y)
{
if (y>x) x=y;
}
inline int min(CI a,CI b)
{
return a<b?a:b;
}
struct Matrix
{
int mat[3][3];
inline int* operator [] (CI x) { return mat[x]; }
inline Matrix(void)
{
memset(mat,-1,sizeof(mat));
}
friend inline bool operator != (Matrix A,Matrix B)
{
for (RI i=0;i<3;++i) for (RI j=0;j<3;++j)
if (A[i][j]!=B[i][j]) return 1; return 0;
}
friend inline bool operator < (Matrix A,Matrix B)
{
for (RI i=0;i<3;++i) for (RI j=0;j<3;++j)
if (A[i][j]!=B[i][j]) return A[i][j]<B[i][j];
}
inline bool Com_Hu(void)
{
for (RI i=0;i<3;++i) for (RI j=0;j<3;++j)
if (mat[i][j]>=4) return 1; return 0;
}
inline void flush(Matrix pre,CI num)
{
for (RI i=0;i<3;++i) for (RI j=0;j<3;++j)
if (~pre[i][j]) for (RI k=0;k<3&&i+j+k<=num;++k)
maxer(mat[j][k],min(i+pre[i][j]+(num-i-j-k)/3,4));
}
};
struct Hu_Auto_Node
{
Matrix p[2]; int cur,ch[5];
inline Hu_Auto_Node(void)
{
memset(ch,0,sizeof(ch)); cur=0; p[0]=p[1]=Matrix();
}
inline bool is_Hu(void)
{
if (cur>=7) return 1; return p[1].Com_Hu();
}
inline void Hu(void)
{
memset(ch,0,sizeof(ch)); cur=-1; p[0]=p[1]=Matrix();
}
friend inline bool operator < (Hu_Auto_Node A,Hu_Auto_Node B)
{
if (A.cur!=B.cur) return A.cur<B.cur;
if (A.p[0]!=B.p[0]) return A.p[0]<B.p[0];
if (A.p[1]!=B.p[1]) return A.p[1]<B.p[1]; return 0;
}
friend inline Hu_Auto_Node operator + (Hu_Auto_Node A,CI num)
{
if (A.is_Hu()) return A.Hu(),A; Hu_Auto_Node s;
s.p[0].flush(A.p[0],num); s.p[1].flush(A.p[1],num);
if (num>=2) s.p[1].flush(A.p[0],num-2);
s.cur=A.cur+(num>=2); if (s.is_Hu()) s.Hu(); return s;
}
};
class Hu_Automation
{
private:
map <Hu_Auto_Node,int> Hash;
inline void expand(CI id)
{
for (RI i=0;i<=4;++i)
{
Hu_Auto_Node son=node[id]+i;
if (!Hash.count(son)) node[Hash[son]=++tot]=son;
node[id].ch[i]=Hash[son];
}
}
public:
Hu_Auto_Node node[2100]; int tot;
inline Hu_Automation(void)
{
node[1].p[0][0][0]=0; node[tot=2].cur=-1;
Hash[node[1]]=1; Hash[node[2]]=2; expand(1);
for (RI i=3;i<=tot;++i) expand(i);
}
/*inline void check(void)
{
printf("%d\n",tot); for (RI i=1;i<=tot;++i,putchar('\n'))
for (RI j=0;j<=4;++j) printf("%d ",node[i].ch[j]);
}*/
}HA;
int f[2][N<<2][MX],a[N],fact[N<<2],inv[N<<2],n,m,x,y,nw,ans;
inline void inc(int& x,CI y)
{
if ((x+=y)>=mod) x-=mod;
}
inline int quick_pow(int x,int p=mod-2,int mul=1)
{
for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
inline void init(CI n)
{
RI i; for (fact[0]=i=1;i<=n;++i) fact[i]=1LL*fact[i-1]*i%mod;
for (inv[n]=quick_pow(fact[n]),i=n-1;~i;--i) inv[i]=1LL*inv[i+1]*(i+1)%mod;
}
inline int C(CI n,CI m)
{
return 1LL*fact[n]*inv[m]%mod*inv[n-m]%mod;
}
inline void calc(CI x,CI y)
{
inc(ans,1LL*f[nw][x][y]*fact[x]%mod*fact[m-x]%mod);
}
int main()
{
RI i,j,k,t; for (scanf("%d",&n),i=1;i<=13;++i) scanf("%d%d",&x,&y),++a[x];
for (init(m=(n<<2)-13),f[0][0][1]=i=1;i<=n;++i)
for (nw=i&1,memset(f[nw],0,sizeof(f[nw])),j=m;~j;--j)
for (k=1;k<=HA.tot;++k) if (f[nw^1][j][k]) for (t=0;t<=4-a[i];++t)
inc(f[nw][j+t][HA.node[k].ch[a[i]+t]],1LL*f[nw^1][j][k]*C(4-a[i],t)%mod);
for (nw=n&1,i=1;i<=m;++i) for (calc(i,1),j=3;j<=HA.tot;++j) calc(i,j);
return printf("%d",(1LL*ans*inv[m]%mod+1)%mod),0;
}
Luogu P5279 [ZJOI2019]麻将的更多相关文章
- 【题解】Luogu P5279 [ZJOI2019]麻将
原题传送门 希望这题不会让你对麻将的热爱消失殆尽 我们珂以统计每种牌出现的次数,不需要统计是第几张牌 判一副牌能不能和,类似这道题 对于这题: 设\(f[i][j][k][0/1]\)表示前\(i\) ...
- 洛谷P5279 [ZJOI2019]麻将
https://www.luogu.org/problemnew/show/P5279 以下为个人笔记,建议别看: 首先考虑如何判一个牌型是否含有胡的子集.先将牌型表示为一个数组num,其中num[i ...
- 洛谷P5279 [ZJOI2019]麻将(乱搞+概率期望)
题面 传送门 题解 看着题解里一堆巨巨熟练地用着专业用语本萌新表示啥都看不懂啊--顺便\(orz\)余奶奶 我们先考虑给你一堆牌,如何判断能否胡牌 我们按花色大小排序,设\(dp_{0/1,i,j,k ...
- 洛谷 P5279 - [ZJOI2019]麻将(dp 套 dp)
洛谷题面传送门 一道 dp 套 dp 的 immortal tea 首先考虑如何判断一套牌是否已经胡牌了,考虑 \(dp\).我们考虑将所有牌按权值大小从大到小排成一列,那我们设 \(dp_ ...
- [ZJOI2019]麻将(动态规划,自动机)
[ZJOI2019]麻将(动态规划,自动机) 题面 洛谷 题解 先做一点小铺垫,对于一堆牌而言,我们只需要知道这\(n\)张牌分别出现的次数就行了,即我们只需要知道一个长度为\(n\)的串就可以了. ...
- 题解 洛谷 P5279 【[ZJOI2019]麻将】
这题非常的神啊...蒟蒻来写一篇题解. Solution 首先考虑如何判定一副牌是否是 "胡" 的. 不要想着统计个几个值 \(O(1)\) 算,可以考虑复杂度大一点的. 首先先把 ...
- [ZJOI2019]麻将
这是一道麻将自动机的模板题(雾 其实这是一道dp套dp借助自动机实现的麻将好题! 首先把期望转化一下,拆成sigema p(x>i) 现在要计算i张牌不胡的概率,也就等价于计算i张牌不胡的方案数 ...
- Luogu P5280 [ZJOI2019]线段树
送我退役的神题,但不得不说是ZJOIDay1最可做的一题了 先说一下考场的ZZ想法以及出来后YY的优化版吧 首先发现每次操作其实就是统计出增加的节点个数(原来的不会消失) 所以我们只要统计出线段树上每 ...
- 【洛谷5279】[ZJOI2019] 麻将(“胡牌自动机”上DP)
点此看题面 大致题意: 给你13张麻将牌,问你期望再摸多少张牌可以满足存在一个胡的子集. 似乎ZJOI2019Day1的最大收获是知道了什么是胡牌? 一个显然的性质 首先我们要知道一个显然的性质,即对 ...
随机推荐
- python3字典:获取json响应值来进行断言
前言:在接口自动化测试中,最后都是需要将返回结果进行断言.本文按照实际情况举例说明字典各种操作,以及如何提取响应数据来进行断言 1.dict = {'code': '200', 'message': ...
- Day4 《机器学习》第四章学习笔记
决策树 前几天学习了<机器学习>的前三章,前三章介绍机器学习的基础知识,接下来,第四章到第十章介绍一些经典而常用的机器学习方法,这部分算是具体的应用篇,第四章介绍了一类机器学习方法——决策 ...
- php curl_errno 60
问题描述 使用curl进行微信统一下单,curl 错误 curl_errno 60 错误码60 因为使用了证书配置项,所以要配置curl证书 解决方法 下载证书并配置php.ini ,配置curl证书 ...
- CountDownLatch简介
CountDownLatch是并发包中提供的一个可用于控制多个线程同时开始某动作的类,可以看做是一个计数器,计数器操作是院子操作,同时只能有一个线程去操作这个计数器.可以向CountDownLatch ...
- HEOI2018——welcome to NOI2018
我不得不和烈士和小丑走在同一道路上, 万人都要将火熄灭, 我一人独将此火高高举起, 我借此火得度一生的茫茫黑夜. ——海子 弹指一瞬间,翘首以盼的HEOI2018就来了. 我,一个滑稽的小丑,带 ...
- [Usaco2007 Open]Fliptile 翻格子游戏 状压dp
n,m<=15,直接搞肯定不行,考虑一行一行来, 每一行的状态只与三行有关,所以从第一行开始枚举,每一次让下面一行填上他上面那行的坑 最后一行必须要同时满足他自己和他上面那行,否则舍去 #inc ...
- BZOJ_1691_[Usaco2007 Dec]挑剔的美食家_贪心
BZOJ_1691_[Usaco2007 Dec]挑剔的美食家_贪心 题意: 与很多奶牛一样,Farmer John那群养尊处优的奶牛们对食物越来越挑剔,随便拿堆草就能打发她们午饭的日子自然是一去不返 ...
- NOIP2017 酱油送命记
Day0 一天,在机房,有点考前的紧张和慌张,打了一下午的模板,立了3个不该立的flag... Day1 拿到试题,万分紧张,T1是数论啊 害怕,一直以为D2T1才是数论,仔细观察却发现(flag1: ...
- 【Canal源码分析】parser工作过程
本文主要分析的部分是instance启动时,parser的一个启动和工作过程.主要关注的是AbstractEventParser的start()方法中的parseThread. 一.序列图 二.源码分 ...
- VUE+webpack+npm项目中的RSA加解密
一.安装jsencrypt npm i jsencrypt node_modules文件夹中出现jsencrypt 二.引入jsencrypt 在main.js中import: import JsEn ...