AtCoder agc024_e - Sequence Growing Hard

tzc:“AtC风格的计数DP。”我就想问了,AtC的题怎么可能不是AtC风格的呢?

洛谷题目页面传送门 & AtC题目页面传送门

给定\(n,m,p\),求有多少个值域为\([1,m]\cap\mathbb Z\)的序列\(n+1\)元组\((a_0,a_1,\cdots,a_n)\),满足:

  1. \(\forall i\in[0,n],|a_i|=i\)
  2. \(\forall i\in[0,n)\)\(a_i\)\(a_{i+1}\)的子序列;
  3. \(\forall i\in[0,n),a_i<a_{i+1}\)

答案对\(p\)取模。

\(n,m\in[1,300],p\in\left[2,10^9\right]\)

显然,原问题等价于,一个序列\(a\)一开始为空,\(n\)次每次往里任意位置塞一个\([1,m]\)内的数,使得字典序大于原来的序列,求\(n\)次组成的序列组的个数(注意这里不能说操作的数量,因为在不同位置插入相同数可能得到的序列一样)。

考虑在位置\(x\)插入\(y\)合法的充要条件。若\(x=|a|+1\)显然合法。剩下的显然要满足\([y]+a_{x,|a|}>a_{x,|a|}\)。若\(y\neq a_x\),那么字典序相对大小决定在它们身上,所以\(y>a_x\)。否则的话,,,注意到若把\(y\)插入到往右数第一个满足\(y\neq a_{x'}\)的位置\(x'\),那么得到的序列是一样的,为了去重,我们可以直接不考虑这种情况,反而更方便了。于是充要条件就是\(x=|a+1|\)\(y>a_x\)

分析完之后还是不会做。这是一个动态的插入问题,每次的\(a\)都会变。不妨将时间定格在最后一刻,所有数都各就各位了,再来考虑每个位置\(i\)上的值\(v_i\)与被插入的时间(时间戳)\(dfn_i\)

稍微转化一下,此时一个\((v,dfn)\)序列合法当且仅当,\(\forall i\in[1,n],v_i>v_{\min\limits_{j>i,dfn_j<dfn_i}\{j\}}\)(为了方便,设\(v_{n+1}=dfn_{n+1}=0\),合理性显然)。考虑\(v_i\)\(>\)的那个东西固定的一个,就是使得\(dfn_x=1\)\(x\)。它的条件是\(v_x>v_{n+1}=0\)。然后不难发现,\(\forall i\in[1,x),\min\limits_{j>i,dfn_j<dfn_i}\{j\}\in[1,x]\),对应的,\(\forall i\in(x,n],\min\limits_{j>i,dfn_j<dfn_i}\{j\}\in[x+1,n+1]\)。这样就可以把问题分解成\(x\)左右两半相似的问题,其中左边的值域变成\((v_x,m]\),右边值域不变。问题就迎刃而解了。

考虑DP。设\(dp_{i,j}\)表示\((v,dfn)\)序列的大小为\(i\),值域为\([j,m]\)的方案数。边界\(dp_{0,j}=1\),目标\(dp_{n,1}\)。枚举\(dfn_x=1\)\(x\)\(v_x\)两个东西,就可以转移了,注意到若左右各找一个方案,那么左右两半内部的\(dfn\)相对大小固定,而跨越左右两半的\(dfn\)相对大小無·所·謂,将它们的\(dfn\)值域共同扩展,还要乘上一个组合数。

状态转移方程:

\[dp_{i,j}=\sum_{k=1}^i\sum_{o=j}^mdp_{k-1,o+1}dp_{i-k,j}\binom{i-1}{k-1} \]

不过这样时间复杂度是\(\mathrm O\!\left(n^4\right)\)的(视\(n,m\)同阶),然鹅很好优化,移一下\(\sum\)得到

\[dp_{i,j}=\sum_{k=1}^idp_{i-k,j}\binom{i-1}{k-1}\sum_{o=j}^mdp_{k-1,o+1} \]

则实时维护\(\forall i\in[0,n],dp_i\)的前缀和,后面那个\(\sum\)调用一下前缀和复杂度就少一个\(n\)了。

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=300,M=300;
int n,m,mod;
int comb[N+1][N+1];
int dp[N+1][M+2];
int Sum[N+1][M+2];
int main(){
	cin>>n>>m>>mod;
	comb[0][0]=1;
	for(int i=1;i<=n;i++)for(int j=0;j<=i;j++)comb[i][j]=((j?comb[i-1][j-1]:0)+(j<i?comb[i-1][j]:0))%mod;//预处理组合数 
	for(int i=1;i<=m+1;i++)Sum[0][i]=(Sum[0][i-1]+(dp[0][i]=1))%mod;//边界 
	for(int i=1;i<=n;i++){//转移 
		for(int j=1;j<=m;j++){
			for(int k=1;k<=i;k++)(dp[i][j]+=1ll*dp[i-k][j]*comb[i-1][k-1]%mod*(Sum[k-1][m+1]-Sum[k-1][j])%mod)%=mod;//转移方程 
			Sum[i][j]=(Sum[i][j-1]+dp[i][j])%mod;//维护前缀和 
//			printf("dp[%d][%d]=%d\n",i,j,dp[i][j]);
		}
		Sum[i][m+1]=Sum[i][m];//维护前缀和 
	}
	cout<<(dp[n][1]+mod)%mod;//目标 
	return 0;
}

猜你喜欢

转载自www.cnblogs.com/ycx-akioi/p/AtCoder-agc024-e.html