算法学习笔记(22): 逆序对与原序列

算法,学习,笔记,逆序,序列 · 浏览次数 : 163

小编点评

**逆序对与原序列** 逆序对是原序列中元素的排列,逆序对的个数与原序列的排列相同,但逆序对的顺序却相反。 **逆序对构建方法** **1. 相对插入法** * 将初始序列为空。 * 从前向后考虑下表,将每个元素放到最适合其位置的元素之前。 * 每次从后向前考虑,直到找到第一个能放进该位置的元素。 * 将该元素放在该位置。 **2. 绝对插入法** * 先构造 n 个空元素。 * 考虑元素是否在原序列中比该元素小的元素。 * 如果在,则将该元素放到原序列的最后位置。 * 否则,从后向前考虑,找到第一个能放进该位置的元素。 * 将该元素放在该位置。 **带修改问题的复杂度** * 修改:O(B log^2 n) * 单点查询:O(log n)

正文

逆序对与原序列

在《组合数学》中有这么一个从逆序列构建一个排列的过程……而刚好有一场考试有考了类似的问题,于是在此总结一下。


逆序列

假定我们有序列 \(P\)\(\{1, 2, \cdots, n\}\) 的一个排列。如果 \(i < j\) 并且 \(p_i > p_j\) 则称数对 \((p_i, p_j)\) 为一个逆序对。

在逆序列中,我们令 \(r_k\) 表示 \(p_j = k\) 的逆序对数量。

注意是数值,而非下标!!!


例子:排列 \(31524\) 的逆序列为 \(1, 2, 0, 1, 0\)

解释:数字 \(1\) 前有一个数比他大,\(2\) 前有两个……所以为 \(1, 2, 0, 1, 0\)


我们不难得知:\(0 \le r_i \le n - i\)。依据乘法原理,我们可知 \(r_i\) 一共有 \(n \times (n-1) \times (n-2) \times \cdots \times 2 \times 1 = n!\) 种。

这提示我们,逆序列与排列一一对应,也就是说,唯一的逆序列可以产生唯一的排列。

那么下面描述两种方法通过逆序列构建一个排列。


算法一:相对插入法

我们令初始序列 \(P\) 为空,逆序考虑 \(k\)\(k\ from\ n \ downto \ 1\)),重复以下步骤:

  • 考虑 \(r_k\),由于 \((k, n]\) 都已经放好,那么可知剩下的数其实对其没有影响。于是我们只需要将 \(k\) 放在第 \(r_k\) 个数后面即可。这样一定可以保证 数 \(k\) 前有 \(r_k\) 个比他大的数。

在这个算法中,我们只做到了这些整数的相对位置不变,但是在排列中的位置是不知道的。

于是我们可以考虑下一个算法。


算法二:绝对插入法

我们先构造 \(n\) 个空位。从 \(1 \sim n\) 考虑 \(r_k\)

  • 因为 \(k\) 前面应该还有 \(r_k\) 个大于 \(k\) 的整数,而且这些数都还没有插进来,因此,必须给这些数留出 \(r_k\) 个空位。于是我们在第 \(r_k + 1\) 的空位上放上数 \(k\)

举个例子:求逆序为 \(1, 2, 0, 1, 0\) 的排列。

算法过程也就如此。


说了两种方法,到底哪一种可行性更高?

第一种方法利用 vector 可以很快的写出来,但是复杂度还是近似于 \(O(n^2)\)。利用平衡树可以把他优化到 \(O(n \log n)\) 的复杂度。

而第二种方法,不太好写,朴素还是 \(O(n^2)\) 的复杂度。可以利用线段树和二分做到 \(O(n \log n)\) 的复杂度,只是不太直观。(用树状数组也行,\(O(n \log^2 n)\) 但常数极小。也非常优秀)。


不过如果我们只需要求其中值的位置呢?

我们还是考虑相对插入算法。首先令 \(b = r_k\),表示 \(k\) 在当前时候前面有几个数。

