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\):

\[ans=\sum_{i=1}^m \frac{f_i\cdot i!\cdot(m-i)!}{m!}+1
\]

这个很好理解,因为前后都可以随便选,因此不再赘述

综上,我们得到了一个复杂度上界为\(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]麻将的更多相关文章

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

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

  2. 洛谷P5279 [ZJOI2019]麻将

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

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

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

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

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

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

    [ZJOI2019]麻将(动态规划,自动机) 题面 洛谷 题解 先做一点小铺垫,对于一堆牌而言,我们只需要知道这\(n\)张牌分别出现的次数就行了,即我们只需要知道一个长度为\(n\)的串就可以了. ...

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

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

  7. [ZJOI2019]麻将

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

  8. Luogu P5280 [ZJOI2019]线段树

    送我退役的神题,但不得不说是ZJOIDay1最可做的一题了 先说一下考场的ZZ想法以及出来后YY的优化版吧 首先发现每次操作其实就是统计出增加的节点个数(原来的不会消失) 所以我们只要统计出线段树上每 ...

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

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

随机推荐

  1. posix,perl正则表达式区别

    1.正则表达式(Regular Expression,缩写为regexp,regex或regxp),又称正规表达式.正规表示式或常规表达式或正规化表示法或正规表示法,是指一个用来描述或者匹配一系列符合 ...

  2. 关于Linux虚拟化技术KVM的科普 科普二(KVM虚拟机代码揭秘)

    代码分析文章<KVM虚拟机代码揭秘--QEMU代码结构分析>.<KVM虚拟机代码揭秘--中断虚拟化>.<KVM虚拟机代码揭秘--设备IO虚拟化>.<KVM虚拟 ...

  3. Windows Server 2008取消登录前的Ctrl+Alt+Delete组合键操作

    前言: 在Windows Server 2008服务器中,为了防止人们登录服务器时错误的将账户和密码输入其他地方导致信息泄漏,所以在我们登录Windows Server 2008服务器操作系统时会要求 ...

  4. Enabling Chrome Developer Tools inside Postman

    Chrome's Developer Tools are an indispensable part of the modern web development workflow. However, ...

  5. Oracle12c中功能及性能新特点之with子句的增强

    1. 设置创建测试表.DROP TABLE test PURGE; CREATE TABLE test ASSELECT 1 AS idFROM   dualCONNECT BY level < ...

  6. app后端设计(4)-- 通讯的安全性

    在app的后台设计中,一个很重要的因素是考虑通讯的安全性. 因此,我们需要考虑的要点有: 1. 在app和后台,都不能保存任何用户密码的明文 2. 在app和后台通讯的过程中,怎么保证用户信息的安全性 ...

  7. Java 读书笔记 (二) 对象和类

    Java 作为一种面向对象语言,支持以下基本概念: 多态 继承 封闭 抽象 类 对象 实例 方法 重载 对象: 是类的一个实例,有状态和行为.以人为例,黄种人.白种人.黑种人为类,每一个具体的人为类的 ...

  8. MySQL 开发实践 8 问,你能 hold 住几个?

    最近研发的项目对DB依赖比较重,梳理了这段时间使用MySQL遇到的8个比较具有代表性的问题,答案也比较偏自己的开发实践,没有DBA专业和深入,有出入的请使劲拍砖!- MySQL读写性能是多少,有哪些性 ...

  9. 不吹不擂,你想要的Python面试都在这里了【315+道题】

    写在前面 近日恰逢老男孩全栈8期毕业季,课程后期大家“期待+苦逼”的时刻莫过于每天早上内容回顾和面试题问答部分[临近毕业每天课前用40-60分钟对之前内容回顾.提问和补充,专挑班里不爱说话就的同学回答 ...

  10. 【卡特兰数】BZOJ1485: [HNOI2009]有趣的数列

    Description 我们称一个长度为2n的数列是有趣的,当且仅当该数列满足以下三个条件: (1)它是从1到2n共2n个整数的一个排列{ai}: (2)所有的奇数项满足a1<a3<…&l ...