[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;
}
posted @ 2020-07-18 22:34  秋千旁的蜂蝶~  阅读(103)  评论(0编辑  收藏  举报