12.1 排列与计算几何

排列(Arrangement)与 Nef多面体 都支持布尔集合运算,而排列的基础概念与 多边形 密切相关。本章深入探讨排列的数学基础、核心算法和高级应用。

排列(Arrangement)是计算几何中的核心数据结构,用于表示平面上几何对象(如线段、曲线)的细分结构。本章深入探讨排列的数学基础、核心算法(如Bentley-Ottmann扫描线算法)、布尔集合运算、闵可夫斯基和以及直骨架等高级主题。这些技术在计算机图形学、机器人运动规划、CAD/CAM系统和地理信息系统等领域有广泛应用。

12.1.1 Arrangement数据结构

DCEL(Doubly Connected Edge List)详解

DCEL是表示平面细分(planar subdivision)的标准数据结构。它将平面划分为顶点、边和面三个基本元素,并通过精心设计的指针结构维护它们之间的拓扑关系。

DCEL的核心组件:

顶点(Vertex):
  - 坐标信息 (x, y)
  - 指向一条关联半边(incident halfedge)的指针

半边(Halfedge):
  - 指向起点顶点的指针
  - 指向孪生半边(twin)的指针
  - 指向下一条半边(next)的指针
  - 指向上一条半边(previous)的指针
  - 指向关联面的指针

面(Face):
  - 指向外部边界(outer CCB)的指针
  - 指向孔洞列表(holes)的指针
  - 指向孤立顶点的指针

DCEL拓扑结构示意图:

                    v2
                   /|\
                  / | \
                 /  |  \
                /   |   \
               /  f1|f2  \
              /     |     \
             /______|______\
            v0      v1      v3

