[CSP-J 2021] 插入排序

GitHub跳转原题关系图返回列表

利用插入排序的稳定性维护按 (值, 原下标) 排序的普通数组,通过二分查找回答排名。

OJ: luogu

题目 ID: P7910

难度:普及/提高-

标签:排序模拟思维cspjpython

日期: 2026-06-19 03:08

题意

给一个数组,要支持两种操作:

  1. 修改 a[x] = v
  2. 假设现在对整个数组执行题目给出的插入排序,询问“原下标为 x 的这个元素”最终会排在第几位。

注意这里问的是“元素的位置”,不是“值的位置”。

如果有多个相同的值,它们仍然要按照原下标区分。

思路

先看最直接的办法:每次查询时都把当前数组复制出来,真的执行一遍题目里的插入排序,再去找原下标 x 的元素最后在哪里。

这个版本最容易理解:

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 105;

struct Node {
    int val;
    int id;
};

int n, q;
int a[MAXN];
Node b[MAXN];

int query_rank(int x) {
    for (int i = 1; i <= n; i++) {
        b[i].val = a[i];
        b[i].id = i;
    }

    // 真的按题目的插入排序伪代码做一遍,利用“<”保证稳定性。
    for (int i = 1; i <= n; i++) {
        for (int j = i; j >= 2; j--) {
            if (b[j].val < b[j - 1].val) {
                swap(b[j], b[j - 1]);
            }
        }
    }

    for (int i = 1; i <= n; i++) {
        if (b[i].id == x) {
            return i;
        }
    }
    return -1;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> q;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }

    while (q--) {
        int op;
        cin >> op;

        if (op == 1) {
            int x, v;
            cin >> x >> v;
            a[x] = v;
        }
        else {
            int x;
            cin >> x;
            cout << query_rank(x) << '\n';
        }
    }

    return 0;
}

但这样一次查询就是 O(n2)O(n^2),显然扛不住。

关键:这个插入排序是稳定的

题目里的交换条件是:

a[j] < a[j-1]

只有严格小于才交换,所以当两个数相等时,它们不会互换位置。

这就说明:相等元素的相对顺序不会变,也就是这个排序是稳定排序。

那么最终结果就不必再从“插入排序过程”去看,而可以直接改写成:

把每个元素看成二元组 (a[i], i),然后按这个二元组升序排序。

原因很直接:

  • 值小的元素一定在前面;
  • 值相同的元素,因为稳定性,原下标小的仍然在前面。

所以查询 2 x 的答案,其实就是 (a[x], x) 在所有二元组里的排名。

方案二:维护有序数组 + 二分

维护一个普通数组 ord,其中按顺序保存当前所有二元组 (a[i], i)

text
ord[p] = 稳定排序后位于第 p 名的元素

初始化时,把所有二元组放入 ord[1..n],再用 std::sort 排序。

查询

对于操作 2 x,在 ord 中二分查找 (a[x], x)。因为原下标属于二元组的一部分,每个二元组都是唯一的,所以二分找到的位置就是答案。

一次查询的复杂度是 O(logn)O(\log n)

修改

对于操作 1 x v

  1. 二分找到旧二元组 (a[x], x)
  2. 把它后面的元素向左移动一位,删除旧二元组;
  3. 更新 a[x] = v
  4. 在剩余的 n1n-1 个二元组中二分新插入位置;
  5. 把插入位置之后的元素向右移动一位,放入 (v, x)

二分只需要 O(logn)O(\log n),但普通数组的移动需要 O(n)O(n),所以一次修改的复杂度是 O(n)O(n)。题目保证修改次数不超过 50005000,这个复杂度可以通过。

这个方案直接维护稳定排序后的完整顺序,思路和代码都比较直观。

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-12 17:56
 * update_at: 2026-07-12 17:56
 */
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 8000 + 5;

struct Node {
    int value;
    int id;
};

int n, q;
int a[MAXN];      // a[i] 表示原下标 i 的元素当前值
Node ord[MAXN];   // ord[pos] 表示稳定排序后第 pos 个元素

bool node_less(const Node &x, const Node &y) {
    if (x.value != y.value) {
        return x.value < y.value;
    }
    return x.id < y.id;
}

