题意简述
冒泡排序的交换次数下界为 (frac{1}{2}sum |i-p_i|)
对于长度为 (n) 的排列,定义交换次数为 (frac{1}{2}sum |i-p_i|) 的排列为 “好”排列
给定长度为 (n) 的排列 (q),求字典序严格大于 (q) 的“好”排列个数
(n leq 6 imes 10^5)
想法
Step1 找性质
首先探究“好”排列的性质。
根据题目中的“提示”:
排序本质上就是数字的移动,因此排序的交换次数应当可以用数字移动的总距离来描述。对于第 (i) 个位置,假设在初始排列中,这个位置上的数字是 (p_i),那么我们需要将这个数字移动到第 (p_i) 个位置上,移动的距离是 (|i-p_i|) 。从而移动的总距离就是 (sum |i-p_i|) ,而冒泡排序每次会交换两个相邻的数字,每次交换可以使移动的总距离至多减少 2 。因此 (frac{1}{2}sum |i-p_i|) 是冒泡排序的交换次数的下界。
可知“好”排列中一个数要么一直往左边挪,要么一直往右边挪
具体地说,设排列 (p) 中数 (i) 的位置为 (b_i)
- 若 (b_i<i) :(i) 要一直往右挪,也就是说排在 (i) 左边的数都小于 (i) ,所有包含 (i) 的逆序对都是 (i) 与右侧的数
- 若 (b_i>i) : (i) 要一直往左挪,也就是说排在 (i) 右边的数都大于 (i) ,所有包含 (i) 的逆序对都是 (i) 与左侧的数
- 若 (b_i=i) :(i) 不能挪动,排在 (i) 左侧的数都小于 (i) ,排在 (i) 右侧的数都大于 (i) ,不存在包含 (i) 的逆序对
也就是说对每个数 (i) ,最多只能与一侧的数构成逆序对。
思考下发现,这个条件也是充分的。只要满足“最多只能与一侧的数构成逆序对”,就一定满足上面三个条件之一,可推出是“好”排列。
性质挖掘完毕。
Step2 特殊情况下的 dp 方程
先忽略“字典序”的要求,考虑如何计数长度为 (n) 的“好”排列个数
考虑一位位填数,假设当前填了 (k) 位,其中的最大数为 (mx),那么对于当前未填的 (<mx) 的数来说,左边已构成逆序对了,右侧不能再有了,所以这些数填的顺序是递增的;而对于未填的 (>mx) 的数,暂时可以随便填
于是设状态 (f[i][j]) 表示还有 (i) 位没有填,有 (j) 个数不被限制,显然它们是剩余数中最大的 (j) 个数
那么这位可填被限制的数中最小的那个,或任一个不受限制的
(f[i][j]=f[i-1][j]+sumlimits_{k=0}^{j-1} f[i-1][k]=sumlimits_{k=0}^j f[i-1][k]=f[i][j-1]+f[i-1][j])
边界为 (f[0][0]=1)
这个 (dp) 让我们想到了走格子,(f[m][n]) 表示从 ((0,0)) 走到 ((m,n)) ,每步可以向右或向上走,任何时候向上步数不能超过向右步数的总方案数。
所有不合法情况都可与从 ((-1,1)) 走到 ((m,n)) 的路径一一对应(不合法 (Leftrightarrow) 与直线 (y=x+1) 有交点,把 ((0,0)) 到第一次交点的路径关于 (y=x+1) 对称,形成的即是 ((-1,1)) 到 ((m,n)) 的路径)
所以,(f[m][n]=inom{m+n}{m}-inom{m+n}{m+1})
Step3 拓展到一般情况
考虑“字典序”条件,从高位到低位枚举首个大于的位置 (k),这个位置要填的数 (>q_k)
设 (q_1,q_2,dots q_k) 中的最大数为 (mx)
若 (q_kgeq mx) ,则此时要填的数位已填部分的最大数,则剩下的数(包括第 (k) 位的数)填法为 (sumlimits_{i=0}^{n-mx-1} f[n-k][i]=f[n-k+1][n-mx+1])
若 (q_k<mx) ,如果第 (k) 位填的数 (t) 比 (mx) 小,而 (t>q_k) ,(q_k) 在 (t) 后面会形成逆序对,不符合条件。所以第 (k) 位填的数 (geq mx),填法同样为 (f[n-k+1][n-mx+1])
最后注意判断当前前缀是否满足条件(可用树状数组),若不满足直接跳出。
总结
技巧
走格子那个挺套路,以及 (m=n) 时是卡特兰数的公式
感想
挖掘性质最费劲……有时候分类讨论后情况比较复杂,可以考虑有没有更简要的条件可以概括上面的多种情况~
代码
#include<cstdio>
#include<iostream>
#include<algorithm>
#define P 998244353
using namespace std;
int read(){
int x=0;
char ch=getchar();
while(!isdigit(ch)) ch=getchar();
while(isdigit(ch)) x=x*10+ch-'0',ch=getchar();
return x;
}
const int N = 600005;
int n,q[N];
int mul[N*2],inv[N*2];
int Plus(int x,int y) { return x+y<P?x+y:x+y-P; }
int Minus(int x,int y) { return x>=y?x-y:x-y+P; }
int Pow_mod(int x,int y){
int ret=1;
while(y){
if(y&1) ret=1ll*ret*x%P;
x=1ll*x*x%P;
y>>=1;
}
return ret;
}
int f(int x,int y) { //x>=y
if(x<y) return 0;
if(y<0 || x<0) return 0;
if(y==0) return 1;
return Minus(1ll*mul[x+y]*inv[x]%P*inv[y]%P,1ll*mul[x+y]*inv[x+1]%P*inv[y-1]%P);
}
int c[N];
void add(int x,int y) { while(x<=n) c[x]+=y,x+=x&(-x); }
int sum(int x){
int ret=0;
while(x) ret+=c[x],x-=x&(-x);
return ret;
}
int main()
{
freopen("inverse.in","r",stdin);
freopen("inverse.out","w",stdout);
int T=read();
mul[0]=1;
for(int i=1;i<N*2;i++) mul[i]=1ll*mul[i-1]*i%P;
inv[N*2-1]=Pow_mod(mul[N*2-1],P-2);
for(int i=N*2-2;i>=0;i--) inv[i]=1ll*inv[i+1]*(i+1)%P;
while(T--){
n=read();
for(int i=1;i<=n;i++) q[i]=read();
int mx=1,ans=f(n,n-q[1]-1);
add(q[1],1);
for(int i=2;i<=n;i++){
if(q[i]<q[mx]){
ans=Plus(ans,f(n-i+1,n-q[mx]-1));
if(sum(q[i]-1)<q[i]-1) break;
}
else{
mx=i;
ans=Plus(ans,f(n-i+1,n-q[i]-1));
}
add(q[i],1);
}
printf("%d
",ans);
//clear
for(int i=0;i<=n;i++) c[i]=0;
}
return 0;
}