[loj#2719] [NOI2018] 冒泡排序
题意简述
冒泡排序的交换次数下界为 \(\frac{1}{2}\sum |i-p_i|\)
对于长度为 \(n\) 的排列,定义交换次数为 \(\frac{1}{2}\sum |i-p_i|\) 的排列为 “好”排列
给定长度为 \(n\) 的排列 \(q\),求字典序严格大于 \(q\) 的“好”排列个数
\(n \leq 6\times 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]+\sum\limits_{k=0}^{j-1} f[i-1][k]=\sum\limits_{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]=\binom{m+n}{m}-\binom{m+n}{m+1}\)
Step3 拓展到一般情况
考虑“字典序”条件,从高位到低位枚举首个大于的位置 \(k\),这个位置要填的数 \(>q_k\)
设 \(q_1,q_2,\dots q_k\) 中的最大数为 \(mx\)
若 \(q_k\geq mx\) ,则此时要填的数位已填部分的最大数,则剩下的数(包括第 \(k\) 位的数)填法为 \(\sum\limits_{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\n",ans);
//clear
for(int i=0;i<=n;i++) c[i]=0;
}
return 0;
}