7.2 二维Delaunay三角化 (Delaunay_triangulation_2)

上一节: 7.1 二维三角化基础 下一节: 7.3 约束三角化 相关章节: 11.1 Voronoi图 (Delaunay对偶关系)

0. 直观理解:为什么需要Delaunay三角化?

生活类比:气泡堆积

想象你在吹一堆气泡,每个气泡的中心是一个数据点。当气泡相互挤压时,它们之间的接触线就形成了Delaunay三角化的边。这就是为什么Delaunay三角化被称为”对偶”于Voronoi图的原因——就像气泡的外壁和内壁的关系。

气泡堆积示意图:

    O       O           O       O
     \     /             \     /
      \   /               \   /
       \ /                 \ /
    ---O---      vs     ---O---
       / \                 /|\
      /   \               / | \
     /     \             /  |  \
    O       O           O   |   O
                           / \
                          /   \
                         O     O

不规则三角化              Delaunay三角化
(细长三角形多)            (三角形更规则)

为什么Delaunay三角化很重要?

问题场景:假设你正在构建一个地形模型,需要将地形采样点连接成三角网格。

  • 不好的三角化:产生大量细长三角形,导致地形表面出现不合理的陡坡
  • Delaunay三角化:最大化最小角,避免细长三角形,地形更平滑

核心优势

  1. 最优三角形质量:在所有可能的三角化中,Delaunay三角化最大化最小内角
  2. 最近邻关系:自动建立点集的自然邻接关系
  3. 对偶Voronoi图:同时获得Voronoi划分,用于空间分析

1. 理论基础

1.1 Delaunay三角化的数学定义

Delaunay三角化是计算几何中最优的三角化方式,由俄罗斯数学家Boris Delaunay于1934年提出。

形式化定义:给定平面点集 ,Delaunay三角化 是满足以下条件的三角化:

空外接圆性质(Empty Circumcircle Property):对于 中的每个三角形,其外接圆内部不包含任何其他输入点。

数学表达:对于 中的任意三角形 ,满足:

存在性与唯一性定理

  • 存在性:对于任意有限点集 ,Delaunay三角化存在
  • 唯一性:如果点集处于一般位置(general position),即不存在四点共圆,则Delaunay三角化唯一

对于非一般位置的点集(存在共圆点),Delaunay三角化不唯一,但Delaunay图(Delaunay graph)唯一。

1.2 空外接圆性质详解

空外接圆性质是Delaunay三角化的核心判定标准。让我们通过可视化来理解:

Delaunay三角形的外接圆:

        p_i
       / \\
      /   \\
     /  O  \\     O = 外接圆心(到三个顶点等距)
    /   |   \\    圆内无其他点
   /    |    \\
  p_j---+----p_k
      半径R

非Delaunay情况:

        p_i
       / \\
      /   \\ p_l  ← 点在外接圆内!
     /  O  /\\
    /   | /  \\
   /    |/    \\
  p_j---+----p_k

违反空外接圆性质,需要边翻转:将边(p_j, p_k)翻转为边(p_i, p_l)

几何解释:空外接圆性质确保三角形尽可能”均匀”,避免细长三角形。直观上,如果某点落在三角形的外接圆内,将该点与三角形顶点连接通常会产生更规则的三角形。

1.3 最大化最小角性质

Delaunay三角化具有最优的三角形质量:

定理(Delaunay, 1934):在所有可能的三角化中,Delaunay三角化最大化最小内角

角度比较示例:

非Delaunay三角化:           Delaunay三角化:
    p_3                        p_3
    /\\                        /|\\
   /  \\                      / | \\
  /    \\                    /  |  \\
 /  5°   \\                  /   |   \\
p_1------p_2               p_1---+----p_2
\\        /                \\  |  /
 \\      /                  \\ | /
  \\    /                    \\|/
   \\  /                      p_4
    p_4

最小角 = 5°                 最小角 = 30°
(细长三角形)              (三角形更规则)

应用意义

  • 有限元分析:避免细长三角形导致的数值不稳定
  • 地形建模:防止不合理的陡坡和地形扭曲
  • 计算机图形学:更均匀的网格带来更好的渲染效果
  • 插值计算:规则三角形的插值误差更小

1.4 对偶Voronoi图关系

Delaunay三角化与Voronoi图互为对偶图(Dual Graph)

Voronoi图定义:对于点集 ,点 的Voronoi单元为:

对偶关系

Delaunay三角化Voronoi图
顶点(输入点)Voronoi单元
Delaunay边Voronoi边(单元边界)
三角形外接圆心Voronoi顶点
三角形Voronoi顶点关联的区域
对偶关系可视化:

Delaunay三角化(实线) + Voronoi图(虚线):

        p_1
       /|\ 
      / | \\        p_1, p_2, p_3, p_4 是Delaunay顶点
     /  |  \\       v_1, v_2 是Voronoi顶点(外接圆心)
    /   v_1--...    
   /   /|\\         实线 = Delaunay边
  p_4-/-|-\\-p_2    虚线 = Voronoi边
    \\/  v_2 \\\        
     \\  |  /       v_1 是三角形(p_1,p_2,p_4)的外接圆心
      \\ | /        v_2 是三角形(p_2,p_3,p_4)的外接圆心
       \\|/         
        p_3         

对偶性质:
- 每条Delaunay边垂直于对应的Voronoi边
- Delaunay边连接的两个点是相邻Voronoi单元的生成点

应用意义

  1. 最近邻查询:Voronoi图天然支持最近邻搜索
  2. 设施选址:Voronoi单元表示最近服务区域
  3. 自然邻域插值:基于Voronoi单元的权重插值
  4. 形态分析:骨架提取、形状描述

1.5 退化情况处理

当点集不满足一般位置假设时,需要特殊处理:

共圆点(Cocircular Points):多个点位于同一圆上

共圆点情况:

    p_1 -------- p_2
    /              \\
   /    O 圆心     \\
  /                \\
 p_4 ------------ p_3

所有四点共圆,Delaunay三角化不唯一:
方案1: 三角形(p_1,p_2,p_3) + 三角形(p_1,p_3,p_4)
方案2: 三角形(p_1,p_2,p_4) + 三角形(p_2,p_3,p_4)

两种方案都满足空外接圆性质!

CGAL使用**符号扰动(Symbolic Perturbation)**技术处理退化:

  • 通过模拟扰动打破平局
  • 确保结果的一致性和唯一性
  • 保持拓扑正确性

