黑匣子

两个堆维护已输出排名左侧与右侧元素,使每次 GET 的目标值位于右堆顶。

OJ: luogu

题目 ID: P1801

难度:普及+/提高

标签:双堆第k小heapqpython

日期: 2026-07-16 21:00

题意

元素按顺序加入;第 i 次 GET 输出当前集合第 i 小值。

思路

每次 GET 都要当前第 ii 小,直接对集合重新排序为什么不行?

集合最多 2×1052\times10^5 个数,GET 也最多 2×1052\times10^5 次,每次重排要 O(mlogm)O(m\log m),总代价 O(m2logm)O(m^2\log m) 必然超时。但题面有一个被忽略的细节:ii 每次只增加 11。相邻两次查询之间,目标只是从第 ii 小挪到第 i+1i+1 小,整个有序序列并没有大变动。

目标排名只挪一位,需要保存的信息能否缩小到"分界线"附近?

ii 小只取决于集合被切成"最小的 i1i-1 个"和"其余"两半:答案就是右半的最小值。维护这条分界线只需要两侧各一个信息——左半的最大值(用来判断新数是否属于左半)、右半的最小值(答案本身)。取最大值和最小值正是堆的职责:左半用最大堆,右半用最小堆,这就是"双堆"的来历。

新元素进来,怎么决定它属于哪一半?

把新数和左半的最大值比较:若比它还小,说明新数应该算进"已输出的前 i1i-1 小",于是把它塞进左堆,再把左堆顶(最大的那个)挤进右堆;否则它本来就属于右半,直接进右堆。这样"左堆恰好是前 i1i-1 小"的不变式始终保持。

GET 之后 ii 变成 i+1i+1,两堆怎么推进?

这次输出的右堆顶就是第 ii 小,下一次它应当成为左半的一员。所以输出后把它移入左堆,右堆顶自然成为第 i+1i+1 小。每个元素进堆、出堆常数次,总复杂度降为 O((m+n)logm)O((m+n)\log m)

lower 用负数最大堆保存已经越过的 i1i-1 个最小元素,upper 保存其余元素,小根堆顶就是下一答案。新值按分界线进入对应堆;每次输出 upper[0] 后,把它移入 lower,为下一排名作准备。

也可以换一个更直接的模型:用一个维护有序集合的平衡树(Treap),插入后直接用 kth 查询第 ii 小(见下面平衡树代码)。双堆版本的优点是只用两个堆,不需要完整有序结构。

Python 知识

  • 负数把 heapq 变成最大堆。
  • heappushpop 一次完成插入和弹出,比两个独立操作更直接。
  • values[inserted:amount] 对应两次 GET 间新增的连续输入段。

代码

双堆解法(C++)

cpp
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;

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

    int m, n;
    cin >> m >> n;
    vector<int> a(m);
    for (int i = 0; i < m; i++) {
        cin >> a[i];
    }
    vector<int> u(n);
    for (int i = 0; i < n; i++) {
        cin >> u[i];
    }

    priority_queue<int> L; // 最大堆,存储前 i-1 个最小元素
    priority_queue<int, vector<int>, greater<int>> R; // 最小堆,存储剩余元素,堆顶为第 i 小

    int inserted = 0;
    for (int q : u) {
        // 插入 a[inserted .. q-1]
        for (int i = inserted; i < q; i++) {
            int x = a[i];
            if (!L.empty() && x < L.top()) {
                L.push(x);
                R.push(L.top());
                L.pop();
            } else {
                R.push(x);
            }
        }
        inserted = q;
        // GET 操作
        cout << R.top() << "\n";
        L.push(R.top());
        R.pop();
    }

    return 0;
}

双堆解法(Python)

python
import heapq
import sys


data = iter(map(int, sys.stdin.buffer.read().split()))
value_count, query_count = next(data), next(data)
values = [next(data) for _ in range(value_count)]
queries = [next(data) for _ in range(query_count)]
lower = []
upper = []
inserted = 0
answers = []

