C++ 知识点选讲 9——骗分试例 1(从暴力开始骗着骗着 AC 了?)
让我们从一道简单的题目入手,看看如何优化自己的暴力。
数对
题目背景
猫猫吃掉了题目背景。
题目描述
给出一个序列 \(a\),长度为 \(n\)。
小 A 对这个数列发明了一个新定义:淘气对。小 C 将来解释一下这是什么意思。
淘气对定义:
当序列中有两个位置不同数。设在前面的数为 \(a_x\),后面的数为 \(a_y\),如果 \(a_x > a_y\) 且 \(x < y\),这个数对 \((x,y)\) 就是“淘气对”。
小 B 想知道,序列 \(a\) 的下标 \(l\) 至 \(r\) 中有几对“淘气对”。当然,下标从 \(1\) 开始。
众所周知,有个叫 rand 的东西,为防止你用这个东西蒙混过关,小 B 会问你 \(q\) 次。
输入格式
第一行,两个整数 \(n\) 和 \(q\),以空格隔开。
第二行,\(n\) 个整数,第 \(i\) 个数表示 \(a_i\)。
第三至 \(2+q\) 行,每行两个整数 \(l\) 和 \(r\),以空格隔开。
输出格式
\(q\) 行,每行一个整数,表示序列 \(a\) 的 \(l \sim r\) 下标中有多少对逆序对。
输入输出样例 #1
输入 #1
4 1
3 2 3 2
1 4
输出 #1
3
说明/提示
| $n \le $ | \(q \le\) | \(l\le\) | \(r\le\) | \(a_i\le\) | 分数 | 特殊性质 |
|---|---|---|---|---|---|---|
| \(2\) | \(1\) | \(1\) | \(2\) | \(10^9\) | \(5\) | 无 |
| \(4\) | \(1\) | \(1\) | \(4\) | \(3\) | \(1\) | A |
| \(10^3\) | \(10\) | \(1\) | \(10^3\) | \(10^9\) | \(10\) | B |
| \(10^4\) | \(10^3\) | \(1\) | \(2\) | \(10^9\) | \(13\) | 无 |
| \(10^4\) | \(10^3\) | \(1\) | \(10^4\) | \(10^9\) | \(56\) | 无 |
| \(2\times10^4\) | \(10^5\) | \(1\) | \(2\times10^4\) | \(10^{14}\) | \(9\) | C |
特殊性质 A:样例。
特殊性质 B:为 \(O(qn^2)\) 预留分数。
特殊性质 C:官方题解的正常时间复杂度可通过。
本题不是捆绑测试,请放心食用。
本题的时限由评测机所定。
好吧如果你在模拟赛里碰到了这道题目,因为你不会写树状数组、线段树或归并排序,你想尽可能多地骗分。
Part 1
首先,看到特殊性质 A,发现是样例,所以输出 3。
当前得分:\(1\)
Part 2
看到第一档,发现 \(n \le 2\)。如果你没有学过数组,如果 \(r=2\) 我们可以用 if 判断大小。如果 \(r =1\) 就没有逆序对。时间复杂度 \(O(1)\)。
当前得分:\(1+5=6\)
Part 3
特殊性质 B 说,这 \(10\) 分是为了 \(O(qn^2)\) 而设,考虑写出 \(O(qn^2)\) 的代码。能不能尝试每次询问都求一遍逆序对呢?可以,先用 \(O(n^2)\) 枚举出所有下标对,再判值的大小就可以了,套上 \(q\) 次询问,就是 \(O(qn^2)\)。
当前得分:\(1+5+10=16\)
Part 4
第四档数据有一个特点——\(r \le 2\)。
从 Part 2 的分析中可知,
如果 \(r=2\) 我们可以用
if判断大小。如果 \(r =1\) 就没有逆序对。
单次询问时间复杂度 \(O(1)\),总体时间复杂度是 \(O(q)\)。
当前得分:\(1+5+10+13=29\)
Part 5
尝试优化 Part 3 中的暴力。
模拟程序运行,发现瓶颈在于,同样的数列,逆序对算了很多次!这意味着有许多重复运算,浪费时间。既然数列不会变,我们可以考虑使用离线思想进行预处理。用一个数组记录每一个下标 \(i(1 \le i \le n)\) 作为每个逆序对的第二个下标时的逆序对数量,前缀和用于查询。
很好,时间复杂度从 \(O(qn^2)\) 降到了 \(O(q+n^2)\),可以通过第五档数据。
结果你惊奇地发现,最后一档也过了!实测确实可以 AC,可能有略微卡常。洛谷评测机实力强劲!
不错,你已经从骗分到暴力、再到 AC 了。当然如果没有 AC 也有八九十分。这要看出题人的想法。
我们要学习的是,先打暴力,然后再优化暴力。甚至可以 AC。
Part 6
Part 5 的实现最多可以跑到 \(4 \times 10^8\)。如果我们要稳过的话,要学习更高级的算法。
6.1 树状数组
为了解决这个问题,我们可以用树状数组求逆序对。
for(int i=n; i>=1; i--){
ans+=Q(a[i]-1);
Add(a[i]);
}
(上核心代码用于求出一个序列的逆序对数量)
在普通逆序对的基础上,使出同样的思路——一个数组记录每一个下标 \(i(1 \le i \le n)\) 作为每个逆序对的第二个下标时的逆序对数量。用前缀和 \(O(1)\) 解决查询,时间复杂度 \(O(n \log n+q)\)。可以稳过。
6.2 莫队
本题重在不修改的查询,可以使用莫队。
Part 7
为什么可以这么做?因为 \(l\) 恒为 \(1\)。
其实本题还有一个加强版,洛谷 P5047,一般使用 二次离线莫队 等解决。
Part 8
在考场上,如果一道题做不出来就建议骗分,蚂蚁腿也是肉。最后说一遍,我们要学习的是,先打暴力,然后再优化暴力。甚至可以 AC。

浙公网安备 33010602011771号