云计算百科
云计算领域专业知识百科平台

UVa 12647 Balloon

题目描述

给定一个由若干线段组成的天花板,线段之间没有公共点。气球从地面点 (x,0)(x, 0)(x,0) 垂直向上释放。当气球碰到水平线段时,它会卡在该位置;当碰到倾斜线段时,它会沿着线段滑到该线段的最高点,然后从该点继续垂直向上运动。这个过程可能重复多次,最终气球要么卡在某条水平线段上,要么从大厅逃出。

需要回答多个查询,每个查询给定一个释放点的横坐标 xxx,输出气球的最终位置:若逃出则输出逃出时的横坐标,否则输出卡住位置的坐标 (x,y)(x, y)(x,y)

输入格式

每个测试用例第一行包含两个整数 NNNCCC,分别表示线段数量和查询数量。

接下来 NNN 行,每行四个整数 X1,Y1,X2,Y2X_1, Y_1, X_2, Y_2X1,Y1,X2,Y2,描述一条线段的两个端点。

接下来 CCC 行,每行一个整数 XXX,表示查询的释放点横坐标。

输入包含多个测试用例,直到文件结束。

输出格式

对于每个查询,输出一行:

  • 若气球逃出,输出一个整数 XXX,表示逃出时的横坐标。
  • 否则输出两个整数 XXXYYY,表示卡住的位置。

样例

输入

4 4
0 1 3 3
1 5 6 5
5 3 2 4
7 4 10 2
2
5
8
6
4 3
1 3 4 2
10 3 7 4
2 3 8 3
3 5 5 4
4
9
8

输出

2 5
2 5
7
6 5
1
7
8 3

题目分析

本题的核心是模拟气球在天花板下的运动轨迹。天花板由若干线段组成,线段之间没有公共点,且所有端点的横坐标互不相同。

直接模拟的方法是从释放点开始,每次找到当前正上方最近的线段。如果是水平线段,气球卡住;如果是倾斜线段,气球滑到最高点后继续向上。这个过程可能重复多次。

然而,直接模拟存在两个问题:

  • 每次查找上方最近线段需要遍历所有线段,复杂度 O(N)O(N)O(N)
  • 每个查询可能经历多次滑动,最坏情况下每个查询需要 O(N)O(N)O(N) 次查找。

总的复杂度为 O(C⋅N2)O(C \\cdot N^2)O(CN2),在 N,C≤105N, C \\le 10^5N,C105 时不可接受。

解题思路

核心洞察

从高到低处理线段 是本题的关键。

假设我们按照线段的高度从高到低处理。当处理到一条倾斜线段时,它最高点正上方的线段一定已经被处理过了(因为更高)。因此,这条倾斜线段可以直接“继承”其最高点正上方线段的结果,而不需要模拟气球在它上方的多次滑动。

这个思路将问题转化为:用线段树维护每个横坐标当前被哪条线段覆盖,从高到低处理每条线段,并记录每条线段最终会卡在哪条水平线段上。

预处理

首先,对每条线段进行处理,保证其左端点的纵坐标不小于右端点的纵坐标:

if (seg[i].y1 < seg[i].y2) {
swap(seg[i].x1, seg[i].x2);
swap(seg[i].y1, seg[i].y2);
}

这样处理后:

  • 水平线段:y1=y2y_1 = y_2y1=y2
  • 倾斜线段:最高点一定在 (x1,y1)(x_1, y_1)(x1,y1)

同时,添加一条无限高的水平线段作为“天花板”,其高度为 MAXC+1MAXC + 1MAXC+1,其中 MAXC=1000010MAXC = 1000010MAXC=1000010 是横坐标的最大范围。这样,所有查询的最终归宿要么是某条真实的水平线段,要么是这条天花板(表示逃出)。

数据结构

使用线段树维护区间覆盖:

  • cover[rt] 表示当前节点对应的区间被哪条线段覆盖,−1-11 表示未被覆盖。
  • 支持区间赋值操作(update)和单点查询操作(query)。