for amount in queries:
    for value in values[inserted:amount]:
        if lower and value < -lower[0]:
            heapq.heappush(upper, -heapq.heappushpop(lower, -value))
        else:
            heapq.heappush(upper, value)
    inserted = amount
    answers.append(str(upper[0]))
    heapq.heappush(lower, -heapq.heappop(upper))

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

平衡树解法(C++)

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

struct Treap {
    struct Node {
        int child[2] = {0, 0};
        int value = 0;
        int priority = 0;
        int count = 0;
        int size = 0;
    };

    vector<Node> tree;
    int root = 0;
    mt19937 rng;

    Treap(int max_nodes = 0) : rng(712367821) {
        tree.reserve(max_nodes + 1);
        tree.push_back(Node()); // node 0 is the null sentinel.
    }

    int new_node(int value) {
        tree.push_back(Node());
        int id = (int)tree.size() - 1;
        tree[id].value = value;
        tree[id].priority = (int)rng();
        tree[id].count = 1;
        tree[id].size = 1;
        return id;
    }

    int node_size(int u) const {
        return u == 0 ? 0 : tree[u].size;
    }

    void pushup(int u) {
        tree[u].size = node_size(tree[u].child[0]) +
                       node_size(tree[u].child[1]) +
                       tree[u].count;
    }

    // direction=0: lift left child by right rotation.
    // direction=1: lift right child by left rotation.
    void rotate(int &u, int direction) {
        int v = tree[u].child[direction];
        tree[u].child[direction] = tree[v].child[direction ^ 1];
        tree[v].child[direction ^ 1] = u;
        pushup(u);
        pushup(v);
        u = v;
    }

    void insert(int &u, int value) {
        if (u == 0) {
            u = new_node(value);
            return;
        }
        if (tree[u].value == value) {
            tree[u].count++;
            pushup(u);
            return;
        }

        int direction = value > tree[u].value;
        insert(tree[u].child[direction], value);
        if (tree[tree[u].child[direction]].priority < tree[u].priority) {
            rotate(u, direction);
        }
        pushup(u);
    }

    void insert(int value) {
        insert(root, value);
    }

    // Rank is 1-based: the smallest value has rank 1.
    int rank_of(int value) const {
        int u = root;
        int rank = 1;
        while (u != 0) {
            if (value <= tree[u].value) {
                u = tree[u].child[0];
            } else {
                rank += node_size(tree[u].child[0]) + tree[u].count;
                u = tree[u].child[1];
            }
        }
        return rank;
    }

    int kth(int k) const {
        int u = root;
        while (u != 0) {
            int left_size = node_size(tree[u].child[0]);
            if (k <= left_size) {
                u = tree[u].child[0];
            } else if (k <= left_size + tree[u].count) {
                return tree[u].value;
            } else {
                k -= left_size + tree[u].count;
                u = tree[u].child[1];
            }
        }
        return -1;
    }
};

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

    int m, n;
    cin >> m >> n;
    vector<int> a(m);
    for (int i = 0; i < m; i++) {
        cin >> a[i];
    }
    vector<int> u(n);
    for (int i = 0; i < n; i++) {
        cin >> u[i];
    }

    Treap treap(m + 5);
    int inserted = 0;
    int rank = 1; // 第 i 次 GET 输出第 i 小
    for (int q : u) {
        for (int i = inserted; i < q; i++) {
            treap.insert(a[i]);
        }
        inserted = q;
        cout << treap.kth(rank) << '\n';
        rank++;
    }

    return 0;
}

复杂度

每个元素移动常数次,总时间 O((m+n)logm)O((m+n)\log m),空间 O(m)O(m)

平衡树解法插入和查询各 O(logm)O(\log m),总时间同为 O((m+n)logm)O((m+n)\log m),空间 O(m)O(m)

总结

目标排名每次只增加 1,双堆分界也只需向右移动一个元素。