Points

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

外层线段树按 x 组存区间最大 y,靠「整段最大值不够就剪掉」单路下降定位最左可行 x;组内可用 std::set 或离线压缩后的 Fenwick 维护。

OJ: codeforces

题目 ID: 19D

难度:提高+/省选-

标签:线段树二分树状数组坐标压缩二维查询集合pythoncpp

创建: 2026-07-16 23:59

更新: 2026-10-28 17:33

形式化题目

平面上动态维护一个点集 SS,初始为空,支持三类操作(n⩽2×105n\leqslant 2\times 10^5):

  • add x y:插入点 (x,y)(x,y),保证该点当前不在 SS 中;
  • remove x y:删除点 (x,y)(x,y),保证该点当前在 SS 中;
  • find x y:在满足 x′>xx'>x 且 y′>yy'>y 的点中,先取 x′x' 最小者,若仍不唯一再取 y′y' 最小者,输出该点;无解输出 −1-1。

也就是每次询问一个「严格右上」的半平面里字典序最小的点。所有坐标不超过 10910^9。

结构上的关键是:xx 与 yy 都要求严格大于,且优先级是「先最小化 xx,再最小化 yy」。于是这是一次二维查询,但两个维度不是对称的——必须先定 xx,再定 yy。

解法总览

两个解法的外层完全一样,差别只在「一个 xx 组内部用什么容器维护它的 yy 集合」:

  • 解法一(main.cpp,正式主解):外层线段树按 xx 分组建,结点存这段 xx 里所有点的最大 yy;组内用 std::set<int> 存活跃 yy。查询时先靠线段树单路下降找到最左可行组,再在该组的 set 上 upper_bound。全程在线,不需要先读完输入。
  • 解法二(main2.cpp / main2.py):外层同一棵线段树;组内换成「离线坐标压缩 + Fenwick 计数树」,用「前缀计数 + kth 二进制提升」定位第一个大于 query_y 的活跃 yy。结构全是数组,常数更小,也是 Python 侧唯一现实可行的写法。

两个解法的复杂度同为 O(nlog⁡n)O(n\log n),是并列的替代关系,不是「先慢后快」的递进:解法一省掉离线预处理、支持边读边做;解法二省掉平衡树的结点开销、容易搬进 Python。

解法 核心观察 复杂度 适用场景 对应代码
解法一:组内 std::set 结点存组区间最大 yy,mx⩽ymx\leqslant y 整段剪掉,单路下降定位最左可行组;组内 set 天然有序 每次 O(log⁡n)O(\log n),空间 O(n)O(n) 强制在线、或不想写离线压缩 main.cpp(C++)
解法二:组内 Fenwick 同一棵树,组内改为「活跃 yy 的计数树」,用秩 + kth 定位 每次 O(log⁡n)O(\log n),空间 O(n)O(n) 允许离线、追求常数;Python 侧唯一可行 main2.cpp(C++)、main2.py(Python)

同目录的 brute.cpp 是「每次 find 扫一遍所有点」的暴力,只作为对拍基线,不单独成章——它每次询问 O(n)O(n),n=2×105n=2\times 10^5 时必然超时,教学价值仅在于确认题意。

解法一:组内 std::set + 外层线段树带剪枝下降

思路

先想法把二维压成一维。 所有 add 出现过的 xx 一共不超过 nn 种,把它们升序去重排成 x1<x2<⋯<xgx_1<x_2<\dots<x_g,gg 就是「组数」。每个 xjx_j 对应一个组,组里装当前挂在这个 xx 上的所有 yy。这样「x′>xx'>x 且 y′>yy'>y,x′x' 最小」就变成:

  1. 在组序列里,从「第一个 xj>xx_j>x 的组」开始往右,找第一个组内存在某个 y>y0y>y_0 的组;
  2. 在那个组里找最小的 y>y0y>y_0。

第 2 步就是有序集合的 upper_bound,std::set<int> 直接给。真正要想清楚的是第 1 步。

下面三张图用一份专门构造的教学数据(不是题目样例)走完整条流程:7 个组、组 2 和组 5 已被删空、查询 find 3 3,最终答案是 (6,5)(6,5)。第一张图看「分组」这件事本身:

按 x 竖直分组,每组柱顶就是线段树叶子值

