• LOJ #2719. 「NOI2018」冒泡排序(组合数 + 树状数组)


    题意

    给你一个长为 (n) 的排列 (p) ,问你有多少个等长的排列满足

    1. 字典序比 (p) 大 ;
    2. 它进行冒泡排序所需要交换的次数可以取到下界,也就是令第 (i) 个数为 (a_i) ,下界为 (displaystyle sum_{i=1}^{n} |i - a_i|)

    题解

    一道特别好的题,理解后做完是真的舒畅~

    参考了 liuzhangfeiabc 大佬的博客

    首先我们观察一下最后的序列有什么性质:

    考试 打表 观察的:对于每个数来说,它后面所有小于它的数都是单调递增的。

    然后问了问肖大佬,肖大佬说这不就等价于

    整个序列最长下降子序列长度不超过 (3) ,或者说整个序列能划分成两个最长上升子序列。

    这看上去很有道理,但并不是那么显然?

    证明:

    考虑整个交换次数取到下限,那么对于任意一个数都需要取到下界。

    反证法:那么如果存在一个长度 (ge 3) 的最长下降子序列的话,那么这个元素首先会被右边小于它的数动一次位置,然后自己需要折返一次才能换到原位,那么就多了次数,不满足条件。

    这个性质有什么用呢?我们发现这个上升子序列与最大值是有关系的。

    也就是说我们填到第 (i) 个位置,假设当前最大值为 (j) ,我们可以随意填一个 (> j) 的数。但如果要填 (< j) 的数,需要从小到大一个个填,并且归入一个上升子序列。

    那么我们可以根据这个进行一个显然的 (dp)

    我们令大于当前最大值的数为 非限制元素 ,小于当前的数为 限制元素

    (f_{i,j}) 表示还剩余 (i) 个数没填,其中后 (j) 个是大于当前最大值的 非限制元素 的方案数。

    转移就是枚举下一个位置填一个 限制元素 或某一个 非限制元素

    如果填限制元素,非限制元素的数量不变;

    否则假设填入从小到大第 (k) 个非限制元素,非限制元素的数量就会减少 (k) 个。

    考虑逆推,那么显然有一个转移方程了:

    [f_{i,j} = sum_{k=0}^{j} f_{i-1, j - k} ]

    边界有

    [f_{i, 0} = 1 \ ]

    我们可以把这个看成一个二维矩阵。

    那么对于 ((i, j)) 这个点就是上一行前 (j) 个数的和,也就等价于

    [f_{i,j} = f_{i - 1, j} + f_{i, j - 1} ]

    这个矩阵其中一部分如下(不难发现要满足 (j le i) 才能有取值):

    [egin{bmatrix} 1 & 0 &0 & 0 & 0 & 0 \ 1 & 1 & 0 & 0 & 0 & 0 \ 1 & 2 & 2 & 0 & 0 & 0 \ 1 & 3 & 5 & 5 & 0 & 0 \ 1 & 4 & 9 & 14 & 14 & 0 \ 1 & 5 & 14 & 28 & 42 & 42 \ end{bmatrix} ]

    对角线上的数就是卡特兰数,但对于其中任意一个数可以由如下组合数导出:

    [inom {i + j - 1} {j} - inom {i + j - 1}{j - 2} ]

    它对于 ((i, j)) 这个点的实际意义为从 ((0, 0)) 一直向下和向右走,对于每一步要满足向下走的步数不少于向右走的步数,且最后走到 ((i, j)) 的方案数。

    对于这个组合数实际的组合意义,我并不知道。。。(有知道大佬快来告诉我啊)

    但我们可以证明这个组合数是正确的:

    类似与数学归纳,我们进行二维归纳

    [egin{align} f_{i, j} &= f_{i,j-1}+ f_{i - 1, j} \ &= (inom {i + j - 2}{j - 1} + inom {i + j - 2}{j}) - (inom{i + j - 2}{j - 3} + inom{i + j - 2}{j - 2}) \ & = inom {i + j - 1} {j} - inom {i + j - 1}{j - 2} end{align} ]

    然后我们继续考虑它的限制。

    对于字典序限制,我们可以这样考虑。

    枚举最终得到的序列和原序列不同的第一位(前面的都相同)然后对于这个分开计数。

    假设当前做到第 (i) 位,给定排列中的这一位为 (p_i) ,后面有 (big) 个数比他大,(small) 个数比它小。

    且当前的 非限制元素(lim) 个(也就是后面大于前面出现过的最大值的数的个数)。

    首先需要把 (lim)(big) 取个 (min) ,这个是我们当前非限制元素的下界。

    如果 (lim = 0) 那就意味着最大的数已经被我们填入,后面所有数只能从小到大填入,但这并不能满足字典序比原序列大的情况,直接退出即可。

    否则我们需要计算的就是

    [sum_{j=0}^{lim - 1} f_{n - i, j} = f_{n - i + 1, lim - 1} ]

    也就是后面有 (n - i) 个数需要填入,我们对于当前这一位任意选取一个 (> p_i) 的数,剩余 (0 sim lim - 1) 个非限制元素的情况的方案数。

    然后我们需要继续考虑能否继续向后填,也就是当前填入的数 (a_i = p_i) 是否合法

    1. 如果当前 (big) 更新了 (lim) ,那么说明 (a_i) 本身是一个非限制元素(也就是当前的最大值),合法;
    2. 否则,如果 (a_i) 是填入的最小数,那么是合法的;
    3. 其他情况显然都是不合法的。

    复杂度是 (O(n log n))

    总结

    对于一类 (dp) 我们考虑忽略它们的具体取值,只考虑他们所属的种类。

    以及一些 (dp) 可以用组合数进行表达。

    然后字典序计数考虑按位去做(似乎可以容斥?)

    代码

    #include <bits/stdc++.h>
    
    #define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
    #define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
    #define Set(a, v) memset(a, v, sizeof(a))
    #define Cpy(a, b) memcpy(a, b, sizeof(a))
    #define debug(x) cout << #x << ": " << x << endl
    #define DEBUG(...) fprintf(stderr, __VA_ARGS__)
    
    using namespace std;
    
    inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
    inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}
    
    inline int read() {
        int x = 0, fh = 1; char ch = getchar();
        for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
        for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
        return x * fh;
    }
    
    void File() {
    	freopen ("inverse.in", "r", stdin);
    	freopen ("inverse.out", "w", stdout);
    }
    
    const int N = 2e6 + 1e3, Mod = 998244353;
    int fac[N], ifac[N];
    
    int n, p[N], maxsta;
    
    int fpm(int x, int power) {
    	int res = 1;
    	for (; power; power >>= 1, x = 1ll * x * x % Mod)
    		if (power & 1) res = 1ll * res * x % Mod;
    	return res;
    }
    
    void Math_Init(int maxn) {
    	fac[0] = ifac[0] = 1;
    	For (i, 1, maxn)
    		fac[i] = 1ll * fac[i - 1] * i % Mod;
    	ifac[maxn] = fpm(fac[maxn], Mod - 2);
    	Fordown (i, maxn - 1, 1)
    		ifac[i] = 1ll * ifac[i + 1] * (i + 1) % Mod;
    }
    
    inline int C(int n, int m) {
    	if (n < 0 || m < 0 || n < m) return false;
    	return 1ll * fac[n] * ifac[m] % Mod * ifac[n - m] % Mod;
    }
    
    #define lowbit(x) (x & -x)
    namespace Fenwick_Tree {
    
    	int sumv[N];
    
    	void Init() { For (i, 1, n) sumv[i] = 0; }
    
    	void Update(int pos, int uv) {
    		for (; pos <= n; pos += lowbit(pos))
    			sumv[pos] += uv;
    	}
    
    	int Query(int pos) {
    		int res = 0;
    		for (; pos; pos -= lowbit(pos))
    			res += sumv[pos];
    		return res;
    	}
    
    }
    
    inline int f(int i, int j) {
    	if (j > i) return 0;
    	return (C(i + j - 1, j) - C(i + j - 1, j - 2) + Mod) % Mod;
    }
    
    int main () {
    
    	File();
    	int cases = read();
    
    	Math_Init(2e6);
    
    	while (cases --) {
    
    		Fenwick_Tree :: Init();
    		n = read();
    		For (i, 1, n)
    			Fenwick_Tree :: Update((p[i] = read()), 1);
    
    		int lim = n, ans = 0;
    		For (i, 1, n) {
    			Fenwick_Tree :: Update(p[i], -1);
    			int small = Fenwick_Tree :: Query(p[i]), big = (n - i) - small;
    
    			if (!big) break ;
    			bool flag = !chkmin(lim, big);
    
    
    			(ans += f(n - i + 1, lim - 1)) %= Mod;
    			if (flag && small) break;
    		}
    
    		printf ("%d
    ", ans);
    
    	}
    
    	return 0;
    }
    
  • 相关阅读:
    Struts的ONGL
    深度解析 Qt 中动态链接库
    QTcpSocket 发送数据的几种方法
    QT GUI总结
    Qt 插件学习(一)
    Qt自定义窗口部件
    Qt事件机制浅析
    VS2008 Qt Designer 中自定义信号槽
    Q窗口操作函数(窗口最大化,全屏,隐藏最大化最小化按钮)
    QT中窗口刷新事件的学习总结
  • 原文地址:https://www.cnblogs.com/zjp-shadow/p/9337678.html
Copyright © 2020-2023  润新知