「NOI2018」冒泡排序
题目描述
最近,小S 对冒泡排序产生了浓厚的兴趣。为了问题简单,小 S 只研究对 1 到n 的排列的冒泡排序。
下面是对冒泡排序的算法描述。
输入:一个长度为n 的排列p[1...n]
输出:p 排序后的结果。
for i = 1 to n do
for j = 1 to n - 1 do
if(p[j] > p[j + 1])
交换p[j] 与p[j + 1] 的值
冒泡排序的交换次数被定义为交换过程的执行次数。可以证明交换次数的一个下 界是$frac{1}{2} sum_{i=1}^n |i-p_i|$其中$p_i$ 是排列p 中第i 个位置的数字。如果你对证明感兴趣,可以看提示。
小S 开始专注于研究长度为n 的排列中,满足交换次数= $frac{1}{2} sum_{i=1}^n |i-p_i|$的排列 (在后文中,为了方便,我们把所有这样的排列叫“好”的排列)。他进一步想,这样的排列到底多不多?它们分布的密不密集? 小S 想要对于一个给定的长度为n 的排列q,计算字典序严格大于q 的“好”的 排列个数。但是他不会做,于是求助于你,希望你帮他解决这个问题,考虑到答案可能会很大,因此只需输出答案对998244353 取模的结果。
输入输出格式
输入格式:从文件inverse.in 中读入数据。
输入第一行包含一个正整数T,表示数据组数。
对于每组数据,第一行有一个正整数n, 保证$n leq 6 imes 10^5$。
接下来一行会输入n 个正整数,对应于题目描述中的qi,保证输入的是一个1 到 n 的排列。
输出格式:输出到文件inverse.out 中。
输出共T 行,每行一个整数。
对于每组数据,输出一个整数,表示字典序严格大于q 的“好”的排列个数对 998244353 取模的结果。
输入输出样例
说明
下面是对本题每个测试点的输入规模的说明。
对于所有数据,均满足T = 5 (样例可能不满足).
记$n_{max}$ 表示每组数据中n 的最大值,
$sum n$ 表示所有数据的n 的和。
测试点 | $n_{max}=$ | $sum nleq$ | 特殊性质 | 测试点 | $n_{max}=$ | $sum nleq$ | 特殊性质 |
---|---|---|---|---|---|---|---|
1 | $8$ | $5n_{max}$ | 无 | 13 | $144$ | $700$ | 无 |
2 | $9$ | $5n_{max}$ | 无 | 14 | $166$ | $700$ | 无 |
3 | $10$ | $5n_{max}$ | 无 | 15 | $200$ | $700$ | 无 |
4 | $12$ | $5n_{max}$ | 无 | 16 | $233$ | $700$ | 无 |
5 | $13$ | $5n_{max}$ | 无 | 17 | $777$ | $4000$ | $forall i ~~p_i=i$ |
6 | $14$ | $5n_{max}$ | 无 | 18 | $888$ | $4000$ | 无 |
7 | $16$ | $5n_{max}$ | 无 | 19 | $933$ | $4000$ | 无 |
8 | $16$ | $5n_{max}$ | 无 | 20 | $1000$ | $4000$ | 无 |
9 | $17$ | $5n_{max}$ | 无 | 21 | $266666$ | $2000000$ | $forall i ~~p_i=i$ |
10 | $18$ | $5n_{max}$ | 无 | 22 | $333333$ | $2000000$ | 无 |
11 | $18$ | $5n_{max}$ | 无 | 23 | $444444$ | $2000000$ | 无 |
12 | $122$ | $700$ | $forall i ~~p_i=i$ | 24 | $555555$ | $2000000$ | 无 |
. | . | . | . | 25 | $600000$ | $2000000$ | 无 |
下面是对交换次数下界是$frac{1}{2} sum_{i=1}^n |i-p_i|$的证明。
排序本质上就是数字的移动,因此排序的交换次数应当可以用数字移动的总距离 来描述。对于第i 个位置,假设在初始排列中,这个位置上的数字是$p_i$,那么我们需要将这个数字移动到第pi 个位置上,移动的距离是$|i-p_i|$。从而移动的总距离就是$sum_{i=1}^n |i-p_i|$,而冒泡排序每次会交换两个相邻的数字,每次交换可以使移动的总距离至多减少2。因此$frac{1}{2} sum_{i=1}^n |i-p_i|$是冒泡排序的交换次数的下界。
并不是所有的排列都达到了下界,比如在n = 3 的时候,考虑排列3 2 1, 这个排 列进行冒泡排序以后的交换次数是3,但是$frac{1}{2} sum_{i=1}^n |i-p_i|$ 只有2。
【样例1 解释】 字典序比1 3 2 大的排列中,除了3 2 1 以外都是“好”的排列,故答案为3。
题解
参照liuzhangfeiabc的题解
在NOI考场上是一道绝好的打表找规律题,打表观察性质。
题目可以转化为:要求排列中不存在长度(ge 3)的下降子序列。
因为如果出现的话,那么这个下降子序列中间的元素需要先与左边比它大的元素交换再与右边比它小的元素交换,需要折返一下,显然就不合法了。(这一步说明题面里的提示不是没用的)
这又等价于可以将序列划分为(2)个上升子序列。
首先我们先不看那个字典序的性质,相信大家打一下表就能发现答案是卡特兰数。然后我们再想想它的本质是什么:
假设前(i)个位置中,最大的数是(j),那么我们会发现,(>j)的数目前是可以随便填的,然而(<j)的数只能限制从小到大按顺序填入(因为这些元素一定被归入同一个上升子序列)。
于是我们就可以设(f(i,j))表示还剩余(i)个数没填,其中后(j)个是大于当前最大值的“非限制元素”的方案数。
转移就是枚举下一个位置填一个限制元素或某一个非限制元素。
如果填限制元素,非限制元素的数量不变;否则假设填入第(k)个非限制元素,非限制元素的数量就会减少(k)个(这是因为最大值发生了变化,使得前面(k-1)个非限制元素变成了限制元素)。
边界是(f(0,0) = 1)。当然在这里(f(i,i)),就是非限制元素个数(i)等于剩余元素个数(i)的时候的转移不对,但先默认不合法的转移的方案数为(0)。
这其实就是个前缀和:
擅长打表熟悉组合数的人会很快发现,这东西就是两个组合数相减:
它的正确性容易用归纳法验证:
特别地,(f(i,0) = 1,f(i,1) = i)。这里的组合数可以保证不合法的转移是(0),也就是说我们用不严谨的递推公式总结出的组合数公式是对的。
这也解释了为什么没有限制时答案是卡特兰数:只需注意到此时的所求是(f(n,n))
我们再考虑限制,假设当前做到第(i)位,给定的排列中这一位是(a_i),后面有(suf_i)个数比它大,前面有(pre_i)个数比它小(这两个数组可以用树状数组方便地计算出来)并且现在的“非限制元素”还有(nw)个。然后类似数位DP那样逐位进行。
- 直接退出的情况:
(suf_i=0),这表明(a_i)是最大的数,这个位置没得选,并且后面没填的数只能按顺序填入,而这个排列的字典序是严格不大于给定排列的,因此就可以退出了。 - 填入的数字(p_i>a_i)的情况:
首先(nw)可以与(suf_i)取个(min),因为填完这一位后非限制元素一定不超过(suf_i)个。
然后我们相当于要求(sum_{j=0}^{nw-1} f(n-i,j)),根据前缀和它等于(f(n-i+1,nw-1)),可以(O(1))计算。 - 令(p_i=a_i),考虑填入(p_i=a_i)是否合法:
如果刚刚(suf_i)更新了(nw),说明(a_i)本身就是一个“非限制元素”,当然合法;
否则,如果(a_i)是当前未填入的元素中最小的(对应(pre_i=a_i-1)),相当于填了一个最小的“限制元素”,也是合法的;
否则,就是乱序填入“限制元素”,不合法,就可以退出了。
总复杂度(O(nlog n)),瓶颈其实在于树状数组求(pre_i)和(suf_i)的部分。
#include<bits/stdc++.h>
#define rg register
#define il inline
#define co const
template<class T>il T read(){
rg T data=0,w=1;rg char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-') w=-w;
for(;isdigit(ch);ch=getchar()) data=data*10+ch-'0';
return data*w;
}
template<class T>il T read(rg T&x) {return x=read<T>();}
typedef long long ll;
using namespace std;
co int mod=998244353;
il int add(int x,int y){
return (x+=y)>=mod?x-mod:x;
}
il int mul(int x,int y){
return (ll)x*y%mod;
}
int fpow(int x,int k){
int ans=1;
for(;k;k>>=1,x=mul(x,x))
if(k&1) ans=mul(ans,x);
return ans;
}
co int N=12e5;
int fac[N],ifac[N];
il int binom(int n,int m){
return mul(fac[n],mul(ifac[m],ifac[n-m]));
}
il int f(int q,int w){
if(w==0) return 1;
if(w==1) return q;
return add(binom(q+w-1,w),mod-binom(q+w-1,w-2));
}
int n,a[N];
int val[N],suf[N],pre[N];
void add(int p){
for(int i=p;i<=n;i+=i&-i) ++val[i];
}
int query(int p){
int ans=0;
for(int i=p;i;i-=i&-i) ans+=val[i];
return ans;
}
void inverse(){
read(n);
for(int i=1;i<=n;++i) read(a[i]),val[i]=0;
for(int i=n;i;--i){
suf[i]=n-i-query(a[i]);
add(a[i]);
pre[i]=i-1-(n-a[i]-suf[i]);
}
int nw=n,ans=0;
for(int i=1;i<=n;++i){
if(suf[i]==0) break;
bool flag=suf[i]<nw;
nw=min(nw,suf[i]);
ans=add(ans,f(n-i+1,nw-1));
if(!flag&&pre[i]!=a[i]-1) break;
}
printf("%d
",ans);
}
int main(){
freopen("inverse.in","r",stdin),freopen("inverse.out","w",stdout);
fac[0]=1;
for(int i=1;i<N;++i) fac[i]=mul(fac[i-1],i);
ifac[N-1]=fpow(fac[N-1],mod-2);
for(int i=N-2;i>=0;--i) ifac[i]=mul(ifac[i+1],i+1);
for(int t=read<int>();t--;) inverse();
return 0;
}