竖着看每一列就是一个组,组内就是 std::set 里的元素;柱顶那根绿线是该组的最大 yy,也正是线段树叶子存的那个数。于是第一维 xx 变成了线段树的下标,第二维 yy 缩成每个下标上的一个数,二维查询就落到了一根一维轴上。图上还有两个后面会反复用到的细节:组 1 和组 3 的柱顶(55、99)都大于 y0=3y_0=3,但它们的 xx 不大于 33,整列被灰虚线罩掉——这就是为什么下降时必须带「越界」剪枝;组 2 和组 5 是空组,叶子存 −1-1,因为坐标非负,−1-1 一定不满足 y>y0y>y_0,等于「该组恒无答案」。

第 1 步不能对着「组内最大 yy」直接二分。 这是本题最容易踩的坑,它和 luogu-U502676 线段树上二分模版 里「不能对 ata_t 二分」是同一个陷阱。设每组当前最大 yy 记作 mjm_j,则 m1=5,m2=1,m3=1,m4=6m_1=5,m_2=1,m_3=1,m_4=6,查询 y0=4y_0=4:

text
组下标 j        :   1     2     3     4
组内最大 y  m_j :   5     1     1     6
m_j > 4        :   真    假    假    真    <- 不是一段假接一段真
max(m_1..m_j)>4 :   真    真    真    真    <- 换成前缀最大值才单调

若按「第一个 mj>4m_j>4 为真」去二分,中点先探到 j=2j=2 得「假」,于是把左边全丢掉,正确答案 j=1j=1 再也回不来。能让二分合法的判据是「这一段的最大值是否 >y0>y_0」——多放一组进来,最大值只会持平或变大,单调。

再往前一步:判据本来就存在结点上,不必真做二分。 线段树每个结点维护它覆盖的那段组上的最大 yy(即「这段里所有点的最大 yy」)。于是从根往下走时,结点自带「这段里还有没有答案」这个信息:

  • 该结点的区间整段落在起点左边(r<startr<\text{start}):与答案无关,剪掉;
  • 该结点最大值 mx⩽y0mx\leqslant y_0:这段里每个组都没有 y>y0y>y_0 的点,整段剪掉;
  • 否则答案可能在段内:先走左儿子,左儿子没有再走右儿子;
  • 走到叶子(l=rl=r)时返回该组号——它必然满足组内最大 y>y0y>y_0。

先左后右保证拿到的是最小的组号,也就是最小的 xx。这就是 luogu-U502676 线段树上二分模版 与 atcoder-aalc001_c Binary Search on a Segment Tree 里的「带可行性剪枝的单路下降」,只是把「ai>ka_i>k」换成「组内最大 y>y0y>y_0」。

第二张图把同一次 find 3 3 的下降过程画在树上,绿框右上角的编号就是下降顺序:

外层线段树上的单路下降:绿框是走过的路,虚线是被整段剪掉的结点

从根 [1,7][1,7] 出发,根的最大值 9>39>3 说明答案确实在里面,于是先看左儿子;[1,4][1,4] 的左儿子 [1,2][1,2] 整段组号都在起点左边(x⩽3x\leqslant 3),被灰色虚线剪掉,转而进右儿子 [3,4][3,4];[3,3][3,3] 是组 3,虽然柱顶 9>39>3,但 x=3x=3 不够大,还是越界剪枝;[4,4][4,4] 的组 4 柱顶只有 33,不大于 y0y_0,红色虚线无解剪枝。左半边彻底没答案,才轮到右半边 [5,7][5,7],同样剪掉 [5,5][5,5](空组,叶子 −1-1)之后命中 [6,6][6,6]。注意整棵树只有一条绿线深浅相连——被剪掉的结点各自只花一次比较就整段放弃,真正向下走的只有通往答案的那一条路径,这就是单次查询 O(log⁡n)O(\log n) 的来源。图里枝干不是完全对称的(左子树一直切到单点,右子树在 [5,7][5,7] 之后才分叉),这是闭区间 mid=(l+r)>>1 切分的自然结果,不影响正确性。

两个剪枝条件都要有。只判 mx⩽y0mx\leqslant y_0 而不判「整段在起点左边」的话,左半边虽然一个答案都不该取,却因为最大值够大而仍然递归下去,单次查询就退化回 O(log⁡2n)O(\log^2 n) 甚至更差——这正是 U502676 第三个 hack 点专门卡的写法。

