[loj#2719] [NOI2018] 冒泡排序

题意简述

冒泡排序的交换次数下界为 (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;
}
原文地址:https://www.cnblogs.com/lindalee/p/13337911.html