那么能够做出贡献的只有 \(r_i, i \in [1, k)\)。我们模仿插入的过程逆序考虑:

  • 如果 \(r_i \le b\),意味着 \(i\) 会插入到 \(k\) 前面,故令 \(b = b + 1\)

  • 否则不改变 \(b\)

最终 \(b\) 的大小也就是 \(k\) 所在的位置。

这个算法是 \(O(n)\) 的,也启发我们有另外一种 \(\Theta(n^2)\) 的写法。


再来一个问题:求某一个位置的值

这一问题作为练习,留给读者思考(还是考虑 \(O(n)\) 做?)


逆序个数

很多时候,出题人并不会基于这种描述方法,而是采用下标来表示。

逆序个数序列 \(b_i\) 表示 \(\sum_{j < i}[p_j > p_i]\),也就是第 \(i\) 个数前有几个数比它大。

我们可以稍加改变前文中的两个算法,从而得到求逆序的一种方法。

这里讲一下基于相对插入法的做法吧。


相对插入法

还是类似的,令初始序列为空,从前向后考虑下表 \(i\)

  • 我们将 \(i\) 放在从后向前 \(b_i\) 个数前(若 \(b_i = 0\),则直接放在末端)

    注意,是直接放下标

于是最终序列 \(a_i = rank(i)\),也就是 \(i\) 从前向后数的位置。


那么这里考虑某一个位置的值

我们从前向后考虑,令 \(r\) 表示在序列中 \(i\) 后面的数的个数,考虑什么时候 \(b_j\) 会对 \(r\) 做出贡献?若 \(b_j \le r\),那么意味着 \(j\) 会放在 \(i\) 的右侧,那么此时令 \(r = r + 1\)

于是最终 \(a_i = n - r\)

于是我们有了 \(\Theta(n^2)\) 的做法了……


带修改问题

考虑这样一个问题:对于逆序对序列 \(b_i\),要求支持单点修改 \(b_i\),以及单点查询 \(a_i\)

原题为 CF1540D

这道题需要用到分块。

