【Coel.解题报告】【您好,这里是CSP-J】[CSP-J 2021] 插入排序
题前闲语
没什么想说的,都放在解题思路里面了。
题目大意
题目描述
插入排序是一种非常常见且简单的排序算法。小 Z 是一名大一的新生,今天 H 老师刚刚在上课的时候讲了插入排序算法。
假设比较两个元素的时间为 \(\mathcal O(1)\),则插入排序可以以 \(\mathcal O(n^2)\) 的时间复杂度完成长度为 \(n\) 的数组的排序。不妨假设这 \(n\) 个数字分别存储在 \(a_1, a_2, \ldots, a_n\) 之中,则如下伪代码给出了插入排序算法的一种最简单的实现方式:
这下面是 C/C++ 的示范代码(注:为使格式统一,以下代码进行了格式调整):
for (int i = 1; i <= n; i++)
for (int j = i; j >= 2; j--)
if (a[j] < a[j - 1]) {
int t = a[j - 1];
a[j - 1] = a[j];
a[j] = t;
}
这下面是 Pascal 的示范代码:
for i:=1 to n do
for j:=i downto 2 do
if a[j]<a[j-1] then
begin
t:=a[i];
a[i] : = a[j];
a[j] : = t;
end;
为了帮助小 Z 更好的理解插入排序,小 Z 的老师 H 老师留下了这么一道家庭作业:
H 老师给了一个长度为 \(n\) 的数组 \(a\),数组下标从 \(1\) 开始,并且数组中的所有元素均为非负整数。小 Z 需要支持在数组 \(a\) 上的 \(Q\) 次操作,操作共两种,参数分别如下:
\(1~x~v\):这是第一种操作,会将 \(a\) 的第 \(x\) 个元素,也就是 \(a_x\) 的值,修改为 \(v\)。保证 \(1 \le x \le n\),\(1 \le v \le 10^9\)。注意这种操作会改变数组的元素,修改得到的数组会被保留,也会影响后续的操作。
\(2~x\):这是第二种操作,假设 H 老师按照上面的伪代码对 \(a\) 数组进行排序,你需要告诉 H 老师原来 \(a\) 的第 \(x\) 个元素,也就是 \(a_x\),在排序后的新数组所处的位置。保证 \(1 \le x \le n\)。注意这种操作不会改变数组的元素,排序后的数组不会被保留,也不会影响后续的操作。
H 老师不喜欢过多的修改,所以他保证类型 \(1\) 的操作次数不超过 \(5000\)。
小 Z 没有学过计算机竞赛,因此小 Z 并不会做这道题。他找到了你来帮助他解决这个问题。
输入输出格式
输入格式
第一行,包含两个正整数 \(n, Q\),表示数组长度和操作次数。
第二行,包含 \(n\) 个空格分隔的非负整数,其中第 \(i\) 个非负整数表示 \(a_i\)。
接下来 \(Q\) 行,每行 \(2 \sim 3\) 个正整数,表示一次操作,操作格式见【题目描述】。
输出格式
对于每一次类型为 \(2\) 的询问,输出一行一个正整数表示答案。
解题思路
这题是去年\(CSP-J\)的第二题,也是深进第一章习题4。
刚看到这题时我真没什么想法,不过仔细一看:
这不就是个数据结构题吗?
操作1单点修改,操作2查排名,直接上平衡树!
等等,我们还要注意到插入排序这个先决条件。
根据小学知识我们可以知道,插入排序具有稳定性,不会更改相同元素的相对位置。
而直接写平衡树是不会有稳定性的,因为权值随机;但是我们可以人工定义元素的相对位置。
怎么做呢?比如说对于这么一个序列(括号为排序前的下标):
我们可以把元素扩大\(n\)倍,在后面加上\(i-1\):
排序之后就是这样:
这与原序列进行插入排序后的位置一致,既可以保证排序后相对位置不变,也可以保证元素排序的正确性。
后面就是平衡树板子题了,代码如下:
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#define int long long//扩大后元素值可能会超过int
namespace FastIO {
inline int read() {
int x = 0, f = 1;
char ch = getchar();
while (!isdigit(ch)) {
if (ch == '-')
f = -1;
ch = getchar();
}
while (isdigit(ch)) {
x = x * 10 + ch - '0';
ch = getchar();
}
return x * f;
}
inline void write(int x) {
if (x < 0) {
x = -x;
putchar('-');
}
static int buf[35];
int top = 0;
do {
buf[top++] = x % 10;
x /= 10;
} while (x);
while (top)
putchar(buf[--top] + '0');
puts("");
}
} // namespace FastIO
using namespace std;
using namespace FastIO;
const int maxn = 1.3e4 + 10, inf = 1e9;//注意数据范围
int n, Q, root;
int a[maxn];
struct FHQ_Treap {
int cnt;
int ch[maxn][2], val[maxn], pri[maxn], size[maxn];
inline void pushup(int x) { size[x] = size[ch[x][0]] + size[ch[x][1]] + 1; }
void New_node(int& id, int v) {
size[++cnt] = 1;
val[cnt] = v;
pri[cnt] = rand();
ch[cnt][0] = ch[cnt][1] = 0;
id = cnt;
}
int merge(int x, int y) {
if (x == 0 || y == 0)
return x + y;
if (pri[x] < pri[y]) {
ch[x][1] = merge(ch[x][1], y);
pushup(x);
return x;
} else {
ch[y][0] = merge(x, ch[y][0]);
pushup(y);
return y;
}
}
void split(int id, int k, int& x, int& y) {
if (id == 0)
x = y = 0;
else {
if (val[id] <= k) {
x = id;
split(ch[id][1], k, ch[id][1], y);
pushup(x);
} else {
y = id;
split(ch[id][0], k, x, ch[id][0]);
pushup(y);
}
}
}
inline void insert(int res) {
int x, y, z;
x = y = z = 0;
split(root, res, x, y);
New_node(z, res);
root = merge(merge(x, z), y);
}
inline void erase(int res) {
int x, y, z;
x = y = z = 0;
split(root, res, x, z);
split(x, res - 1, x, y);
y = merge(ch[y][0], ch[y][1]);
root = merge(merge(x, y), z);
}
int Query_Rank(int res) {
int x, y, ans;
split(root, res - 1, x, y);
ans = size[x] + 1;
root = merge(x, y);
return ans;
}
} FHQ_Treap;
signed main() {
n = read(), Q = read();
for (int i = 1; i <= n; i++) {
a[i] = read();
FHQ_Treap.insert(a[i] * n + i - 1);
}
while (Q--) {
int op = read();
if (op == 1) {//修改=删除+插入
int x = read(), v = read();
FHQ_Treap.erase(a[x] * n + x - 1);
a[x] = v;
FHQ_Treap.insert(a[x] * n + x - 1);
} else {
int x = read();
write(FHQ_Treap.Query_Rank(a[x] * n + x - 1));
}
}
return 0;
}
上面有一个地方提到了数据范围,这里深入讲一下。
一开始给出的序列长度为 \(8000\),所以肯定要开到这么多。
与此同时, FHQ_Treap
的修改操作是先插入再删除,所以要开的数组还得再加上删除操作的数量。
题目中给出保证类型 \(1\) 的操作次数不超过 \(5000\), 因此总数组大小为 \(13000\)。
题后闲话
鬼知道为什么一道普及-的题目要用平衡树做(