半边关系:
  e0: v0 -> v1 (twin: e0', face: f1)
  e1: v1 -> v2 (twin: e1', face: f1)
  e2: v2 -> v0 (twin: e2', face: f1)
  e0': v1 -> v0 (twin: e0, face: f2)
  e1': v2 -> v1 (twin: e1, face: f2)
  e2': v0 -> v2 (twin: e2, face: f2)

DCEL的数学性质:

对于任何有效的平面细分,DCEL满足欧拉公式:

其中 是顶点数, 是边数, 是面数(包括无界面), 是连通分量数。

对于连通的平面细分():

顶点、边、面的拓扑关系

顶点的分类:

  1. 孤立顶点(Isolated Vertex):不与任何边关联,位于某个面的内部
  2. 边界顶点(Boundary Vertex):位于面的边界上,度(degree)至少为1
  3. 内部顶点(Interior Vertex):完全位于某个面的内部

顶点的度(Degree):

顶点的度等于与其关联的半边数量。对于简单多边形,所有顶点的度为2;对于排列,顶点的度可以是任意偶数(因为每条边贡献两个半边)。

// 获取顶点的度
template<typename Arrangement>
size_t vertex_degree(typename Arrangement::Vertex_const_handle v) {
    return v->degree();
}

面的边界表示:

每个面通过**连通分量边界(Connected Component of Boundary, CCB)**来描述其边界:

  • 外部边界(Outer CCB):面的外边界,逆时针方向
  • 孔洞(Holes):面内部的孔洞边界,顺时针方向
  • 孤立顶点:位于面内部的孤立点
面F的结构:
+---------------------------+
|  外部边界 (逆时针)         |
|  +---------------------+  |
|  |  孔洞1 (顺时针)     |  |
|  |  +---+              |  |
|  |  |   |              |  |
|  |  +---+              |  |
|  |                     |  |
|  |  孔洞2 (顺时针)     |  |
|  |     +--+            |  |
|  |     |  |            |  |
|  |     +--+            |  |
|  +---------------------+  |
|                           |
|  * 孤立顶点               |
+---------------------------+

组合嵌入的概念

**组合嵌入(Combinatorial Embedding)**是指平面图的拓扑结构,不考虑具体的几何形状,只关注顶点、边和面的连接关系。

关键概念:

  1. 旋转系统(Rotation System):对于每个顶点,定义与其关联的边的循环顺序
  2. 面迹(Face Walk):沿边的特定遍历顺序定义面
  3. 拓扑等价:两个嵌入如果具有相同的旋转系统,则称为拓扑等价

排列的组合嵌入:

给定一组曲线,其排列的组合嵌入由以下信息完全确定:

  • 所有交点(顶点)
  • 每个顶点处曲线的循环顺序
  • 每条曲线段(边)连接的两个顶点
示例:两条相交线段

    A-------B
      \   /
       \ /
        X      <-- 交点
       / \
      /   \
    C-------D

组合嵌入信息:
- 顶点: A, B, C, D, X
- 顶点X处的循环顺序: (XA, XC, XB, XD) 或 (XA, XD, XB, XC)
- 边: (A,X), (X,B), (C,X), (X,D)
- 面: 4个有界面 + 1个无界面

12.1.2 Bentley-Ottmann扫描线算法

算法原理

Bentley-Ottmann算法是计算线段排列(即求所有线段交点)的经典算法,时间复杂度为 ,其中 是线段数, 是交点数。

核心思想:

使用一条垂直扫描线从左向右扫过平面,维护扫描线与线段集合的交点顺序。当扫描线遇到特定事件(站点事件或交点事件)时,更新数据结构并检测新的交点。

扫描线过程可视化:

时间 t0:                    时间 t1:
    |                           |
    |    s1                     |    s1
    |   /                       |   / \
    |  /                        |  /   \
    | /   s2                    | /  X  s2  <-- 发现交点
    |/    |                     |/  /   |
    +------------>               +--/----->
                                  /
                                 s3

事件队列处理顺序:
1. 线段s1左端点 (站点事件)
2. 线段s2左端点 (站点事件)
3. 线段s3左端点 (站点事件)
4. s1与s2交点 (交点事件)
5. 线段s3右端点 (站点事件)
6. 线段s1右端点 (站点事件)
7. 线段s2右端点 (站点事件)

事件处理

事件类型:

  1. 站点事件(Site Event):扫描线遇到线段的左端点或右端点
  2. 交点事件(Intersection Event):扫描线遇到两个线段的交点

站点事件处理(左端点):

算法: 处理左端点事件
输入: 线段s,扫描线状态T,事件队列Q

1. 将s插入扫描线状态T,保持y坐标顺序
2. 令s_above = T中s上方的线段
3. 令s_below = T中s下方的线段
4. 如果s与s_above相交,将交点加入Q
5. 如果s与s_below相交,将交点加入Q

站点事件处理(右端点):

算法: 处理右端点事件
输入: 线段s,扫描线状态T,事件队列Q

1. 令s_above = T中s上方的线段
2. 令s_below = T中s下方的线段
3. 从T中删除s
4. 如果s_above与s_below相交,将交点加入Q

交点事件处理:

算法: 处理交点事件
输入: 交点p,涉及线段s1和s2,扫描线状态T,事件队列Q

1. 在T中交换s1和s2的位置
2. 令s_new_above = T中s1上方的线段
3. 令s_new_below = T中s2下方的线段
4. 如果s1与s_new_above相交,将交点加入Q
5. 如果s2与s_new_below相交,将交点加入Q
6. 输出交点p

扫描线状态维护:

扫描线状态使用平衡二叉搜索树(如std::set)维护,按线段与扫描线交点的y坐标排序。关键操作:

// 扫描线状态的比较器
struct SweepLineCompare {
    double sweep_x;  // 当前扫描线x坐标
    
    bool operator()(const Segment* s1, const Segment* s2) const {
        double y1 = s1->y_at_x(sweep_x);
        double y2 = s2->y_at_x(sweep_x);
        return y1 < y2;
    }
};

CGAL实现细节

CGAL的Arrangement_2类使用改进的Bentley-Ottmann算法构建排列。以下是核心实现示例:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Arr_non_caching_segment_traits_2.h>
#include <CGAL/Arrangement_2.h>
#include <vector>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef CGAL::Arr_non_caching_segment_traits_2<Kernel> Traits;
typedef Traits::Point_2 Point;
typedef Traits::X_monotone_curve_2 Segment;
typedef CGAL::Arrangement_2<Traits> Arrangement;
 
// 批量插入线段构建排列
void construct_arrangement(std::vector<Segment>& segments) {
    Arrangement arr;
    
    // 使用扫描线算法批量插入
    CGAL::insert(arr, segments.begin(), segments.end());
    
    std::cout << "排列统计:" << std::endl;
    std::cout << "  顶点数: " << arr.number_of_vertices() << std::endl;
    std::cout << "  边数: " << arr.number_of_edges() << std::endl;
    std::cout << "  面数: " << arr.number_of_faces() << std::endl;
}

复杂度分析:

操作时间复杂度空间复杂度
构建排列
单条曲线插入
点定位
遍历所有顶点

其中 是曲线数, 是交点数。

12.1.3 布尔集合运算

2D布尔运算

布尔集合运算对两个或多个多边形执行集合操作:并(Union)、交(Intersection)、差(Difference)和对称差(Symmetric Difference)。

布尔运算定义:

给定两个多边形

  • 并(Join/Union)
  • 交(Intersection)
  • 差(Difference)
  • 对称差

布尔运算可视化:

输入多边形:                    并运算结果:
+-------+                      +-------+
| P     |                      |       |
|   +---+---+                  |       +---+
|   | Q |   |      -->         |           |
+---+---+   |                  +-----------+
        |   |
        +---+

交运算结果:                    差运算结果:
    +---+                          +-------+
    |   |                          | P     |
    |   |                          |   +---+
    +---+                          +---+

对称差结果:
+-------+
|       |
|   +---+---+
+---+       |
    |       |
    +-------+

CGAL布尔运算实现:

#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
#include <CGAL/Boolean_set_operations_2.h>
#include <list>
 
typedef CGAL::Exact_predicates_exact_constructions_kernel Kernel;
typedef Kernel::Point_2 Point_2;
typedef CGAL::Polygon_2<Kernel> Polygon_2;
typedef CGAL::Polygon_with_holes_2<Kernel> Polygon_with_holes_2;
 
// 执行布尔并运算
bool compute_union(const Polygon_2& P, const Polygon_2& Q, 
                   Polygon_with_holes_2& result) {
    return CGAL::join(P, Q, result);
}
 
// 执行布尔交运算
void compute_intersection(const Polygon_2& P, const Polygon_2& Q,
                          std::list<Polygon_with_holes_2>& result) {
    CGAL::intersection(P, Q, std::back_inserter(result));
}
 
// 执行布尔差运算
void compute_difference(const Polygon_2& P, const Polygon_2& Q,
                        std::list<Polygon_with_holes_2>& result) {
    CGAL::difference(P, Q, std::back_inserter(result));
}
 
// 执行对称差运算
void compute_symmetric_difference(const Polygon_2& P, const Polygon_2& Q,
                                  std::list<Polygon_with_holes_2>& result) {
    CGAL::symmetric_difference(P, Q, std::back_inserter(result));
}

完整示例:

#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
#include <CGAL/Boolean_set_operations_2.h>
#include <list>
 
typedef CGAL::Exact_predicates_exact_constructions_kernel Kernel;
typedef Kernel::Point_2 Point_2;
typedef CGAL::Polygon_2<Kernel> Polygon_2;
typedef CGAL::Polygon_with_holes_2<Kernel> Polygon_with_holes_2;
 
typedef std::list<Polygon_with_holes_2> Pwh_list_2;
 
int main() {
    // 构造第一个多边形
    Polygon_2 P;
    P.push_back(Point_2(0, 0));
    P.push_back(Point_2(5, 0));
    P.push_back(Point_2(3.5, 1.5));
    P.push_back(Point_2(2.5, 0.5));
    P.push_back(Point_2(1.5, 1.5));
    
    // 构造第二个多边形
    Polygon_2 Q;
    Q.push_back(Point_2(0, 2));
    Q.push_back(Point_2(1.5, 0.5));
    Q.push_back(Point_2(2.5, 1.5));
    Q.push_back(Point_2(3.5, 0.5));
    Q.push_back(Point_2(5, 2));
    
    // 计算并集
    Polygon_with_holes_2 unionR;
    if (CGAL::join(P, Q, unionR)) {
        std::cout << "并集计算成功" << std::endl;
    }
    
    // 计算交集
    Pwh_list_2 intR;
    CGAL::intersection(P, Q, std::back_inserter(intR));
    
    return 0;
}

Nef多面体回顾

Nef多面体是表示多边形/多面体集合的通用数据结构,支持任意布尔运算。Nef多面体可以表示:

  • 开集和闭集
  • 非流形几何
  • 有孔洞的多边形

Nef多面体的层次结构:

Nef多面体
├── 选择函数(Selective Function)
│   └── 为每个面、边、顶点标记内外状态
├── 球面映射(Spherical Map)
│   └── 将局部邻域映射到单位球面
└── 拓扑结构
    ├── 顶点(Vertex)
    ├── 边(Edge)
    ├── 面(Face)
    └── 体积(Volume,3D)

Nef多面体的局部视图:

点p处的局部邻域:

        外部区域
           |
     ------+-------
          |p
    ------+-------
           |
        内部区域

球面映射: 将p的邻域投影到单位球面

           ^
           | 外部半球 (标记为外部)
    -------+-------
           |
           | 内部半球 (标记为内部)

正则化与非正则化运算

**正则化(Regularization)**是布尔运算的重要概念。集合 的正则化定义为:

即先取内部再取闭包。正则化消除”悬挂边”、“孤立点”等低维特征。

正则化与非正则化比较:

输入: 两个共享边的多边形

+-------+
|   A   |
|       |
+---+---+  <-- 共享边
    |   |
    | B |
    +---+

非正则化并集:              正则化并集:
+-------+                  +-------+
|       |                  |       |
|       |                  |       |
+---+---+                  +       +
    |   |                  |       |
    |   |                  |       |
    +---+                  +-------+
(保留共享边作为内部边)      (消除内部边)

CGAL的布尔运算默认产生正则化结果。使用Gps_segment_traits_2可以控制正则化行为。

12.1.4 闵可夫斯基和

数学定义

两个集合 的闵可夫斯基和定义为:

在几何上,这相当于将 的中心放在 的每个点上,然后取并集。

闵可夫斯基和的可视化:

多边形P:              多边形Q:              P ⊕ Q:
    *                    +-------+            +-----------+
   / \                   |       |           /             \
  /   \                  |   *   |          /   +-------+   \
 *-----*                 +-------+         *---*       *---*
                                              \       /
                                               \     /
                                                *---*

闵可夫斯基和的性质:

  1. 交换律
  2. 结合律
  3. 平移不变性
  4. 凸包性质

卷积方法

**卷积(Convolution)**是计算闵可夫斯基和的核心技术。对于两个多边形,其卷积定义为:

卷积的构建过程:

步骤1: 计算边向量
P的边: e1, e2, ..., en
Q的边: f1, f2, ..., fm

步骤2: 按角度排序
将所有边向量按极角排序

步骤3: 构建卷积多边形
按排序后的顺序连接边向量

示例:
P = 三角形 (边方向: 0°, 120°, 240°)
Q = 正方形 (边方向: 0°, 90°, 180°, 270°)

排序后的方向: 0°, 0°, 90°, 120°, 180°, 180°, 240°, 270°

卷积结果: 八边形

简化卷积(Reduced Convolution):

CGAL使用简化卷积方法,只考虑可能出现在闵可夫斯基和边界上的边对,显著提高计算效率。

#include <CGAL/minkowski_sum_2.h>
#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
#include <CGAL/Polygon_2.h>
#include <CGAL/Polygon_with_holes_2.h>
 
typedef CGAL::Exact_predicates_exact_constructions_kernel Kernel;
typedef Kernel::Point_2 Point_2;
typedef CGAL::Polygon_2<Kernel> Polygon_2;
typedef CGAL::Polygon_with_holes_2<Kernel> Polygon_with_holes_2;
 
// 使用简化卷积计算闵可夫斯基和
Polygon_with_holes_2 compute_minkowski_sum(
    const Polygon_2& P, 
    const Polygon_2& Q) {
    return CGAL::minkowski_sum_by_reduced_convolution_2(P, Q);
}
 
// 使用完整卷积计算闵可夫斯基和
Polygon_with_holes_2 compute_minkowski_sum_full(
    const Polygon_2& P, 
    const Polygon_2& Q) {
    return CGAL::minkowski_sum_by_full_convolution_2(P, Q);
}

完整示例:

#include <CGAL/minkowski_sum_2.h>
#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
#include <CGAL/Polygon_2.h>
#include <CGAL/Polygon_with_holes_2.h>
#include <iostream>
 
typedef CGAL::Exact_predicates_exact_constructions_kernel Kernel;
typedef Kernel::Point_2 Point_2;
typedef CGAL::Polygon_2<Kernel> Polygon_2;
typedef CGAL::Polygon_with_holes_2<Kernel> Polygon_with_holes_2;
 
int main() {
    // 构造三角形
    Polygon_2 P;
    P.push_back(Point_2(-1, -1));
    P.push_back(Point_2(1, -1));
    P.push_back(Point_2(0, 1));
    
    // 构造正方形
    Polygon_2 Q;
    Q.push_back(Point_2(3, -1));
    Q.push_back(Point_2(5, -1));
    Q.push_back(Point_2(5, 1));
    Q.push_back(Point_2(3, 1));
    
    // 计算闵可夫斯基和
    Polygon_with_holes_2 sum = CGAL::minkowski_sum_2(P, Q);
    
    std::cout << "闵可夫斯基和的外边界顶点数: " 
              << sum.outer_boundary().size() << std::endl;
    
    return 0;
}

在机器人运动规划中的应用

闵可夫斯基和在机器人运动规划中有核心应用,特别是在**构型空间(Configuration Space, C-space)**方法中。

障碍物扩展原理:

给定机器人 和障碍物 ,机器人在 附近不能到达的区域可以通过闵可夫斯基和计算:

其中 是机器人关于原点的反射。

C-space障碍物构建:

原始场景:                C-space表示:
    
    障碍物O                扩展障碍物O'
    +----+                    +--------+
    |    |                    |        |
    +----+                    |   +--+ |
                              |   |  | |
    机器人R                   |   +--+ |
      *                       |        |
     / \                      +--------+
    *---*

机器人可以到达的位置 = 工作空间 \ O'

路径规划算法框架:

// 构建C-space障碍物
std::vector<Polygon_2> build_cspace_obstacles(
    const Polygon_2& robot,
    const std::vector<Polygon_2>& obstacles) {
    
    std::vector<Polygon_2> cspace_obstacles;
    
    // 反射机器人
    Polygon_2 reflected_robot = reflect_polygon(robot);
    
    // 计算每个障碍物的扩展
    for (const auto& obstacle : obstacles) {
        Polygon_with_holes_2 expanded = 
            CGAL::minkowski_sum_2(obstacle, reflected_robot);
        cspace_obstacles.push_back(expanded.outer_boundary());
    }
    
    return cspace_obstacles;
}

12.1.5 直骨架与屋顶算法

直骨架定义

多边形 的**直骨架(Straight Skeleton)**是通过连续收缩多边形边界形成的骨架结构。与Voronoi图不同,直骨架完全由直线段组成。

直骨架的构建过程:

时间 t=0:              时间 t=1:              时间 t=2:
+----------------+     +------------+         +--------+
|                |     |            |         |        |
|                |     |   +----+   |         |   /\   |
|                | --> |   |    |   |   -->   |  /  \  |
|                |     |   +----+   |         | /    \ |
|                |     |            |         |/      \|
+----------------+     +------------+         +--------+

直骨架: 追踪顶点运动轨迹形成的树状结构

直骨架的数学定义:

直骨架是收缩过程中多边形顶点的角平分线轨迹的并集。每个顶点沿其内角平分线方向以速度 移动,其中 是内角。

直骨架的组成:

  1. 弧(Arc):顶点运动轨迹
  2. 节点(Node)
    • 原始顶点(收缩开始)
    • 边事件(一条边收缩为点)
    • 分裂事件(多边形分裂)
  3. 面(Face):每个原始边对应一个面

屋顶算法

屋顶算法将直骨架解释为三维屋顶结构。想象多边形边界以45度角向上”生长”,直骨架对应屋顶的”屋脊线”。

屋顶结构可视化:

俯视图:                    3D屋顶视图:
                           
+--------+                 \       /
|        |                  \     /
|        |      -->          \   /
|        |                    \ /
+--------+                     V
                              / \
                             /   \

屋顶面的性质:

  • 每个原始边对应一个屋顶面
  • 屋顶面与水平面成45度角
  • 屋顶面的交线对应直骨架的弧
  • 屋顶的高度对应收缩时间

多边形偏移

**多边形偏移(Offset)**是直骨架的直接应用,用于创建多边形的等距内缩或外扩版本。

内偏移(Inset):

原始多边形:           偏移距离d:            偏移结果:
+--------------+     +--------------+     +--------+
|              |     |  +--------+  |     |        |
|              |     |  |        |  |     |        |
|              | --> |  |        |  | --> |        |
|              |     |  |        |  |     |        |
|              |     |  +--------+  |     |        |
+--------------+     +--------------+     +--------+

外偏移(Offset/ outset):

原始多边形:           外偏移结果:
    +--+                 +-------+
    |  |                /         \
    +--+      -->      |    +--+   |
                      |    |  |   |
                       \    +--+  /
                        +-------+

CGAL直骨架实现:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Polygon_2.h>
#include <CGAL/create_straight_skeleton_2.h>
#include <CGAL/create_offset_polygons_2.h>
#include <memory>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
typedef K::Point_2 Point;
typedef CGAL::Polygon_2<K> Polygon_2;
typedef CGAL::Straight_skeleton_2<K> Ss;
typedef std::shared_ptr<Ss> SsPtr;
 
// 创建直骨架
SsPtr create_skeleton(const Polygon_2& poly) {
    return CGAL::create_interior_straight_skeleton_2(
        poly.vertices_begin(), poly.vertices_end());
}
 
// 创建偏移多边形
std::vector<std::shared_ptr<Polygon_2>> create_offset(
    const Polygon_2& poly, double offset_distance) {
    
    SsPtr skeleton = create_skeleton(poly);
    
    return CGAL::create_offset_polygons_2(
        offset_distance, *skeleton);
}

完整示例:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Polygon_2.h>
#include <CGAL/create_straight_skeleton_2.h>
#include <CGAL/draw_straight_skeleton_2.h>
#include <cassert>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
typedef K::Point_2 Point;
typedef CGAL::Polygon_2<K> Polygon_2;
typedef CGAL::Straight_skeleton_2<K> Ss;
typedef std::shared_ptr<Ss> SsPtr;
 
int main() {
    // 构造星形多边形
    Polygon_2 poly;
    poly.push_back(Point(-1, -1));
    poly.push_back(Point(0, -12));
    poly.push_back(Point(1, -1));
    poly.push_back(Point(12, 0));
    poly.push_back(Point(1, 1));
    poly.push_back(Point(0, 12));
    poly.push_back(Point(-1, 1));
    poly.push_back(Point(-12, 0));
    
    assert(poly.is_counterclockwise_oriented());
    
    // 创建内部直骨架
    SsPtr iss = CGAL::create_interior_straight_skeleton_2(
        poly.vertices_begin(), poly.vertices_end());
    
    // 打印直骨架信息
    CGAL::Straight_skeletons_2::IO::print_straight_skeleton(*iss);
    
    // 可视化
    CGAL::draw(*iss);
    
    // 创建外部直骨架(需要指定最大偏移距离)
    double max_offset = 5;
    SsPtr oss = CGAL::create_exterior_straight_skeleton_2(
        max_offset, poly);
    
    CGAL::draw(*oss);
    
    return 0;
}

偏移多边形创建:

#include <CGAL/create_offset_polygons_2.h>
#include <vector>
 
// 创建内偏移多边形
typedef std::shared_ptr<Polygon_2> PolygonPtr;
typedef std::vector<PolygonPtr> PolygonPtrVector;
 
PolygonPtrVector create_inset_polygons(
    const Ss& skeleton, double offset) {
    return CGAL::create_offset_polygons_2(offset, skeleton);
}
 
// 创建外偏移多边形
PolygonPtrVector create_outset_polygons(
    const Polygon_2& poly, double offset) {
    return CGAL::create_exterior_skeleton_and_offset_polygons_2(
        offset, poly);
}

12.1.6 代码实现详解

完整排列操作示例

// SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-Commercial
/*!
 * 排列操作综合示例
 * 演示DCEL遍历、点定位和面遍历
 */
 
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Arr_non_caching_segment_traits_2.h>
#include <CGAL/Arrangement_2.h>
#include <CGAL/Arr_walk_along_line_point_location.h>
#include <iostream>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef CGAL::Arr_non_caching_segment_traits_2<Kernel> Traits;
typedef Traits::Point_2 Point;
typedef Traits::X_monotone_curve_2 Segment;
typedef CGAL::Arrangement_2<Traits> Arrangement;
typedef CGAL::Arr_walk_along_line_point_location<Arrangement> Point_location;
 
// 遍历排列的所有面
void traverse_faces(const Arrangement& arr) {
    std::cout << "=== 面遍历 ===" << std::endl;
    
    for (auto fit = arr.faces_begin(); fit != arr.faces_end(); ++fit) {
        if (fit->is_unbounded()) {
            std::cout << "无界面:" << std::endl;
        } else {
            std::cout << "有界面:" << std::endl;
        }
        
        // 遍历面的外部边界
        if (!fit->is_unbounded()) {
            std::cout << "  外部边界: ";
            auto circ = fit->outer_ccb();
            auto curr = circ;
            do {
                std::cout << "(" << curr->source()->point() << ") ";
            } while (++curr != circ);
            std::cout << std::endl;
        }
        
        // 遍历面的孔洞
        int hole_count = 0;
        for (auto hit = fit->holes_begin(); hit != fit->holes_end(); ++hit) {
            std::cout << "  孔洞 " << ++hole_count << ": ";
            auto circ = *hit;
            auto curr = circ;
            do {
                std::cout << "(" << curr->source()->point() << ") ";
            } while (++curr != circ);
            std::cout << std::endl;
        }
    }
}
 
// 遍历顶点的邻接关系
void traverse_vertex_neighbors(const Arrangement& arr) {
    std::cout << "\n=== 顶点邻接关系 ===" << std::endl;
    
    for (auto vit = arr.vertices_begin(); vit != arr.vertices_end(); ++vit) {
        std::cout << "顶点 (" << vit->point() << "): ";
        
        if (vit->is_isolated()) {
            std::cout << "孤立顶点" << std::endl;
            continue;
        }
        
        std::cout << "度=" << vit->degree() << ", 邻接: ";
        
        // 使用循环器遍历邻接半边
        auto first = vit->incident_halfedges();
        auto curr = first;
        do {
            std::cout << "(" << curr->source()->point() << ") ";
        } while (++curr != first);
        std::cout << std::endl;
    }
}
 
// 点定位查询
void point_location_query(Point_location& pl, const Point& query_point) {
    std::cout << "\n=== 点定位查询 ===" << std::endl;
    std::cout << "查询点: (" << query_point << ")" << std::endl;
    
    auto obj = pl.locate(query_point);
    
    // 判断查询结果类型
    if (auto* v = boost::get<Arrangement::Vertex_const_handle>(&obj)) {
        std::cout << "结果: 位于顶点上 (" << (*v)->point() << ")" << std::endl;
    } else if (auto* e = boost::get<Arrangement::Halfedge_const_handle>(&obj)) {
        std::cout << "结果: 位于边上 [" << (*e)->curve() << "]" << std::endl;
    } else if (auto* f = boost::get<Arrangement::Face_const_handle>(&obj)) {
        if ((*f)->is_unbounded()) {
            std::cout << "结果: 位于无界面内" << std::endl;
        } else {
            std::cout << "结果: 位于有界面内" << std::endl;
        }
    }
}
 
int main() {
    // 构造排列
    Arrangement arr;
    
    // 插入线段形成两个相交的正方形
    Segment s1(Point(0, 0), Point(4, 0));
    Segment s2(Point(4, 0), Point(4, 4));
    Segment s3(Point(4, 4), Point(0, 4));
    Segment s4(Point(0, 4), Point(0, 0));
    Segment s5(Point(2, -1), Point(2, 5));
    Segment s6(Point(-1, 2), Point(5, 2));
    
    CGAL::insert(arr, s1);
    CGAL::insert(arr, s2);
    CGAL::insert(arr, s3);
    CGAL::insert(arr, s4);
    CGAL::insert(arr, s5);
    CGAL::insert(arr, s6);
    
    // 输出排列统计
    std::cout << "排列统计:" << std::endl;
    std::cout << "  顶点数: " << arr.number_of_vertices() << std::endl;
    std::cout << "  边数: " << arr.number_of_edges() << std::endl;
    std::cout << "  面数: " << arr.number_of_faces() << std::endl;
    
    // 验证欧拉公式: V - E + F = 2 (对于连通平面细分)
    int V = arr.number_of_vertices();
    int E = arr.number_of_edges();
    int F = arr.number_of_faces();
    std::cout << "  欧拉公式验证: " << V << " - " << E << " + " << F 
              << " = " << (V - E + F) << std::endl;
    
    // 遍历面
    traverse_faces(arr);
    
    // 遍历顶点邻接关系
    traverse_vertex_neighbors(arr);
    
    // 点定位
    Point_location pl(arr);
    point_location_query(pl, Point(2, 2));  // 面内部
    point_location_query(pl, Point(2, 0));  // 边上
    point_location_query(pl, Point(0, 0));  // 顶点上
    
    return 0;
}

布尔运算综合示例

// SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-Commercial
/*!
 * 布尔运算综合示例
 * 演示并、交、差、对称差运算
 */
 
#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
#include <CGAL/Boolean_set_operations_2.h>
#include <list>
#include <iostream>
 
typedef CGAL::Exact_predicates_exact_constructions_kernel Kernel;
typedef Kernel::Point_2 Point_2;
typedef CGAL::Polygon_2<Kernel> Polygon_2;
typedef CGAL::Polygon_with_holes_2<Kernel> Polygon_with_holes_2;
typedef std::list<Polygon_with_holes_2> Pwh_list_2;
 
// 打印多边形信息
void print_polygon(const Polygon_2& P) {
    std::cout << "多边形顶点数: " << P.size() << std::endl;
    std::cout << "顶点: ";
    for (auto vit = P.vertices_begin(); vit != P.vertices_end(); ++vit) {
        std::cout << "(" << *vit << ") ";
    }
    std::cout << std::endl;
}
 
void print_polygon_with_holes(const Polygon_with_holes_2& pwh) {
    std::cout << "外边界顶点数: " << pwh.outer_boundary().size() << std::endl;
    std::cout << "孔洞数: " << pwh.number_of_holes() << std::endl;
}
 
// 布尔运算演示
void boolean_operations_demo() {
    // 构造多边形P(L形)
    Polygon_2 P;
    P.push_back(Point_2(0, 0));
    P.push_back(Point_2(4, 0));
    P.push_back(Point_2(4, 2));
    P.push_back(Point_2(2, 2));
    P.push_back(Point_2(2, 4));
    P.push_back(Point_2(0, 4));
    
    // 构造多边形Q(矩形)
    Polygon_2 Q;
    Q.push_back(Point_2(1, 1));
    Q.push_back(Point_2(5, 1));
    Q.push_back(Point_2(5, 3));
    Q.push_back(Point_2(1, 3));
    
    std::cout << "=== 输入多边形 ===" << std::endl;
    std::cout << "多边形P:" << std::endl;
    print_polygon(P);
    std::cout << "多边形Q:" << std::endl;
    print_polygon(Q);
    
    // 1. 并运算
    std::cout << "\n=== 并运算 (P ∪ Q) ===" << std::endl;
    Polygon_with_holes_2 union_result;
    if (CGAL::join(P, Q, union_result)) {
        std::cout << "并集结果:" << std::endl;
        print_polygon_with_holes(union_result);
    } else {
        std::cout << "P和Q不相交" << std::endl;
    }
    
    // 2. 交运算
    std::cout << "\n=== 交运算 (P ∩ Q) ===" << std::endl;
    Pwh_list_2 intersection_result;
    CGAL::intersection(P, Q, std::back_inserter(intersection_result));
    std::cout << "交集组件数: " << intersection_result.size() << std::endl;
    int i = 1;
    for (const auto& pwh : intersection_result) {
        std::cout << "  组件 " << i++ << ": ";
        print_polygon_with_holes(pwh);
    }
    
    // 3. 差运算
    std::cout << "\n=== 差运算 (P \\ Q) ===" << std::endl;
    Pwh_list_2 difference_result;
    CGAL::difference(P, Q, std::back_inserter(difference_result));
    std::cout << "差集组件数: " << difference_result.size() << std::endl;
    
    // 4. 对称差
    std::cout << "\n=== 对称差 (P △ Q) ===" << std::endl;
    Pwh_list_2 sym_diff_result;
    CGAL::symmetric_difference(P, Q, std::back_inserter(sym_diff_result));
    std::cout << "对称差组件数: " << sym_diff_result.size() << std::endl;
    
    // 5. 相交测试
    std::cout << "\n=== 相交测试 ===" << std::endl;
    bool intersect = CGAL::do_intersect(P, Q);
    std::cout << "P和Q是否相交: " << (intersect ? "是" : "否") << std::endl;
    
    // 6. 方向测试
    std::cout << "\n=== 方向测试 ===" << std::endl;
    CGAL::Oriented_side side = CGAL::oriented_side(P, Point_2(1, 1));
    std::cout << "点(1,1)相对于P的位置: ";
    switch (side) {
        case CGAL::ON_ORIENTED_BOUNDARY: std::cout << "边界上"; break;
        case CGAL::ON_POSITIVE_SIDE: std::cout << "内部"; break;
        case CGAL::ON_NEGATIVE_SIDE: std::cout << "外部"; break;
    }
    std::cout << std::endl;
}
 
int main() {
    boolean_operations_demo();
    return 0;
}

闵可夫斯基和与路径规划示例

// SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-Commercial
/*!
 * 闵可夫斯基和在机器人运动规划中的应用
 */
 
#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
#include <CGAL/minkowski_sum_2.h>
#include <CGAL/Polygon_2.h>
#include <vector>
#include <iostream>
 
typedef CGAL::Exact_predicates_exact_constructions_kernel Kernel;
typedef Kernel::Point_2 Point_2;
typedef Kernel::Vector_2 Vector_2;
typedef CGAL::Polygon_2<Kernel> Polygon_2;
typedef CGAL::Polygon_with_holes_2<Kernel> Polygon_with_holes_2;
 
// 反射多边形(关于原点)
Polygon_2 reflect_polygon(const Polygon_2& P) {
    Polygon_2 reflected;
    for (auto vit = P.vertices_begin(); vit != P.vertices_end(); ++vit) {
        reflected.push_back(Point_2(-vit->x(), -vit->y()));
    }
    return reflected;
}
 
// 平移多边形
Polygon_2 translate_polygon(const Polygon_2& P, const Vector_2& v) {
    Polygon_2 translated;
    for (auto vit = P.vertices_begin(); vit != P.vertices_end(); ++vit) {
        translated.push_back(*vit + v);
    }
    return translated;
}
 
// 构建C-space障碍物
std::vector<Polygon_2> build_cspace_obstacles(
    const Polygon_2& robot,
    const std::vector<Polygon_2>& obstacles) {
    
    std::vector<Polygon_2> cspace_obstacles;
    Polygon_2 reflected_robot = reflect_polygon(robot);
    
    std::cout << "构建C-space障碍物..." << std::endl;
    std::cout << "机器人顶点数: " << robot.size() << std::endl;
    std::cout << "障碍物数量: " << obstacles.size() << std::endl;
    
    for (size_t i = 0; i < obstacles.size(); ++i) {
        // 计算闵可夫斯基和: obstacle ⊕ (-robot)
        Polygon_with_holes_2 expanded = 
            CGAL::minkowski_sum_2(obstacles[i], reflected_robot);
        
        cspace_obstacles.push_back(expanded.outer_boundary());
        std::cout << "  障碍物 " << i+1 << " 扩展后顶点数: " 
                  << expanded.outer_boundary().size() << std::endl;
    }
    
    return cspace_obstacles;
}
 
// 简单的碰撞检测(点在多边形内测试)
bool is_collision(const Point_2& point, const std::vector<Polygon_2>& obstacles) {
    for (const auto& obs : obstacles) {
        if (obs.has_on_bounded_side(point) || obs.has_on_boundary(point)) {
            return true;
        }
    }
    return false;
}
 
int main() {
    // 定义三角形机器人
    Polygon_2 robot;
    robot.push_back(Point_2(0, 0.5));
    robot.push_back(Point_2(-0.5, -0.5));
    robot.push_back(Point_2(0.5, -0.5));
    
    // 定义障碍物
    std::vector<Polygon_2> obstacles;
    
    // 障碍物1: 正方形
    Polygon_2 obs1;
    obs1.push_back(Point_2(2, 2));
    obs1.push_back(Point_2(4, 2));
    obs1.push_back(Point_2(4, 4));
    obs1.push_back(Point_2(2, 4));
    obstacles.push_back(obs1);
    
    // 障碍物2: 三角形
    Polygon_2 obs2;
    obs2.push_back(Point_2(6, 1));
    obs2.push_back(Point_2(8, 2));
    obs2.push_back(Point_2(7, 4));
    obstacles.push_back(obs2);
    
    // 构建C-space障碍物
    std::vector<Polygon_2> cspace_obs = build_cspace_obstacles(robot, obstacles);
    
    // 测试路径规划
    Point_2 start(0, 0);
    Point_2 goal(10, 5);
    
    std::cout << "\n路径规划测试:" << std::endl;
    std::cout << "起点 (" << start << ") 碰撞检测: " 
              << (is_collision(start, cspace_obs) ? "碰撞" : "自由") << std::endl;
    std::cout << "终点 (" << goal << ") 碰撞检测: " 
              << (is_collision(goal, cspace_obs) ? "碰撞" : "自由") << std::endl;
    
    // 测试路径上的点
    std::cout << "\n路径采样测试:" << std::endl;
    for (int i = 0; i <= 10; ++i) {
        double t = i / 10.0;
        Point_2 p(start.x() + t * (goal.x() - start.x()),
                  start.y() + t * (goal.y() - start.y()));
        bool collision = is_collision(p, cspace_obs);
        std::cout << "  点 (" << p << "): " 
                  << (collision ? "碰撞" : "自由") << std::endl;
    }
    
    return 0;
}

12.1.7 应用场景

计算机辅助设计(CAD)

布尔运算在CAD中的应用:

实体建模流程:

基本体素          布尔运算           复杂模型
+-------+        +-------+         +-------+
|       |   ∪    |  +--+ |         | +--+  |
|   +   |   -->  |  |  | |   -->   | |  |  |
|       |        |  +--+ |         | +--+  |
+-------+        +-------+         +-------+

特征操作:
- 拉伸 (Extrusion)
- 旋转 (Revolution)
- 扫掠 (Sweep)
- 放样 (Loft)

地理信息系统(GIS)

空间分析操作:

地图叠加分析:

土地利用图层:      土壤类型图层:      叠加结果:
+---+---+---+      +---+---+---+      +---+---+---+
| A | A | B |      | 1 | 2 | 2 |      |A1 |A2 |B2 |
+---+---+---+  ∩   +---+---+---+  =   +---+---+---+
| A | C | C |      | 1 | 1 | 3 |      |A1 |C1 |C3 |
+---+---+---+      +---+---+---+      +---+---+---+

A: 农业用地  B: 林地  C: 城市
1: 肥沃土壤  2: 沙质土壤  3: 粘土

机器人运动规划

构型空间方法:

工作空间:                      C-space:
    
    障碍物                      扩展障碍物
    +----+                      +--------+
    |    |                      |        |
    +----+                      |   +--+ |
                                |   |  | |
    机器人                      |   +--+ |
      *                         |        |
     / \                        +--------+
    *---*
    
路径规划 = 在C-space中寻找从起点到终点的无障碍路径

计算机图形学

多边形裁剪与CSG建模:

CSG树结构:

       ∪
      / \
     ∩   圆柱
    / \
   立方体  球

表示: (立方体 ∩ 球) ∪ 圆柱

12.1.8 本章小结

本章深入探讨了排列与布尔运算的核心概念和算法:

核心数据结构:

  • DCEL(双向连接边表):平面细分的标准表示,通过顶点、半边和面维护拓扑关系
  • 组合嵌入:描述平面图的拓扑结构,与几何形状无关
  • 欧拉公式 是验证平面细分有效性的重要工具

核心算法:

  • Bentley-Ottmann扫描线算法 时间复杂度计算线段交点
  • 事件驱动处理:站点事件和交点事件的正确处理是算法正确性的关键
  • 简化卷积:高效计算闵可夫斯基和的核心技术

布尔运算:

  • 支持并、交、差、对称差等标准集合运算
  • Nef多面体提供通用的集合表示
  • 正则化运算消除低维特征,产生符合物理直觉的结果

高级应用:

  • 闵可夫斯基和:机器人运动规划中C-space障碍物的构建
  • 直骨架:多边形偏移、屋顶设计、形状分析

数值稳健性:

在使用这些算法时,数值稳健性是重要考虑因素:

  1. 精确几何计算:CGAL的Exact_predicates_exact_constructions_kernel提供精确计算,避免浮点误差
  2. 退化情况处理:共线点、重合边等退化情况需要特殊处理
  3. 计算复杂度:理解算法的时间复杂度,选择合适的算法变体

复杂度总结表:

算法/操作时间复杂度空间复杂度
Bentley-Ottmann
布尔运算
闵可夫斯基和(简化卷积)
直骨架构建
点定位(最坏情况)
点定位(期望)

其中 是输入规模, 是交点数, 是直骨架节点数。

12.1.9 练习

理论练习

练习12.1:DCEL验证 给定一个DCEL表示的平面细分,编写算法验证其满足欧拉公式。考虑如何处理多个连通分量的情况?

练习12.2:扫描线算法分析 分析Bentley-Ottmann算法中事件队列的最大长度。证明其空间复杂度为

练习12.3:闵可夫斯基和性质 证明闵可夫斯基和的以下性质:

  1. 交换律:
  2. 平移不变性:
  3. 凸包性质:

练习12.4:直骨架与Voronoi图 比较直骨架和Voronoi图的异同。在什么情况下两者会重合?

编程练习

练习12.5:排列遍历 实现一个函数,遍历排列的所有有界面,并计算每个面的面积。使用CGAL的Arrangement_2类。

练习12.6:布尔运算序列 实现一个函数,接受多边形列表和布尔运算序列(如”A ∪ B ∩ C \ D”),计算最终结果。考虑运算符优先级。

练习12.7:机器人路径规划 扩展12.1.6节的示例,实现A*算法在C-space中搜索从起点到终点的最短路径。

练习12.8:多边形偏移 实现一个函数,给定多边形和偏移距离序列,生成一系列向内偏移的多边形,直到多边形消失。

应用练习

练习12.9:地图叠加 给定两个多边形图层(如行政区划和土地利用),实现空间叠加分析,计算每个叠加区域的属性组合。

练习12.10:屋顶设计 给定建筑物 footprint(多边形),使用直骨架生成不同风格的屋顶设计(如四坡顶、人字顶等)。


参考文献:

  1. de Berg, M., Cheong, O., van Kreveld, M., & Overmars, M. (2008). Computational Geometry: Algorithms and Applications (3rd ed.). Springer.

  2. CGAL Project. (2024). CGAL User and Reference Manual (6.0.1 ed.). https://doc.cgal.org/

  3. Bentley, J. L., & Ottmann, T. A. (1979). Algorithms for reporting and counting geometric intersections. IEEE Transactions on Computers, 28(9), 643-647.

  4. Aichholzer, O., Aurenhammer, F., Alberts, D., & Gärtner, B. (1995). A novel type of skeleton for polygons. Journal of Universal Computer Science, 1(12), 752-761.

  5. Fogel, E., Halperin, D., Wein, R., & Zukerman, B. (2012). 2D Arrangements. In CGAL User and Reference Manual.