组的最大 yy 怎么维护。 add/remove 只改一个组:在组内 set 上插入或删除一个 yy,然后把 set 的最大元素(rbegin())写到线段树对应的叶子上,向上更新祖先。空组写 −1-1(坐标非负,−1-1 一定不满足 y>y0y>y_0,等于「该组恒无答案」)。

第三张图把「落地到组内」和「改回去」这两件事并排放,左栏是 find 的最后一步,右栏是 remove 的维护:

组内 upper_bound 取最小可行 y;删除后沿路径重算最大值

左栏:组 6 的 set 里是 {2,5,7}\{2,5,7\},门槛 y0=3y_0=3 把 22 挡在下面,剩下 5,75,7 都可行,但 upper_bound 直接返回第一个跳出来的 55,后面的 77 根本不用看——这就是「先定 xx 再定 yy」里的第 2 步。右栏:每组的最大值是算出来的(组内 set 的 rbegin()),不是单独存一份,所以删掉组 6 的 77 之后,叶子从 77 变成 55,结点 [5,6][5,6]、[5,7][5,7] 也要跟着重算,而根 [1,7][1,7] 的 99 来自组 3,没有变。需要重算的只有叶子到根这一条路径,长度 O(log⁡n)O(\log n)——这就是 add/remove 的代价。

这份代码全程用闭区间 [l,r][l,r]:切分是 [l,mid] 和 [mid+1,r],叶子是 l==r,越界剪枝写 r < start。组号从 11 开始,线段树区间就是 [1,g组数][1,g_{\text{组数}}],所以不需要把叶子数补齐到 22 的幂——数组直接开 4g组数4g_{\text{组数}} 大小。find_first 找不到时返回 00 作为哨兵(00 不是合法组号),调用处判 g=0g=0 就能输出 −1-1;同理 find 的 xx 可能根本没在 add 里出现过,用 upper_bound 求起点时它会落到最后一组之后,此时 start > g_{\text{组数}},第一个剪枝条件立刻把所有区间剪掉,同样返回 00。

代码

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-08-02
 * update_at: 2026-10-28 15:10
 */

/* CF19D Points —— 解法一:每组一个 std::set + 外层线段树带剪枝单路下降 */
/* 外层按 x 分组建线段树,结点存该区间内所有点的最大 y;
 * 剪枝条件 seg[p] <= query_y 表示整段一个答案都没有,直接砍掉,
 * 于是「第一个 x 更大且组内存在 y 更大」的组可以在 O(log n) 内单路下降定位;
 * 组内 y 交给 std::set 的顺序性,upper_bound 一次就拿到最小可行 y。 */

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

const int MAXN = 200005;

int n;                  // 操作数
char op[MAXN];          // 'a' add / 'r' remove / 'f' find
int ox[MAXN], oy[MAXN]; // 操作参数;坐标不超过 1e9,用 int 省内存
vector<int> xs;         // 所有 add 出现过的 x(升序去重),组号 = 下标 + 1
set<int> group_y[MAXN]; // group_y[g]:第 g 组当前挂着的所有 y
int seg[MAXN * 4];      // seg[p]:第 p 个结点覆盖的组区间内最大 y,空组为 -1
int groups;             // 组数,线段树下标范围是 [1, groups]

// 点更新:把第 pos 组的叶子值改成 value,并向上合并区间最大值。
void seg_update(int p, int l, int r, int pos, int value) {
    if (l == r) {
        seg[p] = value;
        return;
    }
    int mid = (l + r) >> 1;
    if (pos <= mid)
        seg_update(p << 1, l, mid, pos, value);
    else
        seg_update(p << 1 | 1, mid + 1, r, pos, value);
    seg[p] = max(seg[p << 1], seg[p << 1 | 1]);
}