共线点(Collinear Points):所有点位于同一直线上

共线点情况:

p_1 -- p_2 -- p_3 -- p_4 -- p_5

退化为1维:没有有限三角形
Delaunay三角化仅包含连接相邻点的边
CGAL使用"无限远顶点"技术统一处理

2. 算法详解

2.1 增量插入算法(Incremental Insertion)

CGAL采用增量算法构建Delaunay三角化,逐个插入点并维护Delaunay性质。

算法框架

// 伪代码:增量Delaunay三角化
Delaunay_triangulation_2 build_delaunay(Point_set P) {
    // 1. 初始化:创建包含所有点的超大三角形
    Triangle super_triangle = create_super_triangle(P);
    Delaunay_triangulation_2 dt(super_triangle);
    
    // 2. 随机打乱点序(避免最坏情况)
    random_shuffle(P.begin(), P.end());
    
    // 3. 逐个插入点
    for (Point p : P) {
        insert_point(dt, p);
    }
    
    // 4. 移除超三角形相关顶点
    remove_super_triangle(dt, super_triangle);
    
    return dt;
}

单点插入过程

void insert_point(Delaunay_triangulation_2& dt, Point p) {
    // 步骤1: 定位 - 找到包含点p的三角形
    Face_handle f = dt.locate(p);
    
    // 步骤2: 分裂 - 将三角形分裂为3个
    if (p在f内部) {
        split_triangle(dt, f, p);  // 1个三角形 -> 3个三角形
    } else if (p在f的边上) {
        split_edge(dt, f, p);      // 2个三角形 -> 4个三角形
    } else {
        // p与现有点重合,跳过或更新
        return;
    }
    
    // 步骤3: 恢复Delaunay性质 - 边翻转
    restore_delaunay(dt, p);
}

2.2 Bowyer-Watson算法步骤

Bowyer-Watson算法是增量插入的经典实现,使用**冲突区域(Conflict Region)**概念:

Bowyer-Watson算法可视化:

步骤1: 插入新点p,找到包含它的三角形

        a
       /\\
      /  \\
     /    \\      新点p在三角形(a,b,c)内
    /  p   \\
   /   O    \\
  /__________\\
 b            c

步骤2: 找到所有外接圆包含p的三角形(冲突区域)

        a
       /\\
      /  \\
     / T1 \\      T1, T2, T3的外接圆都包含p
    /  p   \\
   / O------O \\
  / T2      T3 \\
 b____________c

冲突区域 = {T1, T2, T3} 形成"星形多边形"

步骤3: 删除冲突区域内的所有三角形

        a
       / \\
      /   \\
     /  p   \\
    /   O    \\
   /  /   \\   \\
  / /       \\ \\
 b/___________\\c

步骤4: 将p与边界连接,创建新三角形

        a
       /|\\
      / | \\
     /  |  \\
    / T1| T2\\
   /____p____ \\
  / T3  |  T4 \\
 /______|______\\
b               c

新三角形: (p,a,b), (p,b,c), (p,c,a) 等

算法复杂度分析

  • 定位步骤 使用历史DAG或 使用行走法
  • 冲突检测,d为冲突区域大小(平均常数)
  • 边翻转
  • 总体平均 每点,总体

2.3 边翻转(Edge Flip)机制

边翻转是维护Delaunay性质的核心操作。

可翻转条件:两个相邻三角形形成凸四边形

边翻转操作:

翻转前(非Delaunay):          翻转后(Delaunay):

      a                             a
     /\\                            /|\\
    /  \\                          / | \\
   / T1 \\                        /  |  \\
  /      \\                      / T1| T2 \\
 b--------c        -->          b---p---c
  \\      /                      \\  |  /
   \\ T2 /                        \\ | /
    \\  /                          \\|/
     \\/                            p
      p

边(b,c)违反Delaunay性质,      翻转为边(a,p)
因为p在三角形(a,b,c)的外接圆内   现在满足空外接圆性质

边翻转判定

// 检查边是否需要翻转
bool needs_flip(Face_handle f, int i) {
    // f是面,i是边索引
    Face_handle n = f->neighbor(i);  // 邻接面
    if (is_infinite(n)) return false;  // 边界边不翻转
    
    Vertex_handle v_op = n->vertex(n->index(f));  // 对角顶点
    
    // 核心测试:对角顶点是否在外接圆内
    // side_of_oriented_circle返回:
    // ON_POSITIVE_SIDE: 在圆外(满足Delaunay,不翻转)
    // ON_NEGATIVE_SIDE: 在圆内(违反Delaunay,需要翻转)
    // ON_ORIENTED_BOUNDARY: 在圆上(退化情况)
    return side_of_oriented_circle(f, v_op->point()) == ON_NEGATIVE_SIDE;
}
 
// 执行边翻转
void flip(Face_handle f, int i) {
    Face_handle n = f->neighbor(i);
    int j = n->index(f);  // f在n中的索引
    
    // 重新连接四个顶点
    // 翻转前: f = (a,b,c), n = (b,p,c) 共享边(b,c)
    // 翻转后: f = (a,b,p), n = (a,p,c) 共享边(a,p)
    
    // 更新顶点关联关系
    // 更新邻接面指针
    // 维护组合数据结构的一致性
}

边翻转算法流程

void restore_delaunay(Vertex_handle v) {
    // 收集所有与新顶点v关联的边
    std::stack<Edge> edges_to_check;
    Face_circulator fc = incident_faces(v), done(fc);
    
    do {
        int i = fc->index(v);
        edges_to_check.push(Edge(fc, i));
    } while (++fc \!= done);
    
    // 边翻转循环
    while (\!edges_to_check.empty()) {
        Edge e = edges_to_check.top();
        edges_to_check.pop();
        
        Face_handle f = e.first;
        int i = e.second;
        
        if (needs_flip(f, i)) {
            Face_handle n = f->neighbor(i);
            
            // 执行翻转
            flip(f, i);
            
            // 将新产生的边加入检查队列
            // 翻转产生两条新边,需要检查它们是否满足Delaunay
            int ni = n->index(f);
            edges_to_check.push(Edge(f, (i+1)%3));
            edges_to_check.push(Edge(n, (ni+1)%3));
        }
    }
}

2.4 时间复杂度分析

