CTSC2018 青蕈领主
CTSC2018 青蕈领主
题意
小绿同学因为微积分这门课,对“连续”这一概念产生了浓厚的兴趣。小绿打算把连续的概念放到由整数构成的序列上,他定义一个长度为\(m\)的整数序列是连续的,当且仅当这个序列中的最大值与最小值的差,不超过\(m−1\)。例如 \(\{1,3,2\}\)是连续的,而\(\{1,3\}\)不是连续的。
某天,小绿的顶头上司板老大,给了小绿\(T\)个长度为\(n\)的排列。小绿拿到之后十分欢喜,他求出了每个排列的每个区间是否是他所定义的“连续”的。然而,小绿觉得被别的“连续”区间包含住的“连续”区间不够优秀,于是对于每个排列的所有右端点相同的“连续”区间,他只记录下了长度最长的那个“连续”区间的长度。也就是说,对于板老大给他的每一个排列,他都只记录下了在这个排列中,对于每一个\(1 \leq i \leq n\),右端点为\(i\)的最长“连续”区间的长度\(L_i\)。显然这个长度最少为\(1\),因为所有长度为\(1\)的整数序列都是连续的。
做完这一切后,小绿爬上绿色床,美美地做了一个绿色的梦。
可是第二天醒来之后,小绿惊讶的发现板老大给他的所有排列都不见了,只剩下他记录下来的\(T\)组信息。小绿知道自己在劫难逃,但是作为一个好奇的青年,他还是想知道:对于每一组信息,有多少个和信息符合的长度为\(n\)的排列。
由于小绿已经放弃治疗了,你只需要告诉他每一个答案对\(998244353\)取模的结果。
我们并不保证一定存在至少一个符合信息的排列,因为小绿也是人,他也有可能犯错。
题解
如此神仙的一道题……技不如人技不如人啊……
为了真正的理解这道题,我们先了解几个定义和引理(以下都已数组\(a\)为例):
\(Definition\) \(1:\)
连续区间,指对于一个区间\([l,r]\),区间中的数在排序之后是连续的。即\(Max\{a_l,a_{l+1},\dots,a _r\}-Min\{a_l,a_{l+1},\dots,a_r\}=r-l\)。
\(Definition\) \(2:\)
极长连续区间,指对于一个左端点\(l\),极长连续区间\([l,r]\)是所有以\(l\)为左端点的合法的连续区间中,\(r\)最大的一个区间。(题目中的\(L_i\)就是右端点为\(a_i\)的极长连续区间的左端点)
\(Theorem\) \(1:\)
对于一个排列,其所有的极长连续区间都只存在包含关系,不存在相交但是不相互包含的关系。
\(Proof:\)
证明比较显然,如果存在两个极长连续区间\([l1,r1]\)和\([l2,r2]\)相交,那么这\([l1,r2]\)这个区间也是极长连续区间,与极长连续区间的定义就会存在矛盾。所以任何两个极长连续区间都不会相交而不包含。
\(Theorem\) \(2:\)
对于一个排列,\([1,n]\)一定是一个极长连续区间。(这个应该不用多说了)
根据这些,我们就可以推导出,在原题中,只要不满足引理1和2,那么满足条件的排列个数就一定为零。
\(Theorem\) \(3:\)
我们把每一个极长连续区间看作一个节点,对于一个极长连续区间\([l,r]\),我们找到包含这个区间的最短的极长连续区间,在两点之间连边,形成的是一个树型结构。
(实际上这个树型结构\(WC2019\)上\(LCA\)已经讲过,应该是叫析合树)。
\(Proof:\)
这个也不用过多的证明,实际上自己画一画就知道了,除了\([1,n]之外,每个极长连续区间都可以找到一个包含它的最短极长连续区间\)。
构建出了这个树型结构,我们就可以着手进行计算了。
\(Definition\) \(3:\)
记\(f_n\)表示对于长度为\(n+1\),并且满足\(L_i=1(i \in [1,n])\)的排列的个数。
我们讲一个连续区间\([l,r]\)拿出来,并且保持\(L_i\)不变,那么这个区间相当于一个长度为\(r-l+1\)的排列,我们记这段区间的答案为\(P[l,r]\),那么原题的答案就是\(P[1,n]\)。
\(Theorem\) \(4:\)
如果一个极长连续区间的儿子节点有\(k\)个,分别为\([s_0,s_1-1],[s_1,s_2-1],\dots,[s_{k-1},s_k-1]\)(\(s_0=l,s_k=r\)),那么\(P[l,r]=f_k * \prod_{i=0}^{k-1}P[s_i,s_{i+1}-1]\)。
\(Proof:\)
对于一个极长连续区间\([s_i,s_{i+1}-1]\),这个区间中元素的值是连续的,所以我们可以将这一段区间缩成一个点,那么整个极长连续区间\([l,r]\)就被分成了\(k+1\)段(包括\([r,r]\)),我们将这\(k+1\)段按照大小的关系进行排序并且离散化一下,那么这个区间就变成了一个长度为\(k+1\)的排列了。
这样变化了之后,我们会发现,这个排列中,除了区间\([r,r]\)所对应的数能够有长度为\(r-l+1\)的极长连续区间,其余的极长连续区间长度都为\(1\),那么满足这个排列的方案数就是\(f_k\)了。然后虽然每一段子区间被离散化了,但是每一段子区间也有不同的排列方法,所以根据乘法原理,区间\([l,r]\)的答案还需要乘上每一段区间的答案。
\(Theorem\) \(5:\)
记\(c_i\)为以\(i\)结尾的极长连续区间的孩子节点的个数,那么答案就是\(\prod_{i=1}^n f(c_i)\)。
\(Proof:\)
这个比较简单,直接将引理4的公式展开来,得到的\(P[1,n]\)就是如上的公式。
至此,假设我们已经知道了如何求出每一个极长连续区间的孩子节点的个数,那么接下来求出\(f_i\)即可。事实上,难点也就在于如何求\(f_i\)
\(Theorem\) \(5:\)
\(f_n=f_{n-1}*(n-1)+\sum_{i=2}^{n-2}f_i*(i-1)*f_{n-i}\)
\(Proof:\)
直接算\(f_i\)似乎有点困难,我们尝试转换一下模型,对于一个长度为\(n\)的排列,令\(b_{a_i}=i\),然后我们会发现,原排列的连续子区间也对应着\(b\)数组中的连续子区间,并且原排列中包含排列最后一个数的极长连续区间,在\(b\)数组中,对应的是包含\(n\)的极长连续区间。于是\(f_i\)在\(b\)数组中的定义就可以变为除了包含最大值的极长连续子区间外,没有长度超过\(1\)的极长连续子区间的排列的个数。这种定义与之前\(f_i\)的定义是等价的。
然而这似乎还是无法入手,我们尝试从一个已经已有排列开始进行构造。我们发现,如果一个长度为\(n\)排列在新加入一个数\(n+1\)之后,能够变成一个合法的排列,当且仅当一下两种情况:
\(1.\)这个排列本身就是合法的。这样的话,我们可以将原排列中,每一个元素的值都加上\(1\),然后将\(1\)插入到该排列中与\(2\)不相邻的位置,就能够保证新排列仍然是合法的。这样原排列的种类数是\(f_{n-1},1\)可以插入的位置一共有\(n-1\)种,所以贡献就是\(f_{n-1}*(n-1)\)。
\(2.\)这个排列当前不合法,但是这个排列中仅有一个极长连续区间不包含最大值,并且长度大于\(1\)。(因为如果存在两个这样的极长连续区间,根据引理可以得知这两个区间没有交,那么加入一个数一定不能让这个排列变得合法。)这个时候,这个极长连续区间的长度取值为\([2,n-2]\),我们假设长度为\(l\),我们考虑向这个极长连续区间中,插入这个区间的最大值,并且把排列中所有大于等于这个最大值的数+\(1\),来使这个排列合法。这样,原来的极长连续区间相当于变成了一个长度为\(l+1\)的合法区间,而其合法的值域范围为个数为\(n-l+1\)个,这一部分的方案数就是\(f_{l+1}*(n-l+1)\)。剩余的数构成了长度为\(n-l\)的合法区间,方案数为\(f_{n-l}\)。
这样总贡献就是:
这个递推式就可以用分治\(FFT\)来优化,时间复杂度为\(O(nlog^2(n))\)。
这样我们就可以预处理出\(f_i\)了,接下来考虑如何求每个极长连续区间的孩子节点个数。我们利用单调栈来维护,每次判断栈顶元素是否在当前区间\([L_i,i]\)内,如果在内则加上贡献,将其弹出。同时也可以判断极长连续区间相交的无解情况。这一部分的复杂度就是\(O(n)\)的,总共的复杂度为\(O(nlog^2(n)+n)\)
Code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int Md=998244353;
const int N=1e5+500;
inline int Add(const int &x,const int &y) {return (x+y)>=Md?(x+y-Md):(x+y);}
inline int Sub(const int &x,const int &y) {return (x-y)<0?(x-y+Md):(x-y);}
inline int Mul(const int &x,const int &y) {return (ll)x*y%Md;}
namespace NTT {
int Powe(int x,int y) {
int ans=1;
while(y) {
if(y&1) ans=Mul(ans,x);
x=Mul(x,x);
y>>=1;
}
return ans;
}
inline int Div(const int &x,const int &y) {return Mul(x,Powe(y,Md-2));}
int rev[N<<2|1];
void DFT(vector<int>&A,int len) {
for(int i=0;i<len;i++) if(i<rev[i]) swap(A[i],A[rev[i]]);
for(int i=1;i<len;i<<=1) {
int wn=Powe(3,(Md-1)/(i<<1));
for(int j=0;j<len;j+=i<<1) {
int nw=1,x,y;
for(int k=0;k<i;k++,nw=Mul(nw,wn)) {
x=A[j+k],y=Mul(nw,A[i+j+k]);
A[j+k]=Add(x,y);A[i+j+k]=Sub(x,y);
}
}
}
}
void IDFT(vector<int>&A,int len) {
reverse(A.begin()+1,A.end());
int Inv=Powe(len,Md-2);
DFT(A,len);
for(int i=0;i<len;i++) A[i]=Mul(A[i],Inv);
}
vector<int> MUL(vector<int>A,vector<int>B) {
int n=A.size(),m=B.size(),len;
for(len=1;len<n+m-1;len<<=1);
A.resize(len);B.resize(len);
for(int i=0;i<len;i++) rev[i]=(rev[i>>1]>>1)|((i&1)?(len>>1):0);
DFT(A,len);DFT(B,len);
for(int i=0;i<len;i++) A[i]=Mul(A[i],B[i]);
IDFT(A,len);
A.resize(n+m-1);
return A;
}
}
vector<int>F,A,B,Ans;
int n,T,tp;
int st[N],l[N],c[N];
void Solve(int l,int r) {
if(l==r) {
F[l]=Add(F[l],Mul((l-1),F[l-1]));
return ;
}
int mid=(l+r)>>1;
Solve(l,mid);
A.clear();B.clear();
for(int i=l;i<=mid;i++) A.push_back(Mul(i-1,F[i])),B.push_back(F[i]);
Ans=NTT::MUL(A,B);
for(int i=mid+1;i<=r;i++) {
if(i>=l*2) F[i]=Add(F[i],Ans[i-2*l]);
}
if(l!=2) {
A.clear();B.clear();
for(int i=2;i<=min(l-1,r-l);i++) A.push_back(F[i]);
for(int i=l;i<=mid;i++) B.push_back(F[i]);
Ans=NTT::MUL(A,B);
for(int i=mid+1;i<=r;i++) {
if(i>=l+2) F[i]=Add(F[i],Mul(i-2,Ans[i-l-2]));
}
}
Solve(mid+1,r);
}
int main() {
scanf("%d%d",&T,&n);
F.resize(n+1);F[0]=1;F[1]=2;
Solve(2,n);
while(T--) {
memset(c,0,sizeof c);
tp=0;
int fl=0;
for(int i=1;i<=n;i++) scanf("%d",&l[i]);
for(int i=1;i<=n;i++) {
while(tp&&st[tp]>=i-l[i]+1) {
if(st[tp]-l[st[tp]]+1<i-l[i]+1) {
fl=1;
break;
}
tp--;c[i]++;
}
st[++tp]=i;
}
if(fl||l[n]!=n) puts("0");
else {
int ans=1;
for(int i=1;i<=n;i++) ans=Mul(ans,F[c[i]]);
printf("%d\n",ans);
}
}
return 0;
}