@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。