操作平均复杂度最坏复杂度说明
构造(n个点)随机增量算法
插入单个点期望复杂度,使用历史DAG
删除顶点需要重建局部区域
点定位walking策略
边翻转局部操作
最近邻查询利用Delaunay性质
Voronoi顶点计算外接圆心计算

复杂度对比

不同三角化算法复杂度比较:

算法                    构造复杂度        空间复杂度
--------------------------------------------------------
暴力算法                 O(n^3)           O(n)
扫描线算法               O(n log n)       O(n)
分治算法                 O(n log n)       O(n)
增量插入(无随机)         O(n^2)           O(n)
增量插入(随机)           O(n log n)       O(n)  <- CGAL默认
Delaunay层次结构         O(n log n)       O(n)  <- 最快,常数小

3. 实现细节

3.1 CGAL的Delaunay_triangulation_2类

类定义

namespace CGAL {
 
template <class Traits_, class Tds_ = Default>
class Delaunay_triangulation_2 
    : public Triangulation_2<Traits_, Tds_> {
public:
    // 类型定义
    typedef Traits_                                     Geom_traits;
    typedef Tds_                                        Triangulation_data_structure;
    typedef typename Traits_::Point_2                   Point;
    typedef typename Traits_::Segment_2                 Segment;
    typedef typename Traits_::Triangle_2                Triangle;
    
    // 句柄和迭代器(继承自Triangulation_2)
    typedef typename Tds_::Vertex_handle                Vertex_handle;
    typedef typename Tds_::Face_handle                  Face_handle;
    typedef typename Tds_::Edge                         Edge;
    typedef typename Tds_::Vertex_iterator              Vertex_iterator;
    typedef typename Tds_::Face_iterator                Face_iterator;
    typedef typename Tds_::Edge_iterator                Edge_iterator;
    typedef typename Tds_::Face_circulator              Face_circulator;
    typedef typename Tds_::Vertex_circulator            Vertex_circulator;
    
    // 构造与析构
    Delaunay_triangulation_2(const Geom_traits& traits = Geom_traits());
    Delaunay_triangulation_2(InputIterator first, InputIterator last);
    
    // 插入操作
    Vertex_handle insert(const Point& p, Face_handle hint = Face_handle());
    Vertex_handle insert(const Point& p, Locate_type lt, Face_handle loc, int li);
    int insert(InputIterator first, InputIterator last);
    
    // 删除操作
    void remove(Vertex_handle v);
    void remove(InputIterator first, InputIterator last);
    
    // 点定位
    Face_handle locate(const Point& p, Face_handle hint = Face_handle()) const;
    Face_handle locate(const Point& p, Locate_type& lt, int& li, 
                      Face_handle hint = Face_handle()) const;
    
    // Delaunay特定查询
    Vertex_handle nearest_vertex(const Point& p, Face_handle hint = Face_handle()) const;
    bool is_delaunay(Face_handle f) const;
    
    // Voronoi图相关
    Point dual(Face_handle f) const;  // Voronoi顶点(外接圆心)
    Object dual(Edge e) const;        // Voronoi边
    Object dual(Edge_circulator ec) const;
    Object dual(Edge_iterator ei) const;
    
    // 几何谓词
    Oriented_side side_of_oriented_circle(Face_handle f, const Point& p) const;
    
    // 验证
    bool is_valid(bool verbose = false, int level = 0) const;
};
 
} // namespace CGAL

继承层次

Triangulation_2<Traits, Tds>           // 基类:通用三角化
    └── Delaunay_triangulation_2       // Delaunay特化
            └── Regular_triangulation_2 // 加权Delaunay推广

3.2 顶点/面/边的遍历

基本遍历模式

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Delaunay_triangulation_2.h>
#include <iostream>
 
using K = CGAL::Exact_predicates_inexact_constructions_kernel;
using DT = CGAL::Delaunay_triangulation_2<K>;
 
using Point = K::Point_2;
using Vertex_handle = DT::Vertex_handle;
using Face_handle = DT::Face_handle;
using Edge = DT::Edge;
 
void traversal_examples(DT& dt) {
    // ========== 顶点遍历 ==========
    std::cout << "=== 所有顶点 ===" << std::endl;
    for (auto vit = dt.vertices_begin(); vit \!= dt.vertices_end(); ++vit) {
        Point p = vit->point();
        std::cout << "顶点: (" << p.x() << ", " << p.y() << ")" << std::endl;
    }
    
    // 仅有限顶点(不包括无限远顶点)
    for (auto vit = dt.finite_vertices_begin(); 
         vit \!= dt.finite_vertices_end(); ++vit) {
        // 处理有限顶点
    }
    
    // ========== 面遍历 ==========
    std::cout << "\n=== 所有面 ===" << std::endl;
    for (auto fit = dt.faces_begin(); fit \!= dt.faces_end(); ++fit) {
        std::cout << "三角形: ";
        for (int i = 0; i < 3; ++i) {
            Point p = fit->vertex(i)->point();
            std::cout << "(" << p.x() << ", " << p.y() << ") ";
        }
        std::cout << std::endl;
    }
    
    // 仅有限面(不包括与无限远顶点关联的面)
    for (auto fit = dt.finite_faces_begin(); 
         fit \!= dt.finite_faces_end(); ++fit) {
        // 处理有限三角形
    }
    
    // ========== 边遍历 ==========
    std::cout << "\n=== 所有边 ===" << std::endl;
    for (auto eit = dt.edges_begin(); eit \!= dt.edges_end(); ++eit) {
        Edge e = *eit;  // Edge = pair<Face_handle, int>
        Segment seg = dt.segment(e);
        std::cout << "边: (" << seg.source().x() << ", " << seg.source().y() 
                  << ") -> (" << seg.target().x() << ", " << seg.target().y() 
                  << ")" << std::endl;
    }
    
    // 仅有限边
    for (auto eit = dt.finite_edges_begin(); 
         eit \!= dt.finite_edges_end(); ++eit) {
        // 处理有限边
    }
}

循环器(Circulators)遍历邻域