算法流程

  • 将所有线段(包括天花板)按纵坐标升序排序。

    • 排序后,seg[0] 是最低的线段,seg[n] 是最高的天花板。
    • 但处理时从高到低,即循环 for (int i = n; i >= 0; –i)。
  • 从高到低处理每条线段:

    • 如果是水平线段:记录 f[i]=if[i] = if[i]=i,表示它最终卡在自己这里。
    • 如果是倾斜线段:
      • 查询其高点 (x1,y1)(x_1, y_1)(x1,y1) 被哪条线段覆盖,得到 k=query(x1)k = \\text{query}(x_1)k=query(x1)
      • 继承该线段的结果:f[i]=f[k]f[i] = f[k]f[i]=f[k]
      • 如果 kkk 是水平线段,则 ansX[i]=x1ansX[i] = x_1ansX[i]=x1(卡住时的横坐标就是当前高点的横坐标);否则继承 ansX[i]=ansX[k]ansX[i] = ansX[k]ansX[i]=ansX[k]
    • 将当前线段覆盖的横坐标区间 [L,R][L, R][L,R] 赋值为 iii(其中 L=min⁡(x1,x2)L = \\min(x_1, x_2)L=min(x1,x2)R=max⁡(x1,x2)R = \\max(x_1, x_2)R=max(x1,x2))。
  • 处理查询:

    • 查询释放点 xxx 被哪条线段覆盖,得到 kkk
    • 如果 kkk 是倾斜线段,将 xxx 修正为 ansX[k]ansX[k]ansX[k]
    • 如果 f[k]=nf[k] = nf[k]=n(即最终归宿是天花板),输出 xxx(逃出);否则输出 xxxseg[f[k]].y1seg[f[k]].y1seg[f[k]].y1(卡在水平线段上)。
  • 正确性说明

    由于我们按照高度从高到低处理,当处理到一条倾斜线段时,它最高点上方的线段已经处理完毕,因此其最终归宿可以直接继承。这与气球实际运动的逻辑一致:气球从低处向上运动,最终卡住的水平线段由最上方的那条决定。

    线段树维护了每个横坐标当前被哪条线段覆盖,保证了查询和继承操作的正确性。

    复杂度分析

    • 排序:O(Nlog⁡N)O(N \\log N)O(NlogN)
    • 线段树操作:每次更新和查询均为 O(log⁡MAXC)O(\\log MAXC)O(logMAXC),共 O(N+C)O(N + C)O(N+C) 次操作
    • 总时间复杂度:O((N+C)log⁡MAXC)O((N + C) \\log MAXC)O((N+C)logMAXC),其中 MAXC=1000010MAXC = 1000010MAXC=1000010 为常数
    • 空间复杂度:O(MAXC)O(MAXC)O(MAXC)

    代码实现

    // Balloon
    // UVa ID: 12647
    // Verdict: Accepted
    // Submission Date: 2026-06-12
    // UVa Run Time: 0.120s
    //
    // 版权所有(C)2026,邱秋。metaphysis # yeah dot net

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

    const int MAXC = 1000010;
    const int MAXN = 100010;

    int cover[MAXC << 2];
    int f[MAXN];
    int ansX[MAXN];
    int n, m;

    struct Segment {
    int x1, y1, x2, y2;
    bool operator<(const Segment &b) const {
    return y1 < b.y1;
    }
    } seg[MAXN];

    void pushDown(int rt) {
    if (cover[rt] != 1) {
    cover[rt << 1] = cover[rt << 1 | 1] = cover[rt];
    cover[rt] = 1;
    }
    }

    void update(int L, int R, int idx, int l, int r, int rt) {
    if (L <= l && r <= R) {
    cover[rt] = idx;
    return;
    }
    int m = (l + r) >> 1;
    pushDown(rt);
    if (L <= m) update(L, R, idx, l, m, rt << 1);
    if (R > m) update(L, R, idx, m + 1, r, rt << 1 | 1);
    }

    int query(int x, int l, int r, int rt) {
    if (cover[rt] >= 0) return cover[rt];
    int m = (l + r) >> 1;
    if (x <= m) return query(x, l, m, rt << 1);
    else return query(x, m + 1, r, rt << 1 | 1);
    }

    void solve() {
    sort(seg, seg + n + 1);
    memset(cover, 1, sizeof(cover));
    for (int i = n; i >= 0; i) {
    if (seg[i].y1 == seg[i].y2) {
    f[i] = i;
    } else {
    int k = query(seg[i].x1, 0, MAXC, 1);
    f[i] = f[k];
    if (seg[k].y1 == seg[k].y2) ansX[i] = seg[i].x1;
    else ansX[i] = ansX[k];
    }
    int L = seg[i].x1, R = seg[i].x2;
    if (L > R) swap(L, R);
    update(L, R, i, 0, MAXC, 1);
    }
    }

    void answerQuery(int x) {
    int k = query(x, 0, MAXC, 1);
    if (seg[k].y1 != seg[k].y2) x = ansX[k];
    if (f[k] == n) cout << x << '\\n';
    else cout << x << ' ' << seg[f[k]].y1 << '\\n';
    }

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

    while (cin >> n >> m) {
    for (int i = 0; i < n; ++i) {
    cin >> seg[i].x1 >> seg[i].y1 >> seg[i].x2 >> seg[i].y2;
    if (seg[i].y1 < seg[i].y2) {
    swap(seg[i].x1, seg[i].x2);
    swap(seg[i].y1, seg[i].y2);
    }
    }
    seg[n].x1 = 0;
    seg[n].y1 = MAXC + 1;
    seg[n].x2 = MAXC;
    seg[n].y2 = MAXC + 1;
    solve();
    while (m) {
    int x;
    cin >> x;
    answerQuery(x);
    }
    }
    return 0;
    }

    总结

    本题的核心技巧是逆向思维和区间覆盖:

  • 从高到低处理:将气球向上运动的过程转化为线段自上而下的处理,避免了模拟多次滑动。
  • 线段树维护覆盖:用区间赋值维护每个横坐标被哪条线段覆盖,支持快速查询和继承。
  • 添加天花板:将逃出情况统一为碰到天花板,简化了边界处理。
  • 这种方法将复杂的问题转化为经典的线段树区间覆盖问题,时间复杂度 O((N+C)log⁡MAXC)O((N + C) \\log MAXC)O((N+C)logMAXC),足以通过 N,C≤105N, C \\le 10^5N,C105 的数据规模。类似的思路可应用于其他涉及“向上碰撞”或“依赖关系”的问题中。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » UVa 12647 Balloon
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!