// 在 ord[1..len] 中找到第一个不小于 target 的位置。
int lower_bound_pos(const Node &target, int len) {
    int left = 1;
    int right = len;
    int answer = len + 1;

    while (left <= right) {
        int mid = (left + right) / 2;
        if (!node_less(ord[mid], target)) {
            answer = mid;
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    return answer;
}

// 删除旧二元组,再把新二元组插入有序数组。
void modify_value(int x, int value) {
    Node old_node;
    old_node.value = a[x];
    old_node.id = x;

    int old_pos = lower_bound_pos(old_node, n);
    for (int i = old_pos; i < n; i++) {
        ord[i] = ord[i + 1];
    }

    a[x] = value;
    Node new_node;
    new_node.value = a[x];
    new_node.id = x;

    int new_pos = lower_bound_pos(new_node, n - 1);
    for (int i = n; i > new_pos; i--) {
        ord[i] = ord[i - 1];
    }
    ord[new_pos] = new_node;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> q;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        ord[i].value = a[i];
        ord[i].id = i;
    }
    sort(ord + 1, ord + n + 1, node_less);

    while (q--) {
        int op, x;
        cin >> op >> x;

        if (op == 1) {
            int value;
            cin >> value;
            modify_value(x, value);
        } else {
            Node target;
            target.value = a[x];
            target.id = x;
            cout << lower_bound_pos(target, n) << '\n';
        }
    }

    return 0;
}

方案一:直接维护排名数组

还可以不保存完整的有序序列,只用 rank_pos[i] 表示原下标为 i 的元素当前排在第几位。

初始化时,把所有元素存入 Node 数组并按 (a[i], i) 排序。排在第 p 位的结点,其原下标为 id,于是:

text
rank_pos[id] = p

查询 2 x 时直接输出 rank_pos[x],单次查询是 O(1)O(1)

修改时,先记下 x 的旧排名。删除旧二元组后,原来排在它后面的元素都向前移动一位:

text
如果 rank_pos[i] > old_rank,就令 rank_pos[i]--

然后更新 a[x] = v,并重新比较 (a[x], x) 与其它每个 (a[i], i)

  • 如果 (a[i], i) < (a[x], x),说明元素 i 排在 x 前面,rank_pos[x] 加一;
  • 否则 x 会插在元素 i 前面,rank_pos[i] 加一。

扫描一遍以后,所有元素重新得到正确排名。一次修改是 O(n)O(n),一次查询是 O(1)O(1)

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-06-19 22:27
 * update_at: 2026-07-12 16:35
 */
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 8000 + 5;

struct Node {
    int value;
    int id;
};

int n, q;
int a[MAXN];          // a[i] 表示原下标 i 的元素当前值
int rank_pos[MAXN];   // rank_pos[i] 表示元素 i 稳定排序后的当前排名
Node nodes[MAXN];     // 初始化时用于按 (值, 原下标) 排序

bool node_less(const Node &x, const Node &y) {
    if (x.value != y.value) {
        return x.value < y.value;
    }
    return x.id < y.id;
}

// 初始化每个原下标对应的稳定排序排名。
void init_rank() {
    sort(nodes + 1, nodes + n + 1, node_less);
    for (int i = 1; i <= n; i++) {
        rank_pos[nodes[i].id] = i;
    }
}

// 把元素 x 从旧排名中删除,再按新值插入到正确排名。
void modify_value(int x, int value) {
    int old_rank = rank_pos[x];

    // 删除旧元素后,排在它后面的元素都向前移动一位。
    for (int i = 1; i <= n; i++) {
        if (i != x && rank_pos[i] > old_rank) {
            rank_pos[i]--;
        }
    }

    a[x] = value;
    rank_pos[x] = 1;

    // 按 (值, 原下标) 比较,确定新元素的排名并调整后继元素。
    for (int i = 1; i <= n; i++) {
        if (i == x) {
            continue;
        }

        if (a[i] < a[x] || (a[i] == a[x] && i < x)) {
            rank_pos[x]++;
        } else {
            rank_pos[i]++;
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> q;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        nodes[i].value = a[i];
        nodes[i].id = i;
    }
    init_rank();

    while (q--) {
        int op, x;
        cin >> op >> x;

        if (op == 1) {
            int value;
            cin >> value;
            modify_value(x, value);
        } else {
            cout << rank_pos[x] << '\n';
        }
    }

    return 0;
}

Python 知识

  • Python 元组按 (值, 原下标) 字典序比较,恰好表达稳定排序后的元素身份。
  • bisect_leftO(logn)O(\log n) 查到旧元素或查询排名。
  • insortlist.pop 的元素搬移由底层连续数组完成;题目限制修改不超过 5000 次,适合这种写法。

代码

python
import sys
from bisect import bisect_left, insort


data = iter(map(int, sys.stdin.buffer.read().split()))
n, queries = next(data), next(data)
values = [next(data) for _ in range(n)]
ordered = sorted((value, index) for index, value in enumerate(values))
answers = []

for _ in range(queries):
    operation, index = next(data), next(data) - 1
    if operation == 1:
        new_value = next(data)
        ordered.pop(bisect_left(ordered, (values[index], index)))
        values[index] = new_value
        insort(ordered, (new_value, index))
    else:
        answers.append(str(bisect_left(ordered, (values[index], index)) + 1))

print("\n".join(answers))

复杂度

设第一类修改操作共有 MM 次,题目保证 M5000M \le 5000。两个方案的空间复杂度都是 O(n)O(n)

Python 正解维护普通有序数组:初始化是 O(nlogn)O(n \log n),每次修改是 O(n)O(n),每次查询是 O(logn)O(\log n),总时间复杂度为:

O(nlogn+Mn+Qlogn) O(n \log n + Mn + Q \log n)

总结

这题真正要抓住的是“插入排序这里是稳定排序”。

一旦把这一点想清楚,问题就从“模拟插入排序过程”变成了维护二元组的有序关系。

方案二直接维护完整有序数组,用二分查找排名,最容易对应到排序结果;方案一只维护每个元素的排名,修改时线性调整,能把查询进一步降到 O(1)O(1)