void circulator_examples(DT& dt, Vertex_handle v) {
    // ========== 绕顶点的面循环器 ==========
    std::cout << "=== 顶点 " << v->point() << " 的关联面 ===" << std::endl;
    
    DT::Face_circulator fc = dt.incident_faces(v), done(fc);
    if (fc \!= nullptr) {  // 检查顶点是否关联面
        do {
            if (\!dt.is_infinite(fc)) {  // 跳过无限面
                std::cout << "三角形: ";
                for (int i = 0; i < 3; ++i) {
                    Point p = fc->vertex(i)->point();
                    std::cout << "(" << p.x() << ", " << p.y() << ") ";
                }
                std::cout << std::endl;
            }
            ++fc;
        } while (fc \!= done);
    }
    
    // ========== 绕顶点的顶点循环器 ==========
    std::cout << "\n=== 顶点 " << v->point() << " 的邻接顶点 ===" << std::endl;
    
    DT::Vertex_circulator vc = dt.incident_vertices(v), vdone(vc);
    if (vc \!= nullptr) {
        do {
            if (\!dt.is_infinite(vc)) {  // 跳过无限远顶点
                Point p = vc->point();
                std::cout << "邻接顶点: (" << p.x() << ", " << p.y() << ")" << std::endl;
            }
            ++vc;
        } while (vc \!= vdone);
    }
    
    // ========== 绕边的顶点循环器 ==========
    Edge e = ...;  // 某条边
    DT::Vertex_circulator ec = dt.incident_vertices(e);
    // 遍历与边e关联的所有顶点(即共享该边的所有三角形)
}

3.3 邻接关系查询

面-顶点关系

void face_vertex_relationships(DT& dt) {
    // 获取面的三个顶点
    Face_handle f = ...;
    Vertex_handle v0 = f->vertex(0);
    Vertex_handle v1 = f->vertex(1);
    Vertex_handle v2 = f->vertex(2);
    
    // 获取顶点在面中的索引
    int idx = f->index(v0);  // 返回0
    
    // 判断顶点是否属于面
    bool has_vertex = f->has_vertex(v0);  // true
    
    // 获取面的对边顶点
    Vertex_handle opposite = f->vertex(f->cw(0));   // 顺时针下一个
    Vertex_handle opposite2 = f->vertex(f->ccw(0)); // 逆时针下一个
}

面-面邻接关系

void face_adjacency(DT& dt) {
    Face_handle f = ...;
    
    // 获取三个邻接面
    Face_handle n0 = f->neighbor(0);  // 对边0的邻接面
    Face_handle n1 = f->neighbor(1);  // 对边1的邻接面
    Face_handle n2 = f->neighbor(2);  // 对边2的邻接面
    
    // 获取邻接面在本面中的索引
    int idx_in_neighbor = f->index(n0);  // n0在f中的索引
    
    // 获取本面在邻接面中的索引
    int idx_in_f = n0->index(f);  // f在n0中的索引
    
    // 遍历所有邻接面
    std::cout << "面 " << f << " 的邻接面:" << std::endl;
    for (int i = 0; i < 3; ++i) {
        Face_handle ni = f->neighbor(i);
        if (\!dt.is_infinite(ni)) {
            std::cout << "  边 " << i << " 的邻接面: " << ni << std::endl;
        } else {
            std::cout << "  边 " << i << " 是边界边(邻接无限面)" << std::endl;
        }
    }
}

高级邻接查询

#include <vector>
#include <set>
 
// 获取顶点的所有邻接顶点
std::vector<Vertex_handle> get_adjacent_vertices(DT& dt, Vertex_handle v) {
    std::vector<Vertex_handle> neighbors;
    
    DT::Vertex_circulator vc = dt.incident_vertices(v), done(vc);
    if (vc \!= nullptr) {
        do {
            if (\!dt.is_infinite(vc)) {
                neighbors.push_back(vc);
            }
            ++vc;
        } while (vc \!= done);
    }
    
    return neighbors;
}
 
// 获取面的所有邻接面
std::vector<Face_handle> get_adjacent_faces(DT& dt, Face_handle f) {
    std::vector<Face_handle> neighbors;
    
    for (int i = 0; i < 3; ++i) {
        Face_handle ni = f->neighbor(i);
        if (\!dt.is_infinite(ni)) {
            neighbors.push_back(ni);
        }
    }
    
    return neighbors;
}
 
// 查找两个顶点的公共邻接顶点
std::vector<Vertex_handle> get_common_neighbors(DT& dt, Vertex_handle v1, Vertex_handle v2) {
    std::set<Vertex_handle> n1(get_adjacent_vertices(dt, v1).begin(), 
                               get_adjacent_vertices(dt, v1).end());
    std::set<Vertex_handle> n2(get_adjacent_vertices(dt, v2).begin(), 
                               get_adjacent_vertices(dt, v2).end());
    
    std::vector<Vertex_handle> common;
    std::set_intersection(n1.begin(), n1.end(), n2.begin(), n2.end(), 
                         std::back_inserter(common));
    
    return common;
}

3.4 几何谓词(Predicate)

几何谓词是计算几何算法的基础,CGAL使用精确算术保证鲁棒性。

核心谓词

// 1. 方向测试(Orientation Test)
// 判断三个点的方向:顺时针、逆时针或共线
Orientation orientation(const Point& p, const Point& q, const Point& r);
// 返回值: LEFT_TURN (逆时针), RIGHT_TURN (顺时针), COLLINEAR (共线)
 
// 2. 有向圆测试(Side of Oriented Circle)
// 判断点d相对于(a,b,c)的有向外接圆的位置
Oriented_side side_of_oriented_circle(const Point& a, const Point& b, 
                                       const Point& c, const Point& d);
// 返回值: ON_POSITIVE_SIDE (圆外), ON_NEGATIVE_SIDE (圆内), 
//         ON_ORIENTED_BOUNDARY (圆上)
 
// 3. 共线点上的顺序
// 判断共线点r是否在线段(p,q)上
bool collinear_are_ordered_along_line(const Point& p, const Point& r, 
                                       const Point& q);
 
// 4. 距离比较
Comparison_result compare_distance(const Point& p, const Point& q, 
                                   const Point& r);
// 比较 |p-q| 和 |p-r| 的大小

谓词使用示例

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <iostream>
 
using K = CGAL::Exact_predicates_inexact_constructions_kernel;
using Point = K::Point_2;
 
