📜  门|门CS 2012 |第 38 题(1)

📅  最后修改于: 2023-12-03 15:42:22.741000             🧑  作者: Mango

门|门CS 2012 |第 38 题

题目描述

在一个平面内给你 $n\ (n \leqslant 10^6)$ 条线段,支持以下操作:

  1. Add x1 y1 x2 y2:加入一条从 $(x_1, y_1)$ 到 $(x_2, y_2)$ 的线段。
  2. Ask x y:查询 $(x, y)$ 最靠近它的线段的编号。
解题思路

这道题可以用扫描线+线段树/平衡树解决。

对于每个询问点 $(x, y)$,我们可以先把所有和 $(x, y)$ 有关的线段提取出来,这个可以用线段树/平衡树实现。

然后再将这些线段按照和点 $(x, y)$ 的距离从小到大排序,距离相同的按照斜率(也就是 $\dfrac{y_i - y}{x_i - x}$)排序。

最后直接取距离最小的线段即可。

Add 操作只需要在线段树/平衡树上插入新的线段即可。

具体实现可参考以下代码:

// C++代码
struct Line {
    int id;
    double k, b, l, r;
    bool operator < (const Line& other) const {
        if (fabs(k - other.k) < eps) {
            return (k > 0 ? b > other.b : b < other.b);
        } else {
            return (k > other.k);
        }
    }
};

struct Query {
    int x, y, id;
    bool operator < (const Query& other) const {
        return x < other.x;
    }
};

vector<Query> qs;

int n, m, idx;
Line lines[MAXN << 2], tmp[MAXN << 2];
double ans[MAXN];

void divide (int l, int r) { // 线段树区间处理
    if (l == r) return;
    int mid = (l + r) >> 1;

    divide(l, mid);
    divide(mid + 1, r);

    int i = l, j = mid + 1;
    merge(lines + l, lines + mid + 1, lines + mid + 1, lines + r + 1, tmp);

    for (int k = l; k <= r; ++k) {
        if (j > r || (i <= mid && tmp[i].b >= tmp[j].b)) {
            lines[k] = tmp[i];
            i++;
        } else {
            lines[k] = tmp[j];
            j++;
        }
    }

    for (int k = l; k <= r; ++k) {
        if (k <= mid) {
            for (int p = j; p <= r; ++p) {
                if (lines[p].k > lines[k].k) {
                    ans[lines[k].id] = min(ans[lines[k].id], calc_dist(lines[k], lines[p]));
                    break;
                }
            }
        } else {
            for (int p = i; p <= mid; ++p) {
                if (lines[p].k < lines[k].k) {
                    ans[lines[k].id] = min(ans[lines[k].id], calc_dist(lines[k], lines[p]));
                    break;
                }
            }
        }
    }
}

int main () {
    // 输入略去
    for (int i = 1; i <= m; ++i) {
        scanf("%d%d", &qs[i].x, &qs[i].y);
        qs[i].id = i;
    }
    sort(qs + 1, qs + 1 + m);

    memset(ans, 0x7f, sizeof(ans));
    idx = 0;

    for (int i = 1; i <= m; ++i) {
        while (idx < n && lines[idx].l <= qs[i].x) idx++; // 将和询问点有关的所有线段插入

        for (int j = idx - 1; j >= 0 && lines[j].r >= qs[i].x; --j) { // 从右往左扫描,因为右边的区间更小
            if (calc_dist(lines[j], qs[i]) < ans[lines[j].id]) {
                ans[lines[j].id] = calc_dist(lines[j], qs[i]);
            }
        }

        Query& q = qs[i];
        lines[idx++] = { i, INF, q.y + 1.0 / INF, q.x, q.x }; // INF是float最大值,保证y比所有线段的最大端点y值都大
    }

    divide(0, idx - 1);

    for (int i = 1; i <= m; ++i) {
        printf("%.2lf\n", ans[i]); // 输出答案
    }
    return 0;
}
时间复杂度

对于 Add 操作,我们需要在线段树/平衡树上插入一条线段,时间复杂度 $O(\log n)$。

对于每个询问点 $(x, y)$,我们需要在线段树/平衡树上查询附近的所有线段,并将其排序,时间复杂度 $O(\log^2 n)$。

因此总时间复杂度为 $O(n\log^2 n)$。

参考资料