考虑以下事实:

  • 对于一定的序列 \(b_i\),如果初始值为 \(r\),则经过操作后 \(r \to r'\) 的结果是一定的。这启发我们可以分块。

  • 而考虑对于每一个 \(r \in [1, n]\),对应的结果 \(r'\) 一定是不下降的。也就是说 \(r\) 越大,\(r'\) 越大。也就是说我们令 \(f(r)\) 表示 \(r\) 经过了序列之后变成的值,\(f\) 是一个单调不下降序列。

于是对于每一块,考虑用线段树(或者树状数组等神秘数据结构)维护(需要支持区间加和二分求某个值的位置),初始 \(T_i= i\)

按顺序加入 \(b_j\),考虑先找到所有 \(\ge b_j\)\(T_i\) ,并整体加 \(1\)。不难发现,其实每一次只是后缀 \(+1\),又由于原序列的单调的,所以新的序列是单调,这为二分查找提供了保障。

补充一下关于使用树状数组这件事。

复杂度为 \(O(B \log^2 n)\) 是单纯的二分 + 树状数组。然而如果使用树状数组上二分的操作可以避免一个 \(\log\),也就是复杂度成为 \(O(B \log n)\)。这样可以过。

提交:https://codeforces.com/contest/1540/submission/211432601

于是我们可以在 \(O(B \log n)\) 的复杂度中处理出 \(T_i\),并可以在 \(O(\log n)\) 内找到变换对应的值。于是查询的复杂度为 \(O(\frac nB \log n)\)

考虑修改?暴力重构每一块就是了。修改为 \(O(B \log n)\)

块长设为 \(\sqrt n\) 即可,总复杂度为 \(O(n \sqrt n \log n)\)。毕竟是 5s 时限,还是可以过的。

然而会有更优秀的做法,参考:CF1540D. Inverse Inversions - jz_597 - 博客园

虽然我看不懂它……

与算法学习笔记(22): 逆序对与原序列相似的内容:

算法学习笔记(22): 逆序对与原序列

# 逆序对与原序列 > 在《组合数学》中有这么一个从逆序列构建一个排列的过程……而刚好有一场考试有考了类似的问题,于是在此总结一下。 [TOC] ## 逆序列 假定我们有序列 $P$ 是 $\{1, 2, \cdots, n\}$ 的一个排列。如果 $i p_j$ 则称数对 $(p_i, p_j)$

算法学习笔记(12): 线性基

# 线性基 > 熟练掌握异或运算是食用本文的大前提,请读者留意 [TOC] ## 是什么? 是一种利用线性代数的知识,用于解决异或问题的一种手段(不能算作数据结构吧这) > 本文并不会涉及到线性代数。而是从OI基础算法思想的角度阐释线性基。尽管这可能违背了设计该方法的初衷。 一般来说,预处理的时间复

OI-Wiki 学习笔记

算法基础 \(\text{Update: 2024 - 07 - 22}\) 复杂度 定义 衡量一个算法的快慢,一定要考虑数据规模的大小。 一般来说,数据规模越大,算法的用时就越长。 而在算法竞赛中,我们衡量一个算法的效率时,最重要的不是看它在某个数据规模下的用时,而是看它的用时随数据规模而增长的趋

算法学习笔记(6): 树链剖分

树链剖分 树链剖分是一个很神奇,但是在树上可以完成一些区间操作问题 简单来说,就是把一棵树分成一条条的链,通过维护链上的信息来维护整棵树的信息 基础知识可以参考我的另外一篇博客:算法学习笔记(5): 最近公共祖先(LCA) 这里假设你已经掌握了上述博客中的所有相关知识,并清晰了其背后的原理 性质?发

算法学习笔记(11): 原根

原根 此文相对困难,请读者酌情食用 在定义原根之前,我们先定义其他的一点东西 阶 通俗一点来说,对于 $a$ 在模 $p$ 意义下的阶就是 $a^x \equiv 1 \pmod p$ 的最小正整数解 $x$ 或者说,$a$ 在模 $p$ 意义下生成子群的阶(群的大小) 再或者说,是 $a$ 在模

算法学习笔记(30):Kruskal 重构树

Kruskal 重构树 这是一种用于处理与最大/最小边权相关的一个数据结构。 其与 kruskal 做最小生成树的过程是类似的,我们考虑其过程: 按边权排序,利用并查集维护连通性,进行合并。 如果我们在合并时,新建一个节点,其权值为当前处理的边的权值,并将合并的两个节点都连向新建的节点,那么就可以得

算法学习笔记(3.1): ST算法

ST表 在RMQ(区间最值)问题中,著名的ST算法就是倍增的产物。ST算法可以在 \(O(n \log n)\) 的时间复杂度能预处理后,以 \(O(1)\) 的复杂度在线回答区间 [l, r] 内的最值。 当然,ST表不支持动态修改,如果需要动态修改,线段树是一种良好的解决方案,是 \(O(n)\

C++算法之旅、09 力扣篇 | 常见面试笔试题(上)算法小白专用

算法学习笔记,记录容易忘记的知识点和难题。详解时空复杂度、50道常见面试笔试题,包括数组、单链表、栈、队列、字符串、哈希表、二叉树、递归、迭代、分治类型题目,均带思路与C++题解

C++算法之旅、08 基础篇 | 质数、约数

算法学习笔记,记录容易忘记的知识点和难题。试除法、分解质因数、筛质数、约数个数、约数之和、最大公约数

算法学习笔记(1): 欧几里得算法及其扩展

扩展欧几里得算法详解 在了解扩欧之前我们应该先了解欧几里得算法 欧几里得算法 这是一个递归求最大公约数(greatest common divisor)的方法 $$ gcd(a, b) = gcd(b, a % b) $$ 可以通过一个类似的简单公式推导而来 好像叫做辗转相减法来着? $$ gcd(