洛谷题面传送门

一道 dp 套 dp 的 immortal tea

首先考虑如何判断一套牌是否已经胡牌了,考虑 \(dp\)​​​​​。我们考虑将所有牌按权值大小从大到小排成一列,那我们设 \(dp_{i,j,k,0/1}\)​​​​ 表示目前考虑了权值 \(\le i\)​​​​ 的牌,我们之前预留了 \(j\)​​​ 张形如 \((i-1,i)\)​​​ 的牌与 \(i+1\)​​​ 形成刻子,又留了 \(k\)​​​ 张 \(i\)​​​ 与 \(i+1,i+2\)​​​ 形成刻子,\(0/1\)​​​ 表示当前是否留过对子,所能够形成的最大刻子数。设 \(c_i\)​​​ 表示有多少张权值为 \(i\)​​ 的牌,那么从 \(dp_{i,j,k,l}\)​​ 转移到 \(dp_{i+1,j',k',l'}\)​​ 时,由于我们预留了 \(k\)​​ 张 \(i-1,i\)​​ 与 \(i+1\)​​ 搭配,\(j\)​​ 张 \(i\)​​ 与 \(i+1,i+2\)​​ 搭配,所以我们首先得拿出 \(j+k\)​​ 张 \(i+1\)​​ 出来填补预留好的部分,而对于剩余的 \(c_{i+1}-j-k\)​ 牌,我们则需要决策它是否留了两张 \(i+1\) 作为对子,以及预留了多少张与 \(i+2,i+3\) 搭配。那么如何处理 \(7\) 对对子就可以胡牌这一条件呢?这个其实很好办,我们只用判断 \(c_i\ge 2\) 的 \(i\) 的个数是否 \(\ge 7\) 即可。

接下来考虑解决原问题,不难发现如果我们将所有权值从小到大读入,并将当前的 \(dp\)​​ 值看作一个状态(即,把当前 \((dp_{*,0,0,0},dp_{*,0,0,1},dp_{*,1,0,0},dp_{*,1,1,1}\cdots,dp_{*,4,4,0},dp_{*,4,4,1},cnt)\)​​ 这样的有序对视作一个个节点建一张图,其中由于七刻子胡牌这一规则的存在,我们需额外记录一个 \(cnt\) 表示当前有多少个权值 \(i\) 满足权值等于 \(i\) 的牌的张数 \(\ge 2\))那么如果我们新添上 \(x\) 张权值为 \(i+1\) 的牌,那么从 \(dp_{i}\) 转移到 \(dp_{i+1}\)​ 则可以视作从当前有序对沿着转移边转移到另一个节点,转移方法与我们之前判断一套牌是否为胡牌的方法一致,当然如果添上这 \(x\) 张权值为 \(i+1\) 的牌后已经胡了则我们要转移到终止节点——也就是胡牌这个状态对应的节点。这样我们即可将这个自动机建出来。

最后考虑怎样求解答案,注意到虽然每个 \(dp\)​​ 值可能的情况可能很多,差不多是指数级别的,可如果我们真正写个程序 BFS 一下就可以发现能够从 \((0,0,\cdots,0)\)​​ 到达的状态并不多——准确来说,是 \(2092\)​​ 个,再联系此题 \(n\)​​ 很小这个数据范围我们可以想到 DP,具体来说,对于一个摸了 \(x\)​​ 张牌后才胡牌的情形,我们可以将它的贡献拆成,摸了 \(1,2,\cdots,x-1\)​​ 张牌后还未胡牌的概率。因此我们即需求出摸了 \(i\)​​ 张牌后未胡牌的概率,将它们加起来再加 \(1\)​​ 就是答案。这样我们可以设 \(dp_{i,j,k}\)​​ 表示有多少个摸牌的集合,满足其中所有牌的权值 \(\le i\)​​,将它们全部读入自动机后当前位于自动机上 \(j\)​​ 节点的位置,并且 \(k\)​​ 张牌。转移就枚举新摸了多少张权值为 \(i+1\)​​ 的牌——设摸了 \(x\)​​ 张权值为 \(i+1\)​​ 的牌,最初的牌堆中有 \(a_{i+1}\)​​ 张权值为 \(i+1\)​ 的牌,那么需有 \(x\ge a_{i+1}\)​。沿着自动机对应的边转移即可。那么最终对于一个 \(dp_{n,j,k}\)​,其中 \(j\)​ 不是终止节点,其对答案的贡献就是 \(dp_{n,j,k}·\dfrac{k!(4n-13-k)!}{(4n-13)!}\)​。

时间复杂度 \(2092·n^2\),可以通过此题。

const int MAXN=400;
const int MAXM=2222;
const int MOD=998244353;
struct mat{
int a[3][3];
void clear(){memset(a,-1,sizeof(a));}
int* operator [](int x){return a[x];}
mat(){clear();}
bool operator ==(const mat &rhs) const{
for(int i=0;i<3;i++) for(int j=0;j<3;j++)
if(a[i][j]^rhs.a[i][j]) return 0;
return 1;
}
bool operator !=(const mat &rhs) const{return !((*this)==rhs);}
bool operator <(const mat &rhs) const{
for(int i=0;i<3;i++) for(int j=0;j<3;j++){
if(a[i][j]<rhs.a[i][j]) return 0;
if(a[i][j]>rhs.a[i][j]) return 1;
} return 0;
}
};
void get_trs(mat &to,mat from,int cnt){
for(int i=0;i<3;i++) for(int j=0;j<3;j++) if(~from[i][j])
for(int k=0;k<3&&i+j+k<=cnt;k++)
chkmax(to[j][k],min(4,from[i][j]+i+(cnt-i-j-k)/3));
}
struct node{
int prs_cnt;mat dp[2];
void clear(){prs_cnt=0;dp[0].clear();dp[1].clear();}
node(){clear();}
void make_zero(){clear();dp[0][0][0]=0;}
void make_win(){prs_cnt=-1;dp[0].clear();dp[1].clear();}
bool check_win(){
if(prs_cnt==7) return 1;
for(int i=0;i<3;i++) for(int j=0;j<3;j++) if(dp[1][i][j]==4)
return 1;
return 0;
}
void check(){if(check_win()) make_win();}
bool operator ==(const node &rhs) const{
return (prs_cnt==rhs.prs_cnt&&dp[0]==rhs.dp[0]&&dp[1]==rhs.dp[1]);
}
bool operator !=(const node &rhs) const{return !((*this)==rhs);}
bool operator <(const node &rhs) const{
if(prs_cnt^rhs.prs_cnt) return prs_cnt<rhs.prs_cnt;
if(dp[0]!=rhs.dp[0]) return dp[0]<rhs.dp[0];
if(dp[1]!=rhs.dp[1]) return dp[1]<rhs.dp[1];
return 0;
}
};
node getnxt(node a,int cnt){
if(!~a.prs_cnt) return a;
node res;
if(cnt>=2) res.prs_cnt=a.prs_cnt+1;
else res.prs_cnt=a.prs_cnt;
if(cnt>=2) get_trs(res.dp[1],a.dp[0],cnt-2);
get_trs(res.dp[0],a.dp[0],cnt);
get_trs(res.dp[1],a.dp[1],cnt);
res.check();return res;
}
map<node,int> id;
int ncnt=0,ch[MAXM+5][5],ed=0;
void find_state(){
queue<node> q;node st;st.make_zero();
id[st]=++ncnt;q.push(st);
while(!q.empty()){
node x=q.front();q.pop();
for(int i=0;i<=4;i++){
node y=getnxt(x,i);
if(!id[y]) id[y]=++ncnt,q.push(y);
ch[id[x]][i]=id[y];
}
} st.make_win();ed=id[st];
}
int fac[MAXN+5],ifac[MAXN+5];
void init_fac(int n){
for(int i=(fac[0]=ifac[0]=ifac[1]=1)+1;i<=n;i++) ifac[i]=1ll*ifac[MOD%i]*(MOD-MOD/i)%MOD;
for(int i=1;i<=n;i++) fac[i]=1ll*fac[i-1]*i%MOD,ifac[i]=1ll*ifac[i]*ifac[i-1]%MOD;
}
int binom(int x,int y){return 1ll*fac[x]*ifac[y]%MOD*ifac[x-y]%MOD;}
int n,cnt[MAXN+5],dp[MAXN/4+5][MAXM+5][MAXN+5];
int main(){
init_fac(MAXN);find_state();//printf("%d\n",ncnt);
scanf("%d",&n);
for(int i=1,x;i<=13;i++) scanf("%d%*d",&x),cnt[x]++;
dp[0][1][0]=1;
for(int i=0;i<n;i++){
for(int j=1;j<=ncnt;j++) for(int k=0;k<=n<<2;k++){
if(dp[i][j][k]){
for(int l=cnt[i+1];l<=4;l++) if(ch[j][l]!=ed){
dp[i+1][ch[j][l]][k+l]=(dp[i+1][ch[j][l]][k+l]+1ll*binom(4-cnt[i+1],l-cnt[i+1])*dp[i][j][k])%MOD;
}
}
}
} int tot=(n<<2)-13,res=0;
for(int i=1;i<=ncnt;i++) for(int j=0;j<=tot;j++) if(dp[n][i][j+13])
res=(res+1ll*dp[n][i][j+13]*fac[j]%MOD*fac[tot-j]%MOD*ifac[tot])%MOD;
printf("%d\n",res);
return 0;
}

洛谷 P5279 - [ZJOI2019]麻将(dp 套 dp)的更多相关文章

  1. 洛谷P5279 [ZJOI2019]麻将

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

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

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

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

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

  4. 洛谷P2507 [SCOI2008]配对 题解(dp+贪心)

    洛谷P2507 [SCOI2008]配对 题解(dp+贪心) 标签:题解 阅读体验:https://zybuluo.com/Junlier/note/1299251 链接题目地址:洛谷P2507 [S ...

  5. 洛谷 P3177 [HAOI2015]树上染色 树形DP

    洛谷 P3177 [HAOI2015]树上染色 树形DP 题目描述 有一棵点数为 \(n\) 的树,树边有边权.给你一个在 \(0 \sim n\)之内的正整数 \(k\) ,你要在这棵树中选择 \( ...

  6. 洛谷 P4072 [SDOI2016]征途 斜率优化DP

    洛谷 P4072 [SDOI2016]征途 斜率优化DP 题目描述 \(Pine\) 开始了从 \(S\) 地到 \(T\) 地的征途. 从\(S\)地到\(T\)地的路可以划分成 \(n\) 段,相 ...

  7. Luogu P5279 [ZJOI2019]麻将

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

  8. bzoj 3864: Hero meet devil [dp套dp]

    3864: Hero meet devil 题意: 给你一个只由AGCT组成的字符串S (|S| ≤ 15),对于每个0 ≤ .. ≤ |S|,问 有多少个只由AGCT组成的长度为m(1 ≤ m ≤ ...

  9. [模板] dp套dp && bzoj5336: [TJOI2018]party

    Description Problem 5336. -- [TJOI2018]party Solution 神奇的dp套dp... 考虑lcs的转移方程: \[ lcs[i][j]=\begin{ca ...

随机推荐

  1. 【Java虚拟机10】ClassLoader.getSystemClassLoader()流程简析

    前言 学习类加载必然离开不了sun.misc.Launcher这个类和Class.forName()这个方法. 分析ClassLoader.getSystemClassLoader()这个流程可以明白 ...

  2. DevOps 时代的高效测试之路

    10 月 22 日,2021 届 DevOps 国际峰会在北京顺利召开,来自国内外的顶级技术专家共同畅谈 DevOps 体系与方法.过程与实践.工具与技术.CODING 测试及研发流程管理产品总监程胜 ...

  3. 5.27日Scrum Metting

    日期:2021年5月27日 会议主要内容概述:确定账单数据格式,确定需要添加新的图表,确定模板分享功能任务量. 一.进度情况# 组员 负责 两日内已完成的工作 后两日计划完成的工作 工作中遇到的困难 ...

  4. 字符串与模式匹配算法(五):BMH算法

    一.BMH算法介绍 在BM算法的实际应用中,坏字符偏移函数的应用次数要远远超过好后缀偏移函数的应用次数,坏字符偏移函数在匹配过程中起着移动指针的主导作用.在实际匹配过程,只是用坏字符偏移函数也非常有效 ...

  5. X264编码测试验证

    之前在做一个rtsp直播需求,其中一个方案是要用的x264来对摄像头数据进行实时编码推流,摄像头帧率是25fps,为了验证方案的可行性,先对x264的编码速度进行一个测试研究,再确认是否要采用此方案. ...

  6. hdu 1861 游船出租(模拟题,,水)

    题意: 现有公园游船租赁处请你编写一个租船管理系统. 当游客租船时,管理员输入船号并按下S键,系统开始计时:当游客还船时,管理员输入船号并按下E键,系统结束计时. 船号为不超过100的正整数.当管理员 ...

  7. 一步一步学ROP之linux_x64篇(蒸米spark)

    目录 一步一步学ROP之linux_x64篇(蒸米spark) 0x00 序 0x01 Memory Leak & DynELF - 在不获取目标libc.so的情况下进行ROP攻击 0x02 ...

  8. python 处理xml 数据

    1 import xml.sax 2 import xml.sax.handler 3 4 # python 处理xml 数据 类,将xml数据转化为字典 5 ''' 6 原数据:<?xml v ...

  9. You (oracle) are not allowed to access to (crontab) because of pam configura

    用oracle用户添加备份计划任务,crontab -e,提示:You (oracle) are not allowed to access to (crontab) because of pam c ...

  10. 【数据结构&算法】05-线性表之数组

    目录 前言 线性结构与非线性结构 数组 数组的两个限制 数组的随机访问特性 数组的操作 插入操作 删除操作 数组越界 容器 数组下标 前言 本笔记主要记录数组的一些基础特性及操作. 顺便解答下为什么大 ...