// 在线段树上找最小的组号 g 使 start <= g 且第 g 组的最大 y > y(线段树二分),
// 不存在返回 0。start 之前(含)的区间整体没有意义,用 r < start 一并剪掉。
int find_first(int p, int l, int r, int start, int y) {
    if (r < start || seg[p] <= y) return 0; // 整段在起点左边,或整段最大值都够不到 y
    if (l == r) return l;                   // 走到叶子,它一定满足最大 y > y
    int mid = (l + r) >> 1;
    int res = find_first(p << 1, l, mid, start, y); // 先左后右,返回的组号自然最小
    if (res) return res;
    return find_first(p << 1 | 1, mid + 1, r, start, y);
}

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

    cin >> n;
    for (int i = 0; i < n; i++) {
        string s;
        cin >> s >> ox[i] >> oy[i];
        op[i] = s[0];
        if (op[i] == 'a') xs.push_back(ox[i]);
    }

    sort(xs.begin(), xs.end());
    xs.erase(unique(xs.begin(), xs.end()), xs.end());
    groups = xs.size();

    fill(seg, seg + 4 * groups + 4, -1); // 初始一个点都没有,所有组都按空组算

    for (int i = 0; i < n; i++) {
        if (op[i] != 'f') { // add / remove:只改动一个组的点集
            int g = lower_bound(xs.begin(), xs.end(), ox[i]) - xs.begin() + 1; // 组号从 1 开始
            if (op[i] == 'a')
                group_y[g].insert(oy[i]);
            else
                group_y[g].erase(oy[i]);
            seg_update(1, 1, groups, g, group_y[g].empty() ? -1 : *group_y[g].rbegin());
            continue;
        }

        // find:先定位最左的可行组,再在组内取第一个严格大于 query_y 的 y
        int start = upper_bound(xs.begin(), xs.end(), ox[i]) - xs.begin() + 1; // 严格大于 query_x 的第一个组号
        int g = find_first(1, 1, groups, start, oy[i]);
        if (g == 0) {
            cout << "-1\n";
            continue;
        }
        int answer_y = *group_y[g].upper_bound(oy[i]);
        cout << xs[g - 1] << ' ' << answer_y << '\n';
    }
    return 0;
}

复杂度

预处理是对至多 nn 个 xx 排序去重,O(nlog⁡n)O(n\log n)。每个操作只碰一条线段树路径和一个 set 结点:add/remove 是 O(log⁡n)O(\log n)(set 一次插入/删除 + 线段树一次更新),find 是 O(log⁡n)O(\log n)(单路下降 + 一次 upper_bound)。总时间 O(nlog⁡n)O(n\log n),空间 O(n)O(n)。

解法二:组内 Fenwick 计数树 + 外层线段树

思路

外层那棵树不动,只把「组内容器」从平衡树换成一个计数树。

先离线读完所有操作,顺便把所有会被插入的点收集起来;按 xx 分组、组内 yy 升序去重,得到每组一张局部坐标表。既然每个组的候选 yy 全集都已知,组内的「当前有哪些 yy 还挂着」就可以用一个 Fenwick 存出现次数(00 或 11),不必真的保存有序结构。

组内要回答的两件事都能化成秩问题:

  • 组内最大 yy:设该组当前活跃点数为 cc,则最大 yy 就是该组第 cc 个活跃 yy。用 Fenwick 的 kth(二进制提升)一次定位。空组直接取 −1-1。
  • 第一个大于 y0y_0 的活跃 yy:先用前缀和数出 ⩽y0\leqslant y_0 的活跃点个数 ss,那么答案是第 s+1s+1 个活跃 yy,同样一次 kth。

kth 的二进制提升就是在计数树上找「最小前缀和达到 kk 的坐标」,每步 O(log⁡)O(\log),所以组内定位和平衡树同阶,但全是数组下标运算,没有指针、没有结点分配。

外层仍然是解法一那套:结点存组区间最大 yy,靠剪枝单路下降找最左可行组,区间同样用闭区间 [1,g组数][1,g_{\text{组数}}]、找不到返回 00,两份代码的线段树骨架逐行对应。两层的分工也完全对应——外层找最小可行的 xx,内层找最小可行的 yy。

Python 侧只能走这条路线。 Python 的 set 只能判存在、不能 upper_bound,标准库也没有平衡树,所以解法一的组内容器在 Python 里没有等价物;而「离线压缩 + 数组 Fenwick」只用 array 和 bisect,可以完整复刻,这也是 main2.py 的写法。代价是很明显的:必须先把输入全部读完,无法边读边答。

代码

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-08-02
 * update_at: 2026-10-28 15:18
 */