void predicate_examples() {
    Point a(0, 0), b(1, 0), c(0, 1), d(0.5, 0.5);
    
    // 方向测试
    K::Orientation_2 orientation;
    CGAL::Orientation orient = orientation(a, b, c);
    std::cout << "orientation(a,b,c) = " 
              << (orient == CGAL::LEFT_TURN ? "LEFT_TURN" :
                  orient == CGAL::RIGHT_TURN ? "RIGHT_TURN" : "COLLINEAR")
              << std::endl;
    
    // 圆测试
    K::Side_of_oriented_circle_2 circle_test;
    CGAL::Oriented_side side = circle_test(a, b, c, d);
    std::cout << "side_of_oriented_circle(a,b,c,d) = "
              << (side == CGAL::ON_POSITIVE_SIDE ? "OUTSIDE" :
                  side == CGAL::ON_NEGATIVE_SIDE ? "INSIDE" : "ON_CIRCLE")
              << std::endl;
    
    // 距离比较
    K::Compare_distance_2 compare_dist;
    CGAL::Comparison_result cmp = compare_dist(a, b, c);
    std::cout << "compare_distance(a,b,c) = "
              << (cmp == CGAL::SMALLER ? "|ab| < |ac|" :
                  cmp == CGAL::LARGER ? "|ab| > |ac|" : "|ab| = |ac|")
              << std::endl;
}

构造器(Constructions)

// 1. 外接圆心计算
K::Construct_circumcenter_2 circumcenter;
Point cc = circumcenter(a, b, c);  // 三角形(a,b,c)的外接圆心
 
// 2. 中点计算
K::Construct_midpoint_2 midpoint;
Point mid = midpoint(a, b);
 
// 3. 线段构造
K::Construct_segment_2 segment;
Segment s = segment(a, b);
 
// 4. 三角形构造
K::Construct_triangle_2 triangle;
Triangle t = triangle(a, b, c);

4. 使用示例

4.1 基本使用示例

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Delaunay_triangulation_2.h>
#include <iostream>
#include <vector>
#include <random>
 
using K = CGAL::Exact_predicates_inexact_constructions_kernel;
using DT = CGAL::Delaunay_triangulation_2<K>;
 
using Point = K::Point_2;
using Vertex_handle = DT::Vertex_handle;
using Face_handle = DT::Face_handle;
 
int main() {
    // 创建Delaunay三角化对象
    DT dt;
    
    std::cout << "=== 基本Delaunay三角化示例 ===" << std::endl;
    
    // ========== 插入点 ==========
    std::cout << "\n--- 插入点 ---" << std::endl;
    
    // 方法1: 逐个插入
    Vertex_handle v1 = dt.insert(Point(0, 0));
    Vertex_handle v2 = dt.insert(Point(1, 0));
    Vertex_handle v3 = dt.insert(Point(0.5, 1));
    
    std::cout << "逐个插入3个点" << std::endl;
    std::cout << "顶点数: " << dt.number_of_vertices() << std::endl;
    std::cout << "面数: " << dt.number_of_faces() << std::endl;
    
    // 方法2: 批量插入(更高效)
    std::vector<Point> points;
    std::mt19937 gen(42);
    std::uniform_real_distribution<> dis(0.0, 1.0);
    
    for (int i = 0; i < 100; ++i) {
        points.push_back(Point(dis(gen), dis(gen)));
    }
    
    dt.insert(points.begin(), points.end());
    
    std::cout << "\n批量插入100个随机点" << std::endl;
    std::cout << "总顶点数: " << dt.number_of_vertices() << std::endl;
    std::cout << "总面数: " << dt.number_of_faces() << std::endl;
    std::cout << "边数: " << dt.number_of_edges() << std::endl;
    
    // ========== 基本查询 ==========
    std::cout << "\n--- 基本查询 ---" << std::endl;
    
    // 检查是否为空
    std::cout << "三角化是否为空: " << (dt.is_empty() ? "是" : "否") << std::endl;
    
    // 获取维度
    std::cout << "维度: " << dt.dimension() << std::endl;
    // 0: 只有顶点, 1: 共线, 2: 平面三角化
    
    // 检查是否为Delaunay(验证)
    std::cout << "是否满足Delaunay性质: " 
              << (dt.is_valid() ? "是" : "否") << std::endl;
    
    // ========== 遍历输出 ==========
    std::cout << "\n--- 遍历有限面 ---" << std::endl;
    
    int face_count = 0;
    for (auto fit = dt.finite_faces_begin(); 
         fit \!= dt.finite_faces_end(); ++fit) {
        if (face_count < 5) {  // 只输出前5个
            std::cout << "面 " << ++face_count << ": ";
            for (int i = 0; i < 3; ++i) {
                Point p = fit->vertex(i)->point();
                std::cout << "(" << p.x() << ", " << p.y() << ") ";
            }
            std::cout << std::endl;
        }
    }
    
    // ========== 几何输出 ==========
    std::cout << "\n--- 输出三角形 ---" << std::endl;
    
    face_count = 0;
    for (auto fit = dt.finite_faces_begin(); 
         fit \!= dt.finite_faces_end(); ++fit) {
        if (face_count < 3) {
            Triangle tri = dt.triangle(fit);
            std::cout << "三角形 " << ++face_count << ": ";
            std::cout << "面积=" << tri.area() << std::endl;
        }
    }
    
    return 0;
}

4.2 顶点插入和删除

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Delaunay_triangulation_2.h>
#include <iostream>
#include <vector>
 
using K = CGAL::Exact_predicates_inexact_constructions_kernel;
using DT = CGAL::Delaunay_triangulation_2<K>;
 
using Point = K::Point_2;
using Vertex_handle = DT::Vertex_handle;
 
