每日算法 — 使用java实现Bentley-Ottmann:扫描线线段交点检测

问题背景

线段交点检测是计算几何中的经典问题:给定平面上 n 条线段,找出所有相交的线段对及其交点。

暴力法需要检查所有 C(n, 2) = O(n²) 对线段,当 n 较大时效率极低。Bentley-Ottmann 算法通过扫描线(Sweep Line)技术,将时间复杂度优化到 O((n + k) log n),其中 k 是交点数量。

核心思想

算法维护一条垂直的扫描线,从左向右移动。在移动过程中,跟踪与扫描线相交的所有线段,并按交点 y 坐标排序存储在平衡树中。

事件点(Event Points)

扫描线在以下位置停止处理:
1. 线段左端点:新线段进入扫描线
2. 线段右端点:线段离开扫描线
3. 线段交点:相交线段的顺序在状态树中交换

扫描线状态(Status Structure)

一棵平衡二叉搜索树(如 TreeSet),存储当前与扫描线相交的线段,按键值排序(键值为线段在当前扫描线 x 位置的 y 坐标)。

关键观察

两条线段只有在扫描线状态中相邻时,才可能相交。因此只需检查相邻线段的交点,将可能交点加入事件队列。

Java 实现

import java.util.*;

public class BentleyOttmann {

    static class Point implements Comparable<Point> {
        double x, y;
        Point(double x, double y) { this.x = x; this.y = y; }

        @Override
        public int compareTo(Point other) {
            if (Math.abs(this.x - other.x) > 1e-9) {
                return Double.compare(this.x, other.x);
            }
            return Double.compare(this.y, other.y);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Point)) return false;
            Point p = (Point) o;
            return Math.abs(x - p.x) < 1e-9 && Math.abs(y - p.y) < 1e-9;
        }

        @Override
        public int hashCode() {
            return Objects.hash((int)(x * 1e9), (int)(y * 1e9));
        }

        @Override
        public String toString() {
            return String.format("(%.3f, %.3f)", x, y);
        }
    }

    static class Segment {
        Point p1, p2; // p1 是左端点,p2 是右端点
        int id;

        Segment(Point p1, Point p2, int id) {
            if (p1.x > p2.x || (Math.abs(p1.x - p2.x) < 1e-9 && p1.y > p2.y)) {
                this.p1 = p2; this.p2 = p1;
            } else {
                this.p1 = p1; this.p2 = p2;
            }
            this.id = id;
        }

        /**
         * 计算线段在当前扫描线 x 处的 y 值
         */
        double yAt(double x) {
            if (Math.abs(p2.x - p1.x) < 1e-9) return p1.y;
            return p1.y + (p2.y - p1.y) * (x - p1.x) / (p2.x - p1.x);
        }
    }

    /**
     * 计算两条线段的交点
     * @return 交点,如果不相交则返回 null
     */
    static Point intersect(Segment s1, Segment s2) {
        double x1 = s1.p1.x, y1 = s1.p1.y, x2 = s1.p2.x, y2 = s1.p2.y;
        double x3 = s2.p1.x, y3 = s2.p1.y, x4 = s2.p2.x, y4 = s2.p2.y;

        double denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
        if (Math.abs(denom) < 1e-9) return null; // 平行

        double px = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / denom;
        double py = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / denom;

        // 检查交点是否在两线段上
        if (px >= Math.min(x1, x2) - 1e-9 && px <= Math.max(x1, x2) + 1e-9 &&
            px >= Math.min(x3, x4) - 1e-9 && px <= Math.max(x3, x4) + 1e-9 &&
            py >= Math.min(y1, y2) - 1e-9 && py <= Math.max(y1, y2) + 1e-9 &&
            py >= Math.min(y3, y4) - 1e-9 && py <= Math.max(y3, y4) + 1e-9) {
            return new Point(px, py);
        }
        return null;
    }

    static class Event implements Comparable<Event> {
        Point point;
        int type; // 0: 左端点, 1: 右端点, 2: 交点
        Segment seg1, seg2;

        Event(Point p, int type, Segment s1, Segment s2) {
            this.point = p; this.type = type;
            this.seg1 = s1; this.seg2 = s2;
        }

        @Override
        public int compareTo(Event other) {
            int cmp = this.point.compareTo(other.point);
            if (cmp != 0) return cmp;
            // 左端点先于交点,交点先于右端点
            return Integer.compare(this.type, other.type);
        }
    }

    static class SweepComparator implements Comparator<Segment> {
        double currentX;

        SweepComparator(double x) { this.currentX = x; }

        @Override
        public int compare(Segment s1, Segment s2) {
            double y1 = s1.yAt(currentX);
            double y2 = s2.yAt(currentX);
            if (Math.abs(y1 - y2) > 1e-9) return Double.compare(y1, y2);
            return Integer.compare(s1.id, s2.id);
        }
    }

    /**
     * Bentley-Ottmann 主算法
     */
    public static List<Point> findIntersections(List<Segment> segments) {
        List<Point> intersections = new ArrayList<>();
        PriorityQueue<Event> eventQueue = new PriorityQueue<>();
        TreeSet<Segment> status = new TreeSet<>();
        Map<Integer, Segment> idToSeg = new HashMap<>();

        // 初始化事件队列
        for (Segment seg : segments) {
            eventQueue.add(new Event(seg.p1, 0, seg, null));
            eventQueue.add(new Event(seg.p2, 1, seg, null));
            idToSeg.put(seg.id, seg);
        }

        double currentX = Double.NEGATIVE_INFINITY;
        SweepComparator comparator = new SweepComparator(currentX);

        while (!eventQueue.isEmpty()) {
            Event event = eventQueue.poll();
            currentX = event.point.x;
            comparator.currentX = currentX;

            if (event.type == 0) { // 左端点:插入线段
                Segment seg = event.seg1;
                status.add(seg);

                Segment above = status.higher(seg);
                Segment below = status.lower(seg);

                if (above != null) {
                    Point inter = intersect(seg, above);
                    if (inter != null && inter.x >= currentX - 1e-9) {
                        eventQueue.add(new Event(inter, 2, seg, above));
                    }
                }
                if (below != null) {
                    Point inter = intersect(seg, below);
                    if (inter != null && inter.x >= currentX - 1e-9) {
                        eventQueue.add(new Event(inter, 2, seg, below));
                    }
                }

            } else if (event.type == 1) { // 右端点:删除线段
                Segment seg = event.seg1;
                Segment above = status.higher(seg);
                Segment below = status.lower(seg);
                status.remove(seg);

                if (above != null && below != null) {
                    Point inter = intersect(above, below);
                    if (inter != null && inter.x >= currentX - 1e-9) {
                        eventQueue.add(new Event(inter, 2, above, below));
                    }
                }

            } else { // 交点
                Point inter = event.point;
                intersections.add(inter);

                Segment s1 = event.seg1;
                Segment s2 = event.seg2;

                // 在状态树中交换两条线段的位置
                status.remove(s1);
                status.remove(s2);
                comparator.currentX = currentX + 1e-6; // 稍微向右移动
                status.add(s1);
                status.add(s2);
                comparator.currentX = currentX;
            }
        }

        return intersections;
    }

    public static void main(String[] args) {
        List<Segment> segments = new ArrayList<>();
        segments.add(new Segment(new Point(0, 0), new Point(5, 5), 0));
        segments.add(new Segment(new Point(0, 5), new Point(5, 0), 1));
        segments.add(new Segment(new Point(2, 0), new Point(2, 6), 2));
        segments.add(new Segment(new Point(1, 2), new Point(4, 2), 3));

        List<Point> intersections = findIntersections(segments);
        System.out.println("找到的交点:");
        for (Point p : intersections) {
            System.out.println("  " + p);
        }
    }
}

