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

题意

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

  1. 字典序比 p 大 ;
  2. 它进行冒泡排序所需要交换的次数可以取到下界,也就是令第 i 个数为 ai ,下界为 i=1n|iai|

题解

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

参考了 liuzhangfeiabc 大佬的博客

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

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

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

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

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

证明:

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

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

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

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

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

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

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

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

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

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

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

fi,j=k=0jfi1,jk

边界有

fi,0=1

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

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

fi,j=fi1,j+fi,j1

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

[100000110000122000135500149141401514284242]

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

(i+j1j)(i+j1j2)

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

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

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

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

(1)fi,j=fi,j1+fi1,j(2)=((i+j2j1)+(i+j2j))((i+j2j3)+(i+j2j2))(3)=(i+j1j)(i+j1j2)

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

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

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

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

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

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

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

否则我们需要计算的就是

j=0lim1fni,j=fni+1,lim1

也就是后面有 ni 个数需要填入,我们对于当前这一位任意选取一个 >pi 的数,剩余 0lim1 个非限制元素的情况的方案数。

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

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

复杂度是 O(nlogn)

总结

对于一类 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\n", ans); } return 0; }

__EOF__

本文作者zjp_shadow
本文链接https://www.cnblogs.com/zjp-shadow/p/9337678.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   zjp_shadow  阅读(381)  评论(5编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示