P5279 [ZJOI2019]麻将

题面

link

今天,可怜想要打麻将,但是她的朋友们都去下自走棋了,因此可怜只能自己一个人打。可怜找了一套特殊的麻将,它有 \(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的一个记录?
posted @ 2022-03-27 10:57  qwq_123  阅读(39)  评论(0编辑  收藏  举报