[NOI2018]冒泡排序
太仙了,完全不会做;
但是为什么能过题呢,因为可以快乐的瞪眼找规律。
读一下题目后面的证明就知道,交换次数达到这个\(\frac{1}{2}\sum_{i=1}^n|i-p_i|\)的下界需要使得每一次交换的两个数\(p_j,p_{j+1}\)里自己的位置更近一下;所以如果存在某一个数,在冒泡的过程中向前移动后又向后移动,或者向后移动后又向前移动的话,那么就一定GG了
不难发现一个数先向后移动后又向前移动是不可能的,于是我们考虑一下什么样的情况会使得一个数先向前移动后又向后移动,如果前面有一个比他大的数,那么这个位置就会向前移动,如果后面有一个比他小的数就会向后移动;所以一旦存在\(i<j<k\)满足\(p_i>p_j>p_k\),那么\(p_j\)就会先前移再后移;换句话说,合法排列不存在长度大于\(2\)的下降子序列
于是现在我们可以写一个状压了,就能拿到高于爆搜的分数了;
之后好像就完全没有头绪了,于是我们写个爆搜打表,发现确实很有规律;
当\(n=4\)的时候,爆搜告诉我们以\(1,2\)开头的合法序列是一样多的,都是\(5\)个;以\(3\)开头的合法排列是\(3\)个,以\(4\)开头的合法排列有\(1\)个。
当\(n=5\)的时候,以\(1,2\)开头的合法序列还是一样多的,都是\(14\)个;以\(3\)开头的是\(9\)个,以\(4\)开头是\(4\)个,以\(5\)开头是\(1\)个。
\(9+5=14,4+5=9\);等等,这太有规律了,我们设\(f(i,j)\)表示长度为\(i\)以\(j\)开头的合法排列的数量,好像有\(f(i,j)=f(i,j+1)+f(i-1,j-1)\),而\(f(i,1)=f(i,2)\)
发现\(n=6,7,8...\)的时候好像也满足这个规律。
考虑这个结论的正确性;
首先\(f(i,1)=f(i,2)\)还是非常显然的,把\(1\)和\(2\)交换一下位置就能实现相互转化了;
对于\(f(i,j)=f(i,j+1)+f(i-1,j-1)\),我们不难将其写成\(f(i,j)=\sum_{k=j-1}^{i-1}f(i-1,k)\);我们分类讨论一下第二个位置填什么
-
当\(j\)后面填\([2,j)\)时,由于\(1\)肯定在第二个位置后面,这样第一个位置、第二个位置以及1所在的位置构成了一个长度为\(3\)的下降序列,这一定是不合法的
-
当\(j\)后面填\((j,n]\)时,\(j\)和第二个位置不会形成下降序列,由于第二个位置比\(j\)大,所以更容易形成下降序列,于是我们完全不需要考虑\(j\)的影响,只需要钦定一个合法排列放在后面就好了,由于\(j\)后面跟的数至少是第\(j+1\)大,也就是至少得是去掉\(j\)后的第\(j\)大;显然和长度\(i-1\)首位数取值\([j,i-1]\)的合法排列是一一对应的,这部分的贡献是\(\sum_{k=j}^{i-1}f(i-1,k)\),即\(f(i,j+1)\)
-
当\(j\)后面填\(1\)的时候,不太会证明的样子,但是可以给出一种构造方法;我们将所有长度为\(i-1\)首位是\(j-1\)的合法排列整体加\(1\),之后将\(1\)放在\(j\)后面即可,显然不会出现长度大于\(2\)的下降子序列;这部分的贡献是\(f(i-1,j-1)\)
于是我们可以在\(O(n^2)\)的时间内求出\(f\)了,但是看起来好像根本没有什么用,我们还是不知道如何在字典序限制下统计答案。
通过对着打出来的表瞪眼,又能发现一些规律;对于\(n=5\)的情况,我们康康首位为\(2\)的合法排列
2 1 3 4 5
2 1 3 5 4
2 1 4 3 5
2 1 4 5 3
2 1 5 3 4
2 3 1 4 5
2 3 1 5 4
2 3 4 1 5
2 3 4 5 1
2 3 5 1 4
2 4 1 3 5
2 4 1 5 3
2 4 5 1 3
2 5 1 3 4
第二位是\(1\)的有\(5\)个,第二位是\(3\)的有\(5\)个,第二位是\(4\)的有\(3\)个,第二位是\(5\)的有\(1\)个;
哎,非常巧合的是\(f(4,1)=5,f(4,2)=5,f(4,3)=3,f(4,1)=1\),这怎么又一一对应起来了;思考一波发现其实挺显然的,这不是和我们的构造方法正好相同吗。
有了这样的性质我们就可以像数位dp那样卡位了,卡位的时候我们需要求\(n\)次后缀和,后缀和可以转化为下一行的单点值,看起来非常科学了;但是问题是,我们得知道每一位是什么才能卡位吧;
通过上面的构造方法,不难总结出下面的两条规律,设当前卡好的最后一位是\(t\),卡好的位中最大值为\(mx\)
-
当前面卡好的位没有\(1\)的时候,后面一位会是会是\(1,t+1,t+2....n\)
-
如果有\(1\),那么后面第一位会是没有卡好的数中的最小值,之后是\(mx+1,mx+2...n\)
当然\(t+1,t+2...n\),\(mx+1,mx+2...n\)都不能是在之前卡好的位中出现过的数,我们利用一个树状数组就能求出当前这一位的排名。
于是我们只要我们能快速求\(f\)的单点值这道题就做完了。
设\(f(i,j)=g(i-1,i-j)\),那么对于\(g\)存在\(g(i,j)=g(i-1,j)+g(i,j-1)\)。放到坐标系里我们就会发现\(g(i,j)\)等价于从\((0,0)\)只能向上向右走,不能超过直线\(y=x\)走到\((i,j)\)的方案数,根据常识这个方案数是\(\binom{i+j}{i}-\binom{i+j}{i+1}\),于是我们预处理阶乘以及阶乘逆元就能快速询问单点值了。
之后我们就这样奥妙重重地做完这道奥妙重重的题目了。
代码
#include<bits/stdc++.h>
#define re register
#define max(a,b) ((a)>(b)?(a):(b))
#define min(a,b) ((a)<(b)?(a):(b))
#define lb(i) ((i)&(-i))
inline int read() {
char c=getchar();int x=0;while(c<'0'||c>'9')c=getchar();
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-48,c=getchar();return x;
}
const int maxn=6e5+5;
const int mod=998244353;
inline int qm(int x) {return x>=mod?x-mod:x;}
inline int dqm(int x) {return x<0?x+mod:x;}
int a[maxn],c[maxn],n,mx[maxn],mn[maxn];
int fac[maxn<<1],ifac[maxn<<1];
inline int ksm(int a,int b) {
int S=1;for(;b;b>>=1,a=1ll*a*a%mod)if(b&1)S=1ll*S*a%mod;return S;
}
inline int C(int n,int m) {
return m>n?0:1ll*fac[n]*ifac[m]%mod*ifac[n-m]%mod;
}
inline int f(int n,int m) {
m=n-m;n--;return dqm(C(n+m,n)-C(n+m,n+1));
}
inline void add(int x,int v) {
for(re int i=x;i<=n;i+=lb(i))c[i]+=v;
}
inline int ask(int x) {
int tot=0;for(re int i=x;i;i-=lb(i))tot+=c[i];
return tot;
}
inline int calc(int l,int r) {return r<l?0:ask(r)-ask(l-1);}
int main() {
fac[0]=ifac[0]=1;int lim=(maxn+maxn-5);
for(re int i=1;i<=lim;i++) fac[i]=1ll*fac[i-1]*i%mod;
ifac[lim]=ksm(fac[lim],mod-2);
for(re int i=lim-1;i;--i) ifac[i]=1ll*ifac[i+1]*(i+1)%mod;
for(re int T=read();T;--T) {
n=read();for(re int i=1;i<=n;i++)a[i]=read();
if(n==1){puts("0");continue;}
for(re int i=1;i<=n;i++)mx[i]=max(mx[i-1],a[i]);
mn[n+1]=n+1;for(re int i=n;i;i--)mn[i]=min(mn[i+1],a[i]);
for(re int i=1;i<=n;i++)c[i]=0;
for(re int i=1;i<=n;i++)add(i,1);
int ans=0,flag=0;add(a[1],-1);
ans=f(n+1,a[1]+2),flag=(a[1]==1);
for(re int i=2;i<=n;i++) {
int x=n-i+2,tot=0,pos=0,k=0;
if(!flag) {
tot=1+calc(a[i-1]+1,n);
pos=(1<a[i])+calc(a[i-1]+1,a[i]-1);
k=(a[i]==1)|(a[i]>a[i-1]);
}else {
int t=mn[i];
if(t!=mx[i-1]+1) ++tot;
tot+=calc(mx[i-1]+1,n);
pos=(t<a[i]&&t!=mx[i-1]+1)+calc(mx[i-1]+1,a[i]-1);
k=(t==a[i])|(a[i]>mx[i-1]);
}
pos++;if(pos>tot) break;pos=x+pos-tot-1;
if(k) ans=qm(ans+f(x,pos+2));
else {ans=qm(ans+f(x,pos+1));break;}
add(a[i],-1);flag|=(a[i]==1);
}
printf("%d\n",ans);
}
}