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. 数组、ArrayList、List、LinkedList的区别

    一.数组 数组在内存中是连续存储的,所以它的索引速度非常快,而且赋值与修改元素也很简单. 1.一维数组 声明一个数组: ]; 初始化一个数组: ] { , , , , }; //定长 声明并初始化: ...

  2. 关于二叉查找树的一些事儿(bst详解,平衡树入门)

    最近刚学了平衡树,然后突发奇想写几篇博客纪念一下,可能由于是刚学的缘故,还有点儿生疏,望大家海涵 说到平衡树,就不得不从基础说起,而基础,正是二叉查找树 什么是二叉查找树?? 大家观察一下下面的这棵二 ...

  3. TestNG 自动化测试入门教程--典型示例

    TestNG介绍 TestNG是Java中的一个测试框架, 类似于JUnit 和NUnit,   功能都差不多, 只是功能更加强大,使用也更方便 Java中已经有一个JUnit的测试框架了.  Tes ...

  4. scala 访问阿里云oss

    我们的数据一天就一个T,数据量不断增大,集群磁盘有限,所以把冷数据放到了oss,偶尔会使用到冷数据,如果使用的时候还的从oss上拉数据这样很浪费时间后来想了个办法可以直接获取到oss上的数据.案例:o ...

  5. Git打标签与版本控制规范

    前言 本文适用于使用Git做VCS(版本控制系统)的场景. 用过Git的程序猿,都喜欢其分布式架构带来的commit快感.不用像使用SVN这种集中式版本管理系统,每一次提交代码,都要为代码冲突捏一把冷 ...

  6. 用C++向一个txt文档中写数据

    bool CMaked::WriteFileMake(CString filePath, const char *isChange) { ofstream file; //filePath为该txt文 ...

  7. C++中函数重载和函数覆盖的区别

    C++中经常会用到函数的重载和覆盖,二者也在很多场合都拿出来进行比较,这里我就对二者的区别做点总结: 函数重载: 函数重载指的是函数名相同.函数特征值不同的一些函数,这里函数的特征值指的是函数的参数的 ...

  8. bzoj 4916: 神犇和蒟蒻 (杜教筛+莫比乌斯反演)

    题目大意: 读入n. 第一行输出“1”(不带引号). 第二行输出$\sum_{i=1}^n i\phi(i)$. 题解: 所以说那个$\sum\mu$是在开玩笑么=.= 设$f(n)=n\phi(n) ...

  9. bzoj5250 [2018多省省队联测]秘密袭击

    博主蒟蒻,目前还不会动态dp,所以下面说的是一个并不优秀的暴力,我会补的! 我们考虑按权值从大到小依次点亮每个点,相同权值可以同时点亮,每次点亮后,我们进行一次树形背包. 处理出$f[i][j]$表示 ...

  10. BZOJ_5015_[Snoi2017]礼物_矩阵乘法

    BZOJ_5015_[Snoi2017]礼物_矩阵乘法 Description 热情好客的请森林中的朋友们吃饭,他的朋友被编号为 1-N,每个到来的朋友都会带给他一些礼物:.其中,第 一个朋友会带给他 ...