void insertion_deletion_example() {
    DT dt;
    
    std::cout << "=== 顶点插入和删除 ===" << std::endl;
    
    // ========== 批量插入优化 ==========
    std::cout << "\n--- 批量插入优化 ---" << std::endl;
    
    std::vector<Point> points;
    for (int i = 0; i < 20; ++i) {
        for (int j = 0; j < 20; ++j) {
            points.push_back(Point(i, j));
        }
    }
    
    // 方法1: 直接插入(CGAL会自动进行空间排序优化)
    dt.insert(points.begin(), points.end());
    std::cout << "直接插入 " << points.size() << " 个点" << std::endl;
    std::cout << "顶点数: " << dt.number_of_vertices() << std::endl;
    
    // 方法2: 手动空间排序(当知道点分布时更高效)
    DT dt2;
    CGAL::spatial_sort(points.begin(), points.end());
    dt2.insert(points.begin(), points.end());
    std::cout << "空间排序后插入,结果相同但可能更快" << std::endl;
    
    // ========== 带提示的插入 ==========
    std::cout << "\n--- 带提示的插入 ---" << std::endl;
    
    // 当知道新点大概位置时,提供提示面可加速定位
    Point new_point(10.5, 10.5);
    Face_handle hint = dt.locate(Point(10, 10));  // 先定位到附近
    Vertex_handle v = dt.insert(new_point, hint);
    
    std::cout << "插入点 (10.5, 10.5),使用提示加速" << std::endl;
    
    // ========== 顶点删除 ==========
    std::cout << "\n--- 顶点删除 ---" << std::endl;
    
    std::cout << "删除前顶点数: " << dt.number_of_vertices() << std::endl;
    
    // 删除单个顶点
    dt.remove(v);
    std::cout << "删除后顶点数: " << dt.number_of_vertices() << std::endl;
    
    // 批量删除
    std::vector<Vertex_handle> to_remove;
    int count = 0;
    for (auto vit = dt.finite_vertices_begin(); 
         vit \!= dt.finite_vertices_end() && count < 10; ++vit, ++count) {
        to_remove.push_back(vit);
    }
    
    dt.remove(to_remove.begin(), to_remove.end());
    std::cout << "批量删除10个顶点后: " << dt.number_of_vertices() << std::endl;
    
    // ========== 移动顶点 ==========
    std::cout << "\n--- 移动顶点 ---" << std::endl;
    
    Vertex_handle vm = dt.insert(Point(5, 5));
    std::cout << "原位置: (" << vm->point().x() << ", " << vm->point().y() << ")" << std::endl;
    
    // 移动顶点到新位置(如果新位置很近,这比删除+插入更高效)
    dt.move(vm, Point(5.5, 5.5));
    std::cout << "新位置: (" << vm->point().x() << ", " << vm->point().y() << ")" << std::endl;
    
    // 验证Delaunay性质仍然保持
    std::cout << "移动后仍满足Delaunay: " << (dt.is_valid() ? "是" : "否") << std::endl;
}
 
int main() {
    insertion_deletion_example();
    return 0;
}

4.3 最近邻查询

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Delaunay_triangulation_2.h>
#include <iostream>
#include <vector>
#include <cmath>
 
using K = CGAL::Exact_predicates_inexact_constructions_kernel;
using DT = CGAL::Delaunay_triangulation_2<K>;
 
using Point = K::Point_2;
using Vertex_handle = DT::Vertex_handle;
 
void nearest_neighbor_example() {
    DT dt;
    
    std::cout << "=== 最近邻查询 ===" << std::endl;
    
    // 创建测试点集
    std::vector<Point> points = {
        Point(0, 0), Point(1, 0), Point(0, 1), Point(1, 1),
        Point(0.5, 0.5), Point(2, 2), Point(-1, -1),
        Point(0.5, 0), Point(0, 0.5), Point(0.25, 0.25)
    };
    dt.insert(points.begin(), points.end());
    
    // ========== 单最近邻查询 ==========
    std::cout << "\n--- 单最近邻查询 ---" << std::endl;
    
    Point query(0.6, 0.6);
    Vertex_handle nearest = dt.nearest_vertex(query);
    
    std::cout << "查询点: (" << query.x() << ", " << query.y() << ")" << std::endl;
    std::cout << "最近邻: (" << nearest->point().x() 
              << ", " << nearest->point().y() << ")" << std::endl;
    
    // 计算实际距离
    double dist = std::sqrt(CGAL::squared_distance(query, nearest->point()));
    std::cout << "欧氏距离: " << dist << std::endl;
    
    // ========== k近邻查询 ==========
    std::cout << "\n--- k近邻查询(手动实现)---" << std::endl;
    
    // CGAL 2D Delaunay不直接支持k近邻,需要手动实现
    auto k_nearest = [&dt](const Point& q, int k) {
        std::vector<std::pair<Vertex_handle, double>> neighbors;
        
        // 遍历所有顶点计算距离
        for (auto vit = dt.finite_vertices_begin(); 
             vit \!= dt.finite_vertices_end(); ++vit) {
            double d = std::sqrt(CGAL::squared_distance(q, vit->point()));
            neighbors.push_back({vit, d});
        }
        
        // 按距离排序
        std::partial_sort(neighbors.begin(), 
                         neighbors.begin() + std::min(k, (int)neighbors.size()),
                         neighbors.end(),
                         [](const auto& a, const auto& b) {
                             return a.second < b.second;
                         });
        
        neighbors.resize(std::min(k, (int)neighbors.size()));
        return neighbors;
    };
    
    auto knn = k_nearest(query, 3);
    std::cout << "3个最近邻:" << std::endl;
    for (const auto& [vh, dist] : knn) {
        std::cout << "  (" << vh->point().x() << ", " << vh->point().y() 
                  << ") 距离=" << dist << std::endl;
    }
    
    // ========== 范围查询 ==========
    std::cout << "\n--- 范围查询 ---" << std::endl;
    
    double radius = 0.5;
    std::cout << "查询点 (" << query.x() << ", " << query.y() << ") 周围 " 
              << radius << " 范围内的点:" << std::endl;
    
    for (auto vit = dt.finite_vertices_begin(); 
         vit \!= dt.finite_vertices_end(); ++vit) {
        double d = std::sqrt(CGAL::squared_distance(query, vit->point()));
        if (d <= radius) {
            std::cout << "  (" << vit->point().x() << ", " << vit->point().y() 
                      << ") 距离=" << d << std::endl;
        }
    }
    
    // ========== 利用Delaunay性质加速范围查询 ==========
    std::cout << "\n--- 利用Delaunay邻域加速 ---" << std::endl;
    
    // 先找到最近邻,然后在其邻域内搜索
    Vertex_handle seed = dt.nearest_vertex(query);
    std::vector<Vertex_handle> local_candidates;
    
    // 获取seed的邻接顶点
    DT::Vertex_circulator vc = dt.incident_vertices(seed), done(vc);
    if (vc \!= nullptr) {
        do {
            if (\!dt.is_infinite(vc)) {
                local_candidates.push_back(vc);
            }
            ++vc;
        } while (vc \!= done);
    }
    
    std::cout << "最近邻的局部邻域有 " << local_candidates.size() << " 个顶点" << std::endl;
    std::cout << "这些顶点是候选的近邻点" << std::endl;
}
 