/* CF19D Points —— 解法二:每组一个 Fenwick + 外层线段树带剪枝单路下降 */
/* 离线坐标压缩:每个 x 组用 Fenwick 维护活跃 y,外层线段树存每组最大 y。
 * find 时先在线段树找第一个 x 更大且最大 y 更大的组,
 * 再在该组 Fenwick 里找第一个大于 query_y 的活跃 y。
 * 与解法一的差别只在组内容器:这里用离线压缩后的 Fenwick 计数树,
 * 组内定位靠「前缀计数 + kth 二进制提升」,不需要 std::set 的结点开销。 */

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

const int MAXN = 200000 + 5;

int n;                         // 操作数
char op[MAXN];
long long ox[MAXN], oy[MAXN];  // 操作参数
vector<long long> xs;          // 所有出现过的 x(升序去重),下标即组号
vector<long long> ys;          // 每个组的 y 连续存放
vector<int> yoff;              // yoff[g]..yoff[g+1] 是组 g 的 y 区间
vector<long long> fenwick;     // 所有组的 Fenwick 共享一块数组
vector<int> bit_off;           // 组 g 的 Fenwick 起点
vector<int> cnt;               // 组 g 当前活跃点数
const int MAXN_SEG = 200000 * 4;
long long seg[MAXN_SEG];       // 线段树:结点覆盖的组区间内最大 y,空组为 -1
int groups;                    // 组数,线段树区间是 [1, groups]

// 组 g(1-based)的候选 y 个数
int bit_len(int g) {
    return yoff[g] - yoff[g - 1];
}

void bit_add(int g, int idx, int delta) {
    int base = bit_off[g];
    int len = bit_len(g);
    while (idx <= len) {
        fenwick[base + idx] += delta;
        idx += idx & -idx;
    }
}

long long bit_sum(int g, int idx) {
    int base = bit_off[g];
    long long res = 0;
    while (idx > 0) {
        res += fenwick[base + idx];
        idx -= idx & -idx;
    }
    return res;
}

// 组内找第 target 个活跃 y 的压缩下标(Fenwick 二进制提升)
int bit_kth(int g, long long target) {
    int base = bit_off[g];
    int len = bit_len(g);
    int idx = 0;
    int step = 1 << (31 - __builtin_clz(len)); // 不超过 len 的最大 2 的幂
    while (step) {
        int nxt = idx + step;
        if (nxt <= len && fenwick[base + nxt] < target) {
            idx = nxt;
            target -= fenwick[base + nxt];
        }
        step >>= 1;
    }
    return idx + 1;
}

// 点更新:把第 pos 组的叶子值改成 value,并向上合并区间最大值
void seg_update(int p, int l, int r, int pos, long long value) {
    if (l == r) {
        seg[p] = value;
        return;
    }
    int mid = (l + r) >> 1;
    if (pos <= mid)
        seg_update(p << 1, l, mid, pos, value);
    else
        seg_update(p << 1 | 1, mid + 1, r, pos, value);
    seg[p] = max(seg[p << 1], seg[p << 1 | 1]);
}