简化版暴力对比实现

public class BruteForceIntersection {
    public static List<BentleyOttmann.Point> bruteForce(
            List<BentleyOttmann.Segment> segments) {
        List<BentleyOttmann.Point> result = new ArrayList<>();
        for (int i = 0; i < segments.size(); i++) {
            for (int j = i + 1; j < segments.size(); j++) {
                BentleyOttmann.Point p = BentleyOttmann.intersect(
                    segments.get(i), segments.get(j));
                if (p != null) result.add(p);
            }
        }
        return result;
    }
}

复杂度分析

指标 暴力法 Bentley-Ottmann
时间复杂度 O(n²) O((n + k) log n)
空间复杂度 O(1) O(n + k)
适用场景 n 小于 100 n 大于 100 或 k 远小于 n²

其中 k 是交点数量。当 k = O(n²) 时,两种方法都退化为 O(n²),但 Bentley-Ottmann 的实际常数因子通常更小。

应用场景

  1. CAD/CAM 系统:检测设计图纸中的线段冲突
  2. GIS 地理信息系统:道路网络交叉点分析
  3. 游戏物理引擎:2D 多边形边碰撞检测
  4. VLSI 设计:集成电路布线的冲突检测
  5. 计算机图形学:光线追踪中的线段求交加速
  6. 模式识别:轮廓线段的交点特征提取

总结

Bentley-Ottmann 算法是计算几何中扫描线技术的典范。通过维护动态有序的状态结构和事件驱动的处理流程,它将看似需要 O(n²) 的交点检测问题优化到近线性时间。理解这一算法有助于掌握计算几何中更复杂的问题求解范式,如多边形布尔运算、Voronoi 图构造和 Delaunay 三角剖分。