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}\),转移:
发现后面乘上的系数只和 \(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;
}
}