// 找最小的组号 g 使 start <= g 且第 g 组的最大 y > y;不存在返回 0
int first_x(int p, int l, int r, int start, long long y) {
    if (r < start || seg[p] <= y) // 整段在起点左边,或整段最大值都够不到 y
        return 0;
    if (l == r)
        return l;
    int mid = (l + r) >> 1;
    int res = first_x(p << 1, l, mid, start, y); // 先左后右,返回的组号自然最小
    if (res)
        return res;
    return first_x(p << 1 | 1, mid + 1, r, start, y);
}

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

    cin >> n;
    set<pair<long long, long long>> points;
    for (int i = 0; i < n; i++) {
        string s;
        cin >> s >> ox[i] >> oy[i];
        op[i] = s[0];
        if (op[i] == 'a')
            points.insert({ox[i], oy[i]});
    }

    // 坐标压缩:x 升序去重,每个 x 的 y 升序去重
    for (auto &p : points) {
        if (xs.empty() || xs.back() != p.first) {
            xs.push_back(p.first);
            yoff.push_back((int)ys.size());
        }
        ys.push_back(p.second);
    }
    yoff.push_back((int)ys.size());
    groups = (int)xs.size();

    bit_off.assign(groups + 2, 0); // bit_off[g]:组 g 的 Fenwick 在共享数组里的起点,1-based
    for (int g = 1; g <= groups; g++)
        bit_off[g + 1] = bit_off[g] + bit_len(g) + 1;
    fenwick.assign(bit_off[groups + 1], 0);
    cnt.assign(groups + 1, 0);

    fill(seg, seg + 4 * groups + 4, -1); // 初始一个点都没有,所有组都按空组算

    for (int i = 0; i < n; i++) {
        if (op[i] != 'f') {
            int g = (int)(lower_bound(xs.begin(), xs.end(), ox[i]) - xs.begin()) + 1; // 组号从 1 开始
            int s = yoff[g - 1], e = yoff[g];
            int pos = (int)(lower_bound(ys.begin() + s, ys.begin() + e, oy[i]) - ys.begin()) - s + 1;
            int delta = (op[i] == 'a') ? 1 : -1;
            bit_add(g, pos, delta);
            cnt[g] += delta;
            long long maxy = -1;
            if (cnt[g] > 0)
                maxy = ys[s + bit_kth(g, cnt[g]) - 1]; // 第 cnt 个即最大
            seg_update(1, 1, groups, g, maxy);
            continue;
        }

        // find:先找第一个 x 更大的可行组
        int start = (int)(upper_bound(xs.begin(), xs.end(), ox[i]) - xs.begin()) + 1;
        int g = first_x(1, 1, groups, start, oy[i]);
        if (g == 0) {
            cout << "-1\n";
            continue;
        }
        int s = yoff[g - 1], e = yoff[g];
        int pos = (int)(upper_bound(ys.begin() + s, ys.begin() + e, oy[i]) - ys.begin()) - s;
        long long target = bit_sum(g, pos) + 1; // 第一个严格大于 query_y 的活跃 y 的秩
        long long ans_y = ys[s + bit_kth(g, target) - 1];
        cout << xs[g - 1] << ' ' << ans_y << '\n';
    }
    return 0;
}
python
import sys
from array import array
from bisect import bisect_left, bisect_right


input = sys.stdin.buffer.readline
request_count = int(input())
kinds = bytearray()
request_x = array("q")
request_y = array("q")
points = []
for _ in range(request_count):
    operation, x, y = input().split()
    kind = 0 if operation == b'add' else 1 if operation == b'remove' else 2
    x, y = int(x), int(y)
    kinds.append(kind)
    request_x.append(x)
    request_y.append(y)
    if kind == 0:
        points.append((x, y))

# 离线坐标压缩:x 升序去重后,组号 = 下标 + 1(与 main2.cpp 的 1-based 组号一致)
points.sort()
x_values_list = []
y_values_list = []
y_offsets_list = []
previous = None
for point in points:
    if point == previous:
        continue
    x, y = point
    if not x_values_list or x != x_values_list[-1]:
        x_values_list.append(x)
        y_offsets_list.append(len(y_values_list))
    y_values_list.append(y)
    previous = point
y_offsets_list.append(len(y_values_list))
del points

x_values = array("q", x_values_list)
y_values = array("q", y_values_list)
y_offsets = array("i", y_offsets_list)
del x_values_list, y_values_list, y_offsets_list

groups = len(x_values)
# 组 g(1-based)的候选 y 是 y_values[y_offsets[g-1] .. y_offsets[g]-1]
bit_offsets = array("i", [0]) * (groups + 2)
for group in range(1, groups + 1):
    bit_offsets[group + 1] = bit_offsets[group] + (y_offsets[group] - y_offsets[group - 1]) + 1
fenwick = array("i", [0]) * bit_offsets[groups + 1]
counts = array("i", [0]) * (groups + 1)
segment = array("q", [-1]) * (4 * groups + 4)


def bit_length_of(group):
    """组 group 的候选 y 个数。"""
    return y_offsets[group] - y_offsets[group - 1]


def bit_add(group, index, delta):
    base = bit_offsets[group]
    length = bit_length_of(group)
    while index <= length:
        fenwick[base + index] += delta
        index += index & -index


def bit_sum(group, index):
    base = bit_offsets[group]
    result = 0
    while index:
        result += fenwick[base + index]
        index -= index & -index
    return result


def bit_kth(group, target):
    """组内找第 target 个活跃 y 的压缩下标(1-based)。"""
    base = bit_offsets[group]
    length = bit_length_of(group)
    index = 0
    step = 1 << (length.bit_length() - 1)
    while step:
        next_index = index + step
        if next_index <= length and fenwick[base + next_index] < target:
            index = next_index
            target -= fenwick[base + index]
        step >>= 1
    return index + 1


