@loj - 2719@「NOI2018」冒泡排序


@description@

最近,小 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\) 取模的结果。

原题传送门。

@solution@

达到下界的要求:一个数只能往前/往后移动。即不存在 \(x < y < z\) 满足 \(a_x > a_y > a_z\),等价于最长下降子序列长度 ≤ 2。

然后打表发现它是个卡特兰数,冷静分析发现这道题就是个折线路径问题,写个组合数就过了。

可以写 dp(i, j) 表示从前往后放数,前 i 个数的最大值为 j。

分析 dp 转移式的组合意义,其对应不跨越直线 y = x,从某个格点走到 (n, n) 的方案数。

这里给出一类不经由动态规划(虽然本质一样)的分析过程。

考虑构造排列的前缀最大值序列 \(m_i = \max_{j=1}^{i}\{a_j\}\)

不难发现一个序列 \(\{m_i\}\) 是“某个排列前缀最大值序列”的等价条件为(1)\(m_{i-1} \leq m_i\)(2)\(i\leq m_i\leq n\)。对应从 (1, 1) 走到 (n, n),必须经过 y = x 的右上方的方案数。即卡特兰数。

我们可以证明“前缀最大值序列”与“最长下降子序列长度 ≤ 2 的序列”一一对应:

(1)“最长下降子序列长度 ≤ 2 的序列” -> “前缀最大值序列”:显然。

(2)“前缀最大值序列” -> “最长下降子序列长度 ≤ 2 的序列”:如果 \(m_i \not= m_{i-1}\),则构造 \(a_i = m_i\);否则构造 \(a_i\) 为当前未使用的最小数。

于是我们可以转化成求前缀最大值序列。之后做类数位 dp 的过程即可。
根据上面的构造过程,注意及时排除掉 \(m_i = m_{i-1}\)\(a_i\) 不是当前未使用的最小数情况。

@accepted code@

#include <cstdio>
#include <algorithm>
using namespace std;

const int MAXN = 2*600000;
const int MOD = 998244353;

inline int add(int x, int y) {x += y; return x >= MOD ? x - MOD : x;}
inline int sub(int x, int y) {x -= y; return x < 0 ? x + MOD : x;}
inline int mul(int x, int y) {return (int)(1LL * x * y % MOD);}

int pow_mod(int b, int p) {
	int ret = 1;
	for(int i=p;i;i>>=1,b=mul(b,b))
		if( i & 1 ) ret = mul(ret, b);
	return ret;
}

int read() {
	int x = 0, ch = getchar();
	while( ch > '9' || ch < '0' ) ch = getchar();
	while( '0' <= ch && ch <= '9' ) x = 10*x + ch - '0', ch = getchar();
	return x;
}

int fct[MAXN + 5], ifct[MAXN + 5];
void init() {
	fct[0] = 1; for(int i=1;i<=MAXN;i++) fct[i] = mul(fct[i - 1], i);
	ifct[MAXN] = pow_mod(fct[MAXN], MOD - 2);
	for(int i=MAXN-1;i>=0;i--) ifct[i] = mul(ifct[i + 1], i + 1);
}
int comb(int n, int m) {
	if( n < m || m < 0 ) return 0;
	else return mul(fct[n], mul(ifct[m], ifct[n-m]));
}
int func(int x, int y) {
	if( y < 0 ) return 0;
	else return sub(comb(x + y, x), comb(x + y, x + 1));
}

int a[MAXN + 5]; bool tag[MAXN + 5];
void solve() {
	int n = read();
	for(int i=1;i<=n;i++)
		a[i] = read(), tag[i] = false;
	
	int ans = 0, mx = 0, nowmin = 1;
	for(int i=1;i<=n;i++) {
		if( a[i] < mx ) {
			ans = add(ans, func(n - i + 1, n - mx - 1));
			if( a[i] != nowmin ) break;
		}
		else mx = a[i], ans = add(ans, func(n - i + 1, n - mx - 1));
		
		tag[a[i]] = true; while( tag[nowmin] ) nowmin++;
	}
	printf("%d\n", ans);
}

int main() {
	freopen("inverse.in", "r", stdin);
	freopen("inverse.out", "w", stdout);
	
	init(); for(int T=read();T;T--) solve();
}

@details@

对于组合数 \({n\choose m}\),如果 \(n < m\)\(m < 0\) 需要特判返回 0。

posted @ 2020-06-10 10:11  Tiw_Air_OAO  阅读(243)  评论(0编辑  收藏  举报