P5279 [ZJOI2019]麻将
题面
今天,可怜想要打麻将,但是她的朋友们都去下自走棋了,因此可怜只能自己一个人打。可怜找了一套特殊的麻将,它有 \(n(n\ge 5)\) 种不同的牌,大小分别为 \(1\) 到 \(n\),每种牌都有 \(4\) 张。
定义面子为三张大小相同或者大小相邻的麻将牌,即大小形如 \(i,i,i(1 \le i \le n)\) 或者\(i,i+1,i+2(1\le i\le n-2)\)。定义对子为两张大小相同的麻将牌,即大小形如 \(i,i(1 \le i \le n)\)。
定义一个麻将牌集合 \(S\) 是胡的当且仅当它的大小为 \(14\) 且满足下面两个条件中的至少一个:
- \(S\) 可以被划分成五个集合 \(S_1\) 至 \(S_5\) 。其中 \(S_1\) 为对子,\(S_2\) 至 \(S_5\) 为面子。
- \(S\) 可以被划分成七个集合 \(S_1\) 至 \(S_7\) ,它们都是对子,且对应的大小两两不同。
可怜先摸出了 \(13\) 张牌,并把剩下的 \(4n-13\) 张牌随机打乱。打乱是等概率随机的,即所有\((4n-13)!\)种排列都等概率出现。
对于一个排列 \(P\),可怜定义 \(S_i\) 为可怜事先摸出的 \(13\) 张牌加上 \(P\) 中的前 \(i\) 张牌构成的集合,定义 \(P\) 的权值为最小的 \(i\) 满足 \(S_i\) 存在一个子集是胡的。
现在可怜想要训练自己的牌效,因此她希望你能先计算出 \(P\) 的权值的期望是多少。
数据范围:\(n\le 100\) 。
题解
感觉算是DP套DP中很高级的了(虽然总共也只见过两题...)
主要是理解DP套的那一个DP是在干什么。
其实 AC自动机结合DP 就是一个DP套DP。(如果知道这个东西那么你完全按理解它的方式理解是完全可以的)
因为内层的DP实际上就是一个自动机的形式。
比如这题中我们需要的就是判断给定一些牌,判断是否是胡牌(AC自动机内是需要判断当前字符串是否包含了给定的一些字符串)。
以下是怎么判断一副牌是胡的。
如果只要判断是否胡牌,那么有用的信息只有每个牌有几张。
所以相当于我们现在只需要输入一个长度为 \(n\) 的数组 \(a\) ,其中 \(a_i\) 表示大小为 \(i\) 的有 \(a_i\) 张。
现在考虑我们需要维护什么。
-
第一种胡法,我们可以设 \(f_{i,j,k,0/1}\) 表示处理完了第 \(i\) 种编号的牌,我们预留了 \(j\) 对形如\(i−1,i\) 的牌,和 \(k\)个编号为 \(i\) 的牌, \(0/1\) 表示之前的决策过程当中有没有预留过对子,dp当中存储的值是这种情况下的最大面子数。
并且三个相同的顺子可以被算成三个连续的刻子,所以 \(j\) 和 \(k\) 这两维都不会超过 \(2\) 。
转移是比较简单的。
-
第二种胡法,我们只需要额外记录一个变量 \(cnt\) 表示出现的对子的数量就行。
但是到现在为止我们也只是知道了怎么判断和牌,和题目要求的东西还相差甚远。
别急,还记得 AC自动机+DP 如何处理的吗,我们是判断 \(tr[j][k]\) 是否合法(不包含给定字符串),其中 \(j\) 为当前状态, \(k\) 为枚举的下一位是什么字符。
所以,这题我们也可以把所有可能的 胡牌状态 记录下来,构造 \(tr[j][k]\) ,然后转移。
具体来说,就是上面提到的 \(f,cnt\) ,我们把它记成一个结构体,用map去重、标号。
下面代码省去了重载和初始化。
struct qqq{
int f[3][3];
int* operator [] (const int &x){
return f[x];
}
void change(qqq &x,const int &y){//下一个大小的牌有 y 张。
for(int i=0;i<3;++i)
for(int j=0;j<3;++j)
if(f[i][j]!=-1)
for(int k=0,la=y-i-j;la>=0&&k<=2;--la,++k)
x[j][k]=max(x[j][k],min(i+f[i][j]+la/3,4));
}
bool pd(){
for(int i=0;i<3;++i)
for(int j=0;j<3;++j)
if(f[i][j]>=4)return 1;
return 0;
}
};
struct qq{
qqq f,g;//f:没有预留对子,g:有预留对子
int cnt;//对子的数目
bool pd(){//判断是否胡牌
if(cnt>=7)return 1;
return g.pd();
}
}a[maxn];
qq ne2(qq x,const int &y){//下一个大小的牌有 y 张。
qq ans;ans.cl();
ans.cnt=x.cnt+(y>=2);
if(y>=2)x.f.change(ans.g,y-2);//可以预留对子
x.f.change(ans.f,y),x.g.change(ans.g,y);
return ans;
}
map<qq,int> id;
int ne[maxn][5];
queue<int> p;
void sous(){
a[0].cl(),id[a[0]]=0;
p.push(0);
while(p.size()){
int x=p.front();p.pop();
for(int i=0;i<=4;++i){
qq y=ne2(a[x],i);
if(!id.count(y)){
if(y.pd())ne[x][i]=id[y]=-1;
else{
ne[x][i]=id[y]=++top,a[top]=y;
p.push(top);
}
}
else ne[x][i]=id[y];
}
}
}
现在我们就可以仿照 AC自动机+DP 去设状态了,
一个很套路的转化: 答案 \(=1+\sum\limits_x \dfrac{f(x)}{\binom{4n-13}{x}}\) ,其中 \(f(x)\) 表示摸了的牌数(不包括本来就有的 \(13\) 张)大于等于 \(x\) 的方案数。
所以就可以设 \(f_{i,j,k}\) 表示当前考虑到了大小为 \(i\) 的牌,摸了 \(j\) 张,当前胡牌状态是 \(k\) ,转移是简单的。
启发
- DP套DP的一个记录?