def seg_update(node, left, right, pos, value):
    """点更新:把第 pos 组的叶子值改成 value,并向上合并区间最大值。"""
    if left == right:
        segment[node] = value
        return
    middle = (left + right) // 2
    if pos <= middle:
        seg_update(node * 2, left, middle, pos, value)
    else:
        seg_update(node * 2 + 1, middle + 1, right, pos, value)
    segment[node] = max(segment[node * 2], segment[node * 2 + 1])


def first_x(node, left, right, start, y):
    """找最小的组号 g 使 start <= g 且第 g 组的最大 y > y;不存在返回 0。"""
    if right < start or segment[node] <= y:  # 整段在起点左边,或整段最大值都够不到 y
        return 0
    if left == right:
        return left
    middle = (left + right) // 2
    result = first_x(node * 2, left, middle, start, y)  # 先左后右,返回的组号自然最小
    return result if result else first_x(node * 2 + 1, middle + 1, right, start, y)


answers = []
for kind, x, y in zip(kinds, request_x, request_y):
    if kind < 2:
        group = bisect_left(x_values, x) + 1  # 组号从 1 开始
        start, end = y_offsets[group - 1], y_offsets[group]
        position = bisect_left(y_values, y, start, end) - start + 1
        delta = 1 if kind == 0 else -1
        bit_add(group, position, delta)
        counts[group] += delta
        if counts[group]:
            maximum = y_values[start + bit_kth(group, counts[group]) - 1]
        else:
            maximum = -1
        seg_update(1, 1, groups, group, maximum)
        continue
    group = first_x(1, 1, groups, bisect_right(x_values, x) + 1, y)
    if group == 0:
        answers.append('-1')
        continue
    start, end = y_offsets[group - 1], y_offsets[group]
    position = bisect_right(y_values, y, start, end) - start
    target = bit_sum(group, position) + 1
    answer_y = y_values[start + bit_kth(group, target) - 1]
    answers.append(f'{x_values[group - 1]} {answer_y}')
sys.stdout.write('\n'.join(answers))

复杂度

预处理 O(nlog⁡n)O(n\log n)(排序去重 + 建 Fenwick)。每个操作 O(log⁡n)O(\log n):add/remove 是组内一次 Fenwick 更新 + 一次 kth 求最大 yy + 一次线段树更新;find 是一次单路下降 + 一次前缀和 + 一次 kth。总时间 O(nlog⁡n)O(n\log n),空间 O(n)O(n)。

复杂度对比

方案 组内容器 是否在线 每次操作 空间 关键代价
解法一 main.cpp std::set<int> 是 O(log⁡n)O(\log n) O(n)O(n) 平衡树结点开销、常数稍大
解法二 main2.cpp 离线压缩 + Fenwick 否 O(log⁡n)O(\log n) O(n)O(n) 必须先读完输入;多一个 kth 定位
解法二 main2.py 同上(array) 否 O(log⁡n)O(\log n) O(n)O(n) 同上;Python 下靠纯数组换常数

三份实现的渐进复杂度相同,拉开差距的是常数与前提条件。本题 n⩽2×105n\leqslant 2\times 10^5、坐标上限 10910^9,两个解法都能轻松通过(实测在本机于最大数据上均在 0.2s 内)。

总结

这道题的解法可以概括成一句话:二维的最左可行点查询,拆成「外层找最小可行的 xx,内层找最小可行的 yy」,外层用带极值剪枝的线段树单路下降。

几个值得记住的细节:

  • 外层不能在「组内最大 yy」这个量上直接二分,因为它不单调;合法的判据是「区间最大值」,而它恰好就是线段树结点存的东西,于是二分可以整个搬进下降过程,把 O(log⁡2n)O(\log^2 n) 压回 O(log⁡n)O(\log n)。
  • 剪枝条件有两个,缺一个就退化:mx⩽y0mx\leqslant y_0 管「这段无解」,r<startr<\text{start} 管「这段越界」。
  • 内层容器可以自由替换——平衡树在线、Fenwick 离线省常数,两者的接口都只是「取最大」和「取第一个大于 y0y_0 的」,所以外层代码可以完全不动。
  • 严格大于、严格右上都体现为 upper_bound 与 seg[p] <= y 里的那个等号方向,改错一个符号就会把「共线」的点也算进答案。