int main() {
    nearest_neighbor_example();
    return 0;
}

4.4 自然邻域插值

自然邻域插值(Natural Neighbor Interpolation)是一种基于Voronoi图的高精度插值方法。

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Delaunay_triangulation_2.h>
#include <CGAL/natural_neighbor_coordinates_2.h>
#include <iostream>
#include <vector>
#include <utility>
 
using K = CGAL::Exact_predicates_inexact_constructions_kernel;
using DT = CGAL::Delaunay_triangulation_2<K>;
 
using Point = K::Point_2;
using Vertex_handle = DT::Vertex_handle;
 
// 带高度信息的顶点
typedef std::pair<Vertex_handle, double> Vertex_value;
 
void natural_neighbor_interpolation_example() {
    DT dt;
    
    std::cout << "=== 自然邻域插值 ===" << std::endl;
    
    // 创建采样点(带高度值)
    std::vector<std::pair<Point, double>> samples = {
        {{0, 0}, 0.0},
        {{1, 0}, 1.0},
        {{0, 1}, 1.0},
        {{1, 1}, 2.0},
        {{0.5, 0.5}, 1.0},
        {{0.5, 0}, 0.5},
        {{0, 0.5}, 0.5},
        {{1, 0.5}, 1.5},
        {{0.5, 1}, 1.5}
    };
    
    // 存储顶点与值的映射
    std::map<Vertex_handle, double> values;
    
    for (const auto& [p, v] : samples) {
        Vertex_handle vh = dt.insert(p);
        values[vh] = v;
    }
    
    std::cout << "插入 " << samples.size() << " 个采样点" << std::endl;
    
    // ========== 自然邻域坐标 ==========
    std::cout << "\n--- 计算自然邻域坐标 ---" << std::endl;
    
    Point query(0.6, 0.6);
    std::cout << "查询点: (" << query.x() << ", " << query.y() << ")" << std::endl;
    
    // 计算自然邻域坐标
    std::vector<std::pair<Vertex_handle, double>> coords;
    double normalization_factor;
    
    CGAL::natural_neighbor_coordinates_2(dt, query, 
                                        std::back_inserter(coords),
                                        normalization_factor);
    
    std::cout << "自然邻域坐标 (未归一化):" << std::endl;
    for (const auto& [vh, coord] : coords) {
        std::cout << "  顶点 (" << vh->point().x() << ", " << vh->point().y() 
                  << "): 坐标=" << coord << std::endl;
    }
    std::cout << "归一化因子: " << normalization_factor << std::endl;
    
    // ========== 插值计算 ==========
    std::cout << "\n--- 插值计算 ---" << std::endl;
    
    // 使用自然邻域坐标进行加权平均
    double interpolated_value = 0.0;
    for (const auto& [vh, coord] : coords) {
        interpolated_value += coord * values[vh];
    }
    interpolated_value /= normalization_factor;
    
    std::cout << "插值结果: " << interpolated_value << std::endl;
    
    // 与最近邻插值比较
    Vertex_handle nearest = dt.nearest_vertex(query);
    double nearest_value = values[nearest];
    std::cout << "最近邻插值结果: " << nearest_value << std::endl;
    
    // ========== 梯度估计 ==========
    std::cout << "\n--- 梯度估计 ---" << std::endl;
    
    // 使用自然邻域坐标估计梯度
    // 梯度 = sum(coord_i * value_i * (p_i - query)) / normalization
    double grad_x = 0.0, grad_y = 0.0;
    for (const auto& [vh, coord] : coords) {
        double dx = vh->point().x() - query.x();
        double dy = vh->point().y() - query.y();
        grad_x += coord * values[vh] * dx;
        grad_y += coord * values[vh] * dy;
    }
    grad_x /= normalization_factor;
    grad_y /= normalization_factor;
    
    std::cout << "估计梯度: (" << grad_x << ", " << grad_y << ")" << std::endl;
    
    // ========== 多个查询点插值 ==========
    std::cout << "\n--- 网格插值 ---" << std::endl;
    
    std::cout << "在规则网格上插值:" << std::endl;
    for (double x = 0.2; x <= 0.8; x += 0.2) {
        for (double y = 0.2; y <= 0.8; y += 0.2) {
            Point p(x, y);
            std::vector<std::pair<Vertex_handle, double>> nncoords;
            double norm;
            
            CGAL::natural_neighbor_coordinates_2(dt, p, 
                                                std::back_inserter(nncoords), norm);
            
            double val = 0.0;
            for (const auto& [vh, coord] : nncoords) {
                val += coord * values[vh];
            }
            val /= norm;
            
            std::cout << "  (" << x << ", " << y << "): " << val << std::endl;
        }
    }
}
 
int main() {
    natural_neighbor_interpolation_example();
    return 0;
}

5. 复杂度分析

5.1 时间/空间复杂度表格

操作平均复杂度最坏复杂度空间复杂度说明
构造(n个点)随机增量算法
插入单个点期望复杂度
删除顶点需要重建局部区域
移动顶点小位移时高效
点定位walking策略
最近邻查询利用Delaunay性质
遍历所有面线性遍历
Voronoi图计算计算所有对偶
验证检查Delaunay性质

5.2 与其他三角化对比

特性对比表:

特性                      Delaunay    约束Delaunay    正则三角化
-------------------------------------------------------------------
构造复杂度                 O(n log n)   O(n log n)      O(n log n)
插入复杂度                 O(log n)     O(log n)        O(log n)
空外接圆性质               有           有(局部)       有(正交圆)
最大化最小角               是           是(约束内)      否
支持约束边                 否           是              否
支持加权点                 否           否              是
Voronoi图                 标准         受限Voronoi     幂图
最近邻查询                 O(log n)     O(log n)        O(log n)
空间复杂度                 O(n)         O(n)            O(n + h)
                                                    h=隐藏点数

适用场景选择

  • Delaunay_triangulation_2:通用高质量三角化,需要最优三角形质量
  • Constrained_Delaunay_triangulation_2:需要保留特征边(如地形中的山脊线)
  • Regular_triangulation_2:处理加权点,如不同大小的球体填充

6. 可视化内容

6.1 空外接圆性质的ASCII图示

Delaunay三角化的空外接圆验证:

示例1: 满足Delaunay性质的三角化

        p_1(0,2)
           /\\
          /  \\
         /    \\
        /   O   \\      O = 外接圆心
       /   /|\\   \\     R = 外接圆半径
      /  R/ | \\R  \\    圆内无其他点
     /   /  |  \\   \\
    /__/____|____\\__\\
  p_0(0,0)  |    p_2(2,0)
            |
           p_3(2,2)  ← 在圆外(满足Delaunay)

验证:p_3到外接圆心距离 > R

示例2: 违反Delaunay性质的情况

        p_1(0,2)
           /\\
          /  \\
         /    \\
        /   O   \\      
       /   /|\\   \\     
      /  R/ | \\R  \\    
     /   /  |  \\   \\
    /__/____|____\\__\\
  p_0(0,0)  |    p_2(2,0)
            |
           p_3(1,1)  ← 在圆内(违反Delaunay)

验证:p_3到外接圆心距离 < R

需要边翻转:将边(p_0,p_2)翻转为边(p_1,p_3)

翻转后:

        p_1(0,2)
           /|\\
          / | \\
         /  |  \\
        /   |   \\
       /    |    \\
      /     |     \\
     /      |      \\
    /_______|_______\\
  p_0(0,0) p_3(1,1) p_2(2,0)

现在两个三角形都满足空外接圆性质!

6.2 边翻转过程的可视化

边翻转详细过程:

步骤1: 识别需要翻转的边

      p_1                    p_1
     /\\                     /|\\
    /  \\                   / | \\
   / T1 \\                 /  |  \\
  /      \\      -->      / T1| T2 \\
 /   O    \\             /    |    \\
 p_0---p---p_2           p_0--p_3--p_2
  \\      /               \\   |   /
   \\ T2 /                 \\  |  /
    \\  /                   \\ | /
     \\/                     \\|/
      p_3                    p_3

T1 = 三角形(p_0,p_1,p)    T1' = 三角形(p_0,p_1,p_3)
T2 = 三角形(p_0,p,p_2)    T2' = 三角形(p_1,p_2,p_3)
边(p_0,p)需要翻转为边(p_1,p_3)

步骤2: 检查翻转条件

翻转前四边形(p_0,p_1,p_2,p_3)必须是凸的:

凸四边形(可翻转):      凹四边形(不可翻转):

    p_1                      p_1
   /  \\                    /  \\
  /    \\                  /    \\
 /      \\                /      \\
p_0      p_2            p_0------p_2
 \\      /                \\    /
  \\    /                  \\  /
   \\  /                    \\/
    p_3                      p_3
   凸的                     凹的
   (对角线在内部)            (对角线在外部)

步骤3: 执行翻转

翻转前邻接关系:
- T1: 顶点(p_0,p_1,p), 邻接(T3,T2,T4)
- T2: 顶点(p_0,p,p_2), 邻接(T5,T1,T6)

翻转后邻接关系:
- T1': 顶点(p_0,p_1,p_3), 邻接(T3,T2',T6)
- T2': 顶点(p_1,p_2,p_3), 邻接(T1',T5,T4)

步骤4: 验证Delaunay性质

翻转后检查新边(p_1,p_3)是否满足Delaunay:
- 三角形(p_0,p_1,p_3)的外接圆不包含p_2
- 三角形(p_1,p_2,p_3)的外接圆不包含p_0

如果满足,翻转成功!
如果不满足,继续翻转其他边...

6.3 Delaunay vs 非Delaunay对比图

相同点集的不同三角化对比:

输入点集:
    p_1       p_2
      \\     /
       \\   /
        \\ /
         p_3
        / \\
       /   \\
      /     \\
    p_4       p_5

非Delaunay三角化(贪心算法):

    p_1-------p_2
     \\       /
      \\     /
       \\   /
        \\ /
         p_3
        / \\
       /   \\
      /     \\
    p_4-------p_5

问题:
- 三角形(p_1,p_2,p_3)非常细长
- 最小角约15度
- 三角形(p_3,p_4,p_5)同样细长
- 数值稳定性差
- 插值误差大

Delaunay三角化:

    p_1-------p_2
     \\       /
      \\     /
       \\   /
        \\ /
         p_3
        / \\
       /   \\
      /     \\
    p_4-------p_5

优化:
- 边(p_1,p_2)被翻转为边(p_3,p_x)
- 三角形更规则
- 最小角约45度
- 数值稳定性好
- 插值精度高

角度统计对比:
+-------------------+------------+------------+
| 指标              | 非Delaunay | Delaunay   |
+-------------------+------------+------------+
| 最小角            | 15.2°      | 42.8°      |
| 最大角            | 127.5°     | 87.3°      |
| 平均角            | 58.4°      | 60.0°      |
| 标准差            | 32.1°      | 15.6°      |
| 细长三角形数量    | 3          | 0          |
+-------------------+------------+------------+

结论:Delaunay三角化产生更规则的三角形,提高后续计算质量!

7. 总结

Delaunay_triangulation_2是CGAL中最重要、最常用的三角化类:

核心优势

  1. 最优三角形质量:最大化最小角,避免细长三角形
  2. 高效算法:随机增量算法,期望 构造时间
  3. 丰富的查询:点定位、最近邻、Voronoi图等
  4. 数值鲁棒性:精确谓词保证几何正确性

适用场景

  • 地形建模:从采样点构建高质量地形网格
  • 有限元分析:生成数值稳定的计算网格
  • 计算机图形学:网格变形、纹理映射、 morphing
  • 插值与逼近:自然邻域插值、曲面重建
  • 最近邻搜索:高效的空间索引结构

使用建议

  1. 内核选择:默认使用 Exact_predicates_inexact_constructions_kernel
  2. 批量插入:使用 insert(first, last) 而非逐个插入
  3. 空间排序:对大规模数据先进行 spatial_sort
  4. 验证调试:开发时使用 is_valid() 检查三角化正确性

学习路径

  1. 掌握 Triangulation_2 的基本概念和操作
  2. 理解Delaunay性质及其几何意义
  3. 学习边翻转算法和增量构造
  4. 探索Voronoi图和对偶关系
  5. 应用到实际问题:插值、网格生成、空间查询

Delaunay三角化是计算几何的基石,理解其原理和实现对于解决各类几何问题至关重要。CGAL的 Delaunay_triangulation_2 提供了高效、鲁棒、易用的实现,是学习和应用的理想工具。