Atcoder Grand Contest 024

024E Sequence Growing Hard

题目描述

点此看题

解法

首先转化一下题目的条件,发现 \(A_i\)\(A_{i+1}\) 的子序列和 \(A_i\) 的字典序小于 \(A_{i+1}\) 这两个条件可以有机地联系在一起,考虑从 \(A_{i+1}\) 删除 \(1\) 个元素得到 \(A_i\),并且删除的元素 \(j\) 需要满足 \(A_{i+1,j}<A_{i+1,j+1}\),或者 \(j\) 的末尾的元素。考虑满足这个条件显然能删,并且对于一段连续的相同元素,我们只需要考虑删去最后一个即可,所以字典序一定是在 \(j\) 这里比较出来的!

考虑 \(dp\) 计数,设 \(f_{i,j}\) 表示长度为 \(i\) 的序列,只填入 \([1,j]\) 中的数的方案数。考虑有两种可能的转移方式(这是做题的思路):要么我们枚举当前被删除的数,要么我们枚举某个数被删除的时间。第一种想法会产生形如 \(s_i<s_{i+1}\) 的限制,但是对于这个限制并不好下手,我们考虑使用第二种思路。

那么枚举哪个数呢?枚举最小值、最大值?题解给出了一种惊为天人的想法:枚举 \(1\),仔细想想它的优点是明显的,那就是我们不用规划这个数的方案,它直接就是 \(1\) 了。但是这种想法能起作用的最大原因就在于状态的第二维——这使得转化到子问题是容易的。

现在开始写转移,首先如果序列中不存在 \(1\),那么 \(f_{i,j}\leftarrow f_{i,j-1}\),也就是我们把非 \(1\) 的数值都向左平移一位,因为本题的限制是大小关系,所以方案数是对得上的。

考虑如果序列中存在 \(1\),那么我们枚举 \(1\) 第一次出现的位置 \(k\),那么单独考虑 \([1,k)\) 的方案就是 \(f_{k-1,j-1}\),单独考虑 \((k,i]\) 的方案数是 \(f_{i-k,j}\),我们再枚举 \(1\) 的删除时间 \(p\),因为 \(k\) 的前后两部分没有之间的限制,所以只要计算删除时间的方案它们就是独立的(此情况下删除时间不同说明序列组不同),注意 \((k,i]\) 的数必须先于 \(1\) 删除,所以方案数是 \({p-1\choose i-k}\),转移:

\[f_{i,j}\leftarrow \sum_{k=1}^i f_{k-1,j-1}\cdot f_{i-k,j}\cdot \sum_{p}{p-1\choose i-k} \]

发现后面乘上的系数只和 \(i,k\) 有关,所以可以提前预处理,那么时间复杂度 \(O(n^3)\)

总结

写转移时的维度观念很重要,比如本题的大维度就是时间和元素,那么从其中哪个角度入手导出了不同的思路。常见写 \(dp\) 的思路就是:减小一个维度的大小,再考虑这样做带来的影响,维度决定了转移顺序。

枚举特殊值来转移,不仅可以枚举最大值最小值,也可以枚举 \(1\) 这种极其特殊的常数,这样可以省去一些计数的麻烦,当然前提条件是本题的限制是基于数的大小关系的。

#include <cstdio>
const int M = 305;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,k,p,C[M][M],t[M][M],f[M][M];
void add(int &x,int y) {x=(x+y)%p;}
void init()
{
	for(int i=0;i<=n;i++)
	{
		C[i][0]=1;
		for(int j=1;j<=i;j++)
			C[i][j]=(C[i-1][j-1]+C[i-1][j])%p;
	}
	for(int i=1;i<=n;i++)
		for(int k=1;k<=i;k++)
			for(int j=1;j<=i;j++)
				add(t[i][k],C[j-1][i-k]);
}
signed main()
{
	n=read();k=read();p=read();init();
	f[0][0]=1;
	for(int j=1;j<=k;j++)
	{
		f[0][j]=1;
		for(int i=1;i<=n;i++)
		{
			for(int k=1;k<=i;k++)
			{
				int s=f[k-1][j-1]*f[i-k][j]%p;
				add(f[i][j],s*t[i][k]%p);
			}
			add(f[i][j],f[i][j-1]);
		}
	}
	printf("%lld\n",f[n][k]);
}

024F Simple Subsequence Problem

题目描述

点此看题

解法

字符串的总长很短,所以可以考虑搞出每个字符串 \(A\) 作为是 \(S\) 中多少个串的子序列,那么求答案的时候枚举一下所有字符串 \(A\) 即可。

我们先来回忆一下怎么匹配子序列的(判定方法),对于字符串 \(A\),如果下一个字符是 \(c\),那么就在 \(B\) 中找到第一个 \(c\),然后把 \(B\) 的这个前缀删去,再匹配下一个字符。

考虑不同的 \(A/B\) 匹配方式是一样的,这启发我们使用整体 \(dp\),设 \(f(A|B)\) 表示已匹配了字符串 \(A\),剩下可用的匹配串是 \(B\),方案数是多少。那么初始化 \(f(\varnothing|S_i)=1\) 表示可以从 \(S\) 中的任意一个字符串为起点开始匹配,最后 \(A\) 的次数就是 \(f(A|\varnothing)\),表示它能被多少 \(S\) 中的字符串匹配。

转移考虑枚举下一位填 \(0/1\),然后按照判定方法模拟即可。因为 \(|A|+|B|\leq n\),所以总状态数大约是 \(O(2^n)\) 这么多,但是本题的难点是把状态记录下来。因为有前导 \(0\) 我们需要知道 \(|A|,|B|\),那么我们再开一维 \(k=|B|\) 记录长度,然后用一个 \(1\) 来标记这个状态的起始点,这个 \(1\) 后面的东西就是有效状态。

时间复杂度 \(O(n2^n)\),但是因为本题全是位运算所以应该没有这么严格,实现不太优美也是可以的。

#include <cstdio>
const int M = 21;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,f[1<<M][M];char s[1<<M];
signed main()
{
	n=read();m=read();
	for(int i=0;i<=n;i++)
	{
		scanf("%s",s);
		for(int j=0;j<(1<<i);j++)
			f[(1<<i)|j][i]=(s[j]=='1');
	}
	for(int i=n;i>=1;i--) for(int j=0;j<(1<<i);j++)
		for(int k=i;k>0;k--) if(f[(1<<i)|j][k])
		{
			int t=f[(1<<i)|j][k],S=j>>k,T=j&((1<<k)-1);
			f[(1<<i-k)|S][0]+=t;
			if(T)//add 1
			{
				int u=k-1;
				while(!(T>>u&1)) u--;
				f[(1<<i-k+u+1)|(S<<u+1)|(1<<u)
				|(T&((1<<u)-1))][u]+=t;
			}
			if((~T)&((1<<k)-1))
			{
				int u=k-1;
				while(T>>u&1) u--;
				f[(1<<i-k+u+1)|(S<<u+1)
				|(T&((1<<u)-1))][u]+=t;
			}
		}
	for(int i=n;i>=0;i--)
		for(int j=0;j<(1<<i);j++) if(f[(1<<i)|j][0]>=m)
		{
			for(int k=i-1;k>=0;k--)
				printf("%d",j>>k&1);
			puts("");
			return 0;
		}
}
posted @ 2022-03-18 16:28  C202044zxy  阅读(75)  评论(2编辑  收藏  举报