7375. 【2021.11.11NOIP提高组联考】叁仟柒佰万
Description
给定一个序列 \(a\) ,你需要把它划分成任意多段,满足任意一段的 \(mex\) 值相同,求方案数,对 \(10^{9}+7\) 取模。
定义一个区间的 \(mex\) 为区间中最小的没有出现过的自然数。
\(1\le n\le 37000000\)。
Solution
首先有个结论,就是最后划分的各个区间的 \(mex\) 跟整个区间的 \(mex\) 是一样的。
证明:
令全局 \(mex\) 为 \(K\), 则说明 \(0 \sim K-1\) 的数都存在于数列中而 \(K\) 不存在。
假设我们最后每个区间的 \(mex\) 均为 \(X\)。
若 \(X<K\), 由于序列中为 \(X\) 的数存在, \(X\) 必定在其中一个区间中, 与所有区间 \(mex=X\) 矛盾。
若 \(X>K\), 由于不存在 \(K\), 显然不合法。
于是只能是 \(X=K\) 。
知道了这个结论,\(\text{dp}\) 转移式子就好搞了。
设 \(f_i\) 表示到 \(i\) 结尾的方案数,转移方程:
\(f_i=\sum_{1\le j<i} f_j[get(j,i)=mex]\)(令 \(get(j,i)\) 表示区间 \(j\sim i\) 的 \(mex\))。
注意到 \(get(j,i)\) 在某个 \(j\) 使得 \(get(j,i)=mex\) 后,保持不变。
因此我们只需要在 \(\mathcal{O}(n)\) 的时间里求出 \(j\),然后用前缀和更新答案。
至于求出 \(j\),可以使用桶来处理。
题外话:至于 \(n\) 的范围为什么是 37000000,原因如下:
题目背景:当年陈刀仔,他用 20 块贏到三千七百万,今天我也要从 \(n=10\) 出到 \(n=37000000\) !
Code
#include<cstdio>
#include<cstring>
#include<algorithm>
#define mod 1000000007
#define N 37000005
using namespace std;
int T,n,x,y,mex,ans,num,now,fs,a[N],sum[N],near[N];
bool flag,t[N];
int getmex()
{
int res=0;
while (t[res]) ++res;
return res;
}
int main()
{
freopen("clods.in","r",stdin);
freopen("clods.out","w",stdout);
scanf("%d",&T);
while (T--)
{
scanf("%d",&n);
mex=0;num=0;
if (n!=37000000)
for (int i=1;i<=n;++i)
scanf("%d",&a[i]);
else
{
scanf("%d%d",&x,&y);
for (int i=2;i<=n;++i)
a[i]=(a[i-1]*x+y+i)&262143;
}
for (int i=0;i<=n;++i)
t[i]=0;
for (int i=1;i<=n;++i)
t[a[i]]=1;
mex=getmex();
if (mex==0)
{
ans=1;
for (int i=1;i<n;++i)
ans=ans*2%mod;
printf("%d\n",ans);
continue;
}
for (int i=0;i<mex;++i)
t[i]=0,near[i]=-1;
for (int i=1;i<=n;++i)
if (a[i]<mex)
{
if (!t[a[i]]) t[a[i]]=1,++num;
if (num==mex)
{
fs=i;
break;
}
}
for (int i=0;i<=n;++i)
t[i]=0,sum[i]=0;
flag=false;now=1;ans=0;
for (int i=1;i<=n;++i)
{
if (a[i]<mex)
{
if (near[a[i]]!=-1) t[near[a[i]]]=0;
near[a[i]]=i;
t[i]=1;
}
if (i==fs) flag=true;
while (flag&&!t[now]) ++now;
if (flag) ans=sum[now-1]+1;
if (ans==mod) ans=0;
sum[i]=(sum[i-1]+ans)%mod;
}
printf("%d\n",ans);
}
return 0;
}