7.4 正则三角化 (Regular_triangulation_2)
上一节: 7.2 二维Delaunay三角化 下一节: 7.5 特殊三角化
1. 数学理论基础
1.1 加权点与幂距离
加权点 (Weighted Point) 是带有权重的平面点:
其中 是位置, 是权重。
幂距离 (Power Distance) 定义为:
几何解释:
- 当 :加权点对应以 为中心、 为半径的圆
- 当 :退化为普通点
- 当 :对应虚圆
1.2 幂图 (Power Diagram)
幂图是Voronoi图在加权点上的推广:
幂图的性质:
- 单元可能是空集(被隐藏的加权点)
- 单元是凸多边形(可能无界)
- 边的数目可能少于Voronoi图
1.3 正则三角化的定义
正则三角化 (Regular Triangulation) 是幂图的对偶图:
两个加权点的单元共享一条边,当且仅当它们在正则三角化中通过一条边连接。
形式化定义:对于加权点集 ,正则三角化 满足:
1.4 空外接圆性质的推广
正则三角化推广了Delaunay的空外接圆性质:
正交圆测试 (Orthogonality Test):
三角形 是正则的,如果不存在其他加权点 使得:
\text{orthogonal_circle}(\hat{p}_i, \hat{p}_j, \hat{p}_k) \text{ 包含 } p_l \text{ 且 } w_l > \text{power}(p_l, \text{circle})
等价表述:对于三角形的外接正交圆 ,所有其他加权点的中心都在 外部,或者在 上但权重更小。
1.5 隐藏点 (Hidden Points)
当加权点的权重过大时,其幂单元可能为空,该点被隐藏:
即存在另一个加权点的圆完全包含 。
隐藏点的性质:
- 隐藏点不参与正则三角化
- 隐藏点可能被”揭示”(当其他点被删除时)
- 正则三角化只包含可见点
1.6 与Delaunay的关系
| 特性 | Delaunay | Regular |
|---|---|---|
| 输入 | 普通点 | 加权点 |
| 对偶 | Voronoi图 | 幂图 |
| 空圆测试 | 外接圆 | 正交圆 |
| 隐藏点 | 无 | 可能有 |
| 退化情况 | 四点共圆 | 四点共正交圆 |
当所有权重为0时,正则三角化退化为Delaunay三角化。
2. 类架构分析
2.1 继承层次
Triangulation_2<Traits, Tds>
└── Delaunay_triangulation_2<Traits, Tds>
└── Regular_triangulation_2<Traits, Tds>
注意:Regular_triangulation_2继承自Delaunay_triangulation_2,而非直接继承Triangulation_2。
2.2 模板参数
template <class Traits_, class Tds_ = Default>
class Regular_triangulation_2 : public Delaunay_triangulation_2<Traits_, Tds_> {
// ...
};Traits要求
class RegularTriangulationTraits_2 : public DelaunayTriangulationTraits_2 {
public:
// 加权点类型
typedef Weighted_point_2 Weighted_point;
// 构造加权点
typedef Construct_weighted_point_2 Construct_weighted_point;
// 幂距离
typedef Power_distance_2 Power_distance;
// 正交圆测试
typedef Power_test_2 Power_test; // 相当于Delaunay的side_of_oriented_circle
// 从加权点提取普通点
typedef Construct_point_2 Construct_point;
};2.3 核心类型与方法
// 继承自Delaunay_triangulation_2的所有类型
using Base = Delaunay_triangulation_2<Traits, Tds>;
// 新增类型
typedef Traits::Weighted_point_2 Weighted_point;
// 核心方法
// 插入加权点
Vertex_handle insert(const Weighted_point& wp, Face_handle hint = Face_handle());
// 点定位(基于幂距离)
Face_handle locate(const Weighted_point& wp, Face_handle hint = Face_handle());
// 隐藏点相关
bool is_hidden(Vertex_handle v); // 顶点是否被隐藏
void hide_vertex(Vertex_handle v); // 隐藏顶点
void reveal_vertex(Vertex_handle v); // 揭示顶点
// 幂图相关
Point dual(Face_handle f); // 面的对偶点(正交中心)
Segment dual(Edge e); // 边的对偶线段
// 批量操作
template <class InputIterator>
int insert(InputIterator first, InputIterator last);2.4 内部表示
// 顶点存储加权点
class Regular_vertex : public Delaunay_vertex {
Weighted_point wp;
bool hidden;
public:
const Weighted_point& point() const { return wp; }
bool is_hidden() const { return hidden; }
void set_hidden(bool h) { hidden = h; }
};
// 隐藏点链表(存储在三角化数据结构中)
class Regular_triangulation_2 {
std::list<Vertex_handle> hidden_vertices;
// 当点被隐藏时,从三角化中移除但保留在链表中
};3. 实现细节
3.1 加权点插入算法
// 插入加权点的主算法
Vertex_handle insert(const Weighted_point& wp) {
// 1. 提取几何点
Point p = wp.point();
// 2. 检查是否被隐藏
Face_handle loc = locate(wp);
if (is_hidden_by_face(wp, loc)) {
// 点被隐藏,加入隐藏链表
Vertex_handle v = create_vertex(wp);
v->set_hidden(true);
hidden_vertices.push_back(v);
return v;
}
// 3. 检查是否有被该点隐藏的顶点
std::list<Vertex_handle> newly_hidden;
find_hidden_vertices(wp, newly_hidden);
// 4. 隐藏这些顶点
for (Vertex_handle hv : newly_hidden) {
hide_vertex(hv);
}
// 5. 插入点(使用Delaunay插入)
Vertex_handle v = Delaunay_triangulation_2::insert(wp);
// 6. 恢复正则性质(边翻转)
restore_regular(v);
return v;
}
// 检查点是否被面隐藏
bool is_hidden_by_face(const Weighted_point& wp, Face_handle f) {
// 检查wp是否被f的三个顶点中的任何一个隐藏
for (int i = 0; i < 3; ++i) {
const Weighted_point& wp_i = f->vertex(i)->point();
if (power_distance(wp_i, wp.point()) < 0) {
return true; // 被隐藏
}
}
return false;
}
// 恢复正则性质
void restore_regular(Vertex_handle v) {
// 类似Delaunay的边翻转,但使用power_test
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;
Face_handle n = f->neighbor(i);
if (is_infinite(n)) continue;
// 使用power_test代替side_of_oriented_circle
Vertex_handle v_op = n->vertex(n->index(f));
if (power_test(f, v_op->point()) == ON_POSITIVE_SIDE) {
flip(f, i);
edges_to_check.push(Edge(f, (i+1)%3));
edges_to_check.push(Edge(n, (n->index(f)+1)%3));
}
}
}3.2 幂测试 (Power Test)
// 判断点相对于正交圆的位置
Oriented_side power_test(Face_handle f, const Weighted_point& wp) {
// 计算三角形三个加权点的正交圆
// 然后测试wp相对于该圆的位置
const Weighted_point& wp0 = f->vertex(0)->point();
const Weighted_point& wp1 = f->vertex(1)->point();
const Weighted_point& wp2 = f->vertex(2)->point();
// 使用行列式计算
// | x0 y0 x0²+y0²-w0 1 |
// | x1 y1 x1²+y1²-w1 1 |
// | x2 y2 x2²+y2²-w2 1 |
// | xp yp xp²+yp²-wp 1 |
return traits.power_test_2_object()(wp0, wp1, wp2, wp);
}
// 幂距离计算
double power_distance(const Weighted_point& wp1, const Point& p2) {
double dx = wp1.x() - p2.x();
double dy = wp1.y() - p2.y();
return dx*dx + dy*dy - wp1.weight();
}3.3 删除算法
// 删除顶点
void remove(Vertex_handle v) {
// 1. 检查是否有被隐藏的顶点可能被揭示
std::list<Vertex_handle> to_reveal;
for (Vertex_handle hv : hidden_vertices) {
if (would_be_revealed(hv, v)) {
to_reveal.push_back(hv);
}
}
// 2. 执行Delaunay删除
Delaunay_triangulation_2::remove(v);
// 3. 揭示顶点
for (Vertex_handle hv : to_reveal) {
reveal_vertex(hv);
hidden_vertices.remove(hv);
// 重新插入(现在可见)
insert(hv->point());
delete_vertex(hv); // 删除旧的隐藏顶点
}
}
// 检查删除v后hv是否会被揭示
bool would_be_revealed(Vertex_handle hv, Vertex_handle v_to_remove) {
// 如果hv被v_to_remove隐藏,则删除后可能被揭示
const Weighted_point& wp_hv = hv->point();
const Weighted_point& wp_v = v_to_remove->point();
return power_distance(wp_v, wp_hv.point()) < 0;
}3.4 幂图计算
// 计算面的对偶点(正交中心)
Point power_center(Face_handle f) {
const Weighted_point& wp0 = f->vertex(0)->point();
const Weighted_point& wp1 = f->vertex(1)->point();
const Weighted_point& wp2 = f->vertex(2)->point();
// 计算三个加权点的正交中心
// 这是到三个圆都有相等幂距离的点
return traits.construct_power_center_2_object()(wp0, wp1, wp2);
}
// 计算边的对偶
Segment dual_power_edge(Edge e) {
Face_handle f1 = e.first;
Face_handle f2 = f1->neighbor(e.second);
Point pc1 = power_center(f1);
Point pc2 = power_center(f2);
return Segment(pc1, pc2);
}4. 完整代码示例
4.1 基础正则三角化
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Regular_triangulation_2.h>
#include <iostream>
#include <vector>
using K = CGAL::Exact_predicates_inexact_constructions_kernel;
using Regular = CGAL::Regular_triangulation_2<K>;
using Weighted_point = Regular::Weighted_point;
using Point = K::Point_2;
using Vertex_handle = Regular::Vertex_handle;
using Face_handle = Regular::Face_handle;
int main() {
Regular rt;
// ========== 插入加权点 ==========
std::cout << "=== 正则三角化 ===" << std::endl;
// 创建加权点 (位置, 权重)
// 权重 = 半径的平方
std::vector<Weighted_point> weighted_points = {
Weighted_point(Point(0, 0), 1.0), // 圆心在(0,0),半径1
Weighted_point(Point(4, 0), 1.0), // 圆心在(4,0),半径1
Weighted_point(Point(2, 3), 1.0), // 圆心在(2,3),半径1
Weighted_point(Point(2, 1), 0.5), // 圆心在(2,1),半径sqrt(0.5)
Weighted_point(Point(1, 1), 0.25), // 圆心在(1,1),半径0.5
Weighted_point(Point(3, 1), 0.25), // 圆心在(3,1),半径0.5
};
// 批量插入
rt.insert(weighted_points.begin(), weighted_points.end());
std::cout << "插入 " << weighted_points.size() << " 个加权点" << std::endl;
std::cout << "三角化中顶点数: " << rt.number_of_vertices() << std::endl;
std::cout << "面数: " << rt.number_of_faces() << std::endl;
// ========== 检查隐藏点 ==========
std::cout << "\n=== 隐藏点检查 ===" << std::endl;
int hidden_count = 0;
int visible_count = 0;
for (auto vit = rt.all_vertices_begin(); vit != rt.all_vertices_end(); ++vit) {
if (rt.is_hidden(vit)) {
++hidden_count;
} else {
++visible_count;
}
}
std::cout << "可见点: " << visible_count << std::endl;
std::cout << "隐藏点: " << hidden_count << std::endl;
// ========== 遍历三角形 ==========
std::cout << "\n=== 正则三角化中的三角形 ===" << std::endl;
int triangle_count = 0;
for (auto fit = rt.finite_faces_begin(); fit != rt.finite_faces_end(); ++fit) {
std::cout << "三角形 " << ++triangle_count << ": ";
for (int i = 0; i < 3; ++i) {
Weighted_point wp = fit->vertex(i)->point();
Point p = wp.point();
std::cout << "[(" << p.x() << ", " << p.y() << "), w=" << wp.weight() << "] ";
}
std::cout << std::endl;
}
// ========== 幂测试验证 ==========
std::cout << "\n=== 正则性质验证 ===" << std::endl;
bool is_regular = true;
for (auto fit = rt.finite_faces_begin(); fit != rt.finite_faces_end(); ++fit) {
for (auto vit = rt.finite_vertices_begin(); vit != rt.finite_vertices_end(); ++vit) {
if (fit->has_vertex(vit)) continue;
auto side = rt.power_test(fit, vit->point());
if (side == CGAL::ON_POSITIVE_SIDE) {
std::cout << "违反正则性质!" << std::endl;
is_regular = false;
break;
}
}
}
std::cout << "正则验证: " << (is_regular ? "通过" : "失败") << std::endl;
// ========== 点定位 ==========
std::cout << "\n=== 点定位 ===" << std::endl;
Weighted_point query(Point(2, 1.5), 0.1);
Face_handle loc = rt.locate(query);
if (!rt.is_infinite(loc)) {
std::cout << "查询点位于三角形内,顶点为:" << std::endl;
for (int i = 0; i < 3; ++i) {
Weighted_point wp = loc->vertex(i)->point();
Point p = wp.point();
std::cout << " [(" << p.x() << ", " << p.y() << "), w=" << wp.weight() << "]" << std::endl;
}
}
return 0;
}4.2 幂图计算
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Regular_triangulation_2.h>
#include <iostream>
#include <vector>
#include <fstream>
using K = CGAL::Exact_predicates_inexact_constructions_kernel;
using Regular = CGAL::Regular_triangulation_2<K>;
using Weighted_point = Regular::Weighted_point;
using Point = K::Point_2;
using Segment = K::Segment_2;
using Face_handle = Regular::Face_handle;
using Edge = Regular::Edge;
int main() {
Regular rt;
// 创建加权点(模拟不同大小的圆)
std::vector<Weighted_point> points;
// 中心大圆
points.push_back(Weighted_point(Point(5, 5), 4.0));
// 周围小圆
for (int i = 0; i < 8; ++i) {
double angle = 2 * M_PI * i / 8;
double x = 5 + 3 * cos(angle);
double y = 5 + 3 * sin(angle);
points.push_back(Weighted_point(Point(x, y), 0.5));
}
rt.insert(points.begin(), points.end());
std::cout << "=== 幂图计算 ===" << std::endl;
std::cout << "加权点数: " << points.size() << std::endl;
std::cout << "三角化顶点数: " << rt.number_of_vertices() << std::endl;
// ========== 计算幂图顶点(正交中心) ==========
std::cout << "\n=== 幂图顶点 ===" << std::endl;
std::vector<Point> power_vertices;
for (auto fit = rt.finite_faces_begin(); fit != rt.finite_faces_end(); ++fit) {
Point pc = rt.dual(fit); // 正交中心
power_vertices.push_back(pc);
std::cout << "正交中心: (" << pc.x() << ", " << pc.y() << ")" << std::endl;
}
// ========== 计算幂图边 ==========
std::cout << "\n=== 幂图边 ===" << std::endl;
std::vector<Segment> power_edges;
for (auto eit = rt.finite_edges_begin(); eit != rt.finite_edges_end(); ++eit) {
Face_handle f1 = eit->first;
Face_handle f2 = f1->neighbor(eit->second);
if (!rt.is_infinite(f2)) {
Segment seg = rt.dual(*eit);
power_edges.push_back(seg);
std::cout << "幂边: (" << seg.source().x() << ", " << seg.source().y()
<< ") -> (" << seg.target().x() << ", " << seg.target().y() << ")" << std::endl;
}
}
// ========== 输出到SVG ==========
std::ofstream svg("power_diagram.svg");
svg << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
svg << "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"600\" height=\"600\">\n";
// 绘制加权圆(输入圆)
svg << "<g fill=\"none\" stroke=\"red\" stroke-width=\"1\">\n";
for (const auto& wp : points) {
Point p = wp.point();
double r = std::sqrt(wp.weight());
svg << "<circle cx=\"" << p.x() * 50 + 50
<< "\" cy=\"" << 550 - p.y() * 50
<< "\" r=\"" << r * 50 << "\"/>\n";
}
svg << "</g>\n";
// 绘制正则三角化(灰色虚线)
svg << "<g stroke=\"gray\" stroke-width=\"1\" stroke-dasharray=\"5,5\">\n";
for (auto eit = rt.finite_edges_begin(); eit != rt.finite_edges_end(); ++eit) {
Segment seg = rt.segment(*eit);
svg << "<line x1=\"" << seg.source().x() * 50 + 50
<< "\" y1=\"" << 550 - seg.source().y() * 50
<< "\" x2=\"" << seg.target().x() * 50 + 50
<< "\" y2=\"" << 550 - seg.target().y() * 50 << "\"/>\n";
}
svg << "</g>\n";
// 绘制幂图(蓝色)
svg << "<g stroke=\"blue\" stroke-width=\"2\">\n";
for (const auto& seg : power_edges) {
svg << "<line x1=\"" << seg.source().x() * 50 + 50
<< "\" y1=\"" << 550 - seg.source().y() * 50
<< "\" x2=\"" << seg.target().x() * 50 + 50
<< "\" y2=\"" << 550 - seg.target().y() * 50 << "\"/>\n";
}
svg << "</g>\n";
// 绘制圆心(红色点)
svg << "<g fill=\"red\">\n";
for (const auto& wp : points) {
Point p = wp.point();
svg << "<circle cx=\"" << p.x() * 50 + 50
<< "\" cy=\"" << 550 - p.y() * 50 << "\" r=\"3\"/>\n";
}
svg << "</g>\n";
svg << "</svg>\n";
svg.close();
std::cout << "\n幂图已保存到 power_diagram.svg" << std::endl;
return 0;
}4.3 隐藏点演示
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Regular_triangulation_2.h>
#include <iostream>
#include <vector>
using K = CGAL::Exact_predicates_inexact_constructions_kernel;
using Regular = CGAL::Regular_triangulation_2<K>;
using Weighted_point = Regular::Weighted_point;
using Point = K::Point_2;
using Vertex_handle = Regular::Vertex_handle;
int main() {
Regular rt;
std::cout << "=== 隐藏点演示 ===" << std::endl;
// 首先插入一个大圆
Weighted_point big_circle(Point(5, 5), 9.0); // 半径3
Vertex_handle v_big = rt.insert(big_circle);
std::cout << "插入大圆 [(5,5), r=3]" << std::endl;
std::cout << "顶点数: " << rt.number_of_vertices() << std::endl;
// 插入一个小圆(在大圆内部)
Weighted_point small_circle(Point(5, 5), 0.25); // 半径0.5,同圆心
Vertex_handle v_small = rt.insert(small_circle);
std::cout << "\n插入小圆 [(5,5), r=0.5]" << std::endl;
std::cout << "小圆是否隐藏: " << (rt.is_hidden(v_small) ? "是" : "否") << std::endl;
std::cout << "顶点数: " << rt.number_of_vertices() << std::endl;
// 插入另一个圆(部分与大圆重叠)
Weighted_point overlap_circle(Point(6, 5), 1.0); // 半径1
Vertex_handle v_overlap = rt.insert(overlap_circle);
std::cout << "\n插入重叠圆 [(6,5), r=1]" << std::endl;
std::cout << "重叠圆是否隐藏: " << (rt.is_hidden(v_overlap) ? "是" : "否") << std::endl;
std::cout << "顶点数: " << rt.number_of_vertices() << std::endl;
// 显示所有可见点
std::cout << "\n=== 可见点 ===" << std::endl;
for (auto vit = rt.finite_vertices_begin(); vit != rt.finite_vertices_end(); ++vit) {
Weighted_point wp = vit->point();
Point p = wp.point();
std::cout << "[(" << p.x() << ", " << p.y() << "), r="
<< std::sqrt(wp.weight()) << "]" << std::endl;
}
// 演示删除后揭示隐藏点
std::cout << "\n=== 删除大圆 ===" << std::endl;
rt.remove(v_big);
std::cout << "删除后顶点数: " << rt.number_of_vertices() << std::endl;
// 检查之前隐藏的点是否被揭示
std::cout << "\n删除后的可见点:" << std::endl;
for (auto vit = rt.finite_vertices_begin(); vit != rt.finite_vertices_end(); ++vit) {
Weighted_point wp = vit->point();
Point p = wp.point();
std::cout << "[(" << p.x() << ", " << p.y() << "), r="
<< std::sqrt(wp.weight()) << "]" << std::endl;
}
return 0;
}5. 复杂度分析
5.1 时间复杂度
| 操作 | 平均复杂度 | 最坏复杂度 | 说明 |
|---|---|---|---|
| insert(wp) | O(log n) | O(n) | 加权点插入 |
| insert(first, last) | O(n log n) | O(n²) | 批量插入 |
| remove(v) | O(log n) | O(n) | 顶点删除(可能揭示隐藏点) |
| locate(wp) | O(log n) | O(n) | 点定位 |
| is_hidden(v) | O(1) | O(1) | 常数时间 |
| power_test | O(1) | O(1) | 正交圆测试 |
5.2 空间复杂度
| 存储项目 | 空间 |
|---|---|
| 基础三角化 | O(n) |
| 隐藏点链表 | O(h),h为隐藏点数 |
| 权重存储 | O(n) |
| 总计 | O(n + h) |
5.3 与Delaunay的比较
| 特性 | Delaunay | Regular |
|---|---|---|
| 输入 | 普通点 | 加权点 |
| 插入 | O(log n) | O(log n) |
| 隐藏点 | 无 | 有 |
| 删除复杂度 | O(log n) | O(log n + r),r为揭示点数 |
| 空间开销 | O(n) | O(n + h) |
| 应用场景 | 一般三角化 | 带权重的邻近查询、球体填充 |
6. 应用场景
6.1 分子生物学
// 计算分子表面的溶剂可及表面积
// 原子作为加权点(位置+范德华半径)
// 正则三角化计算分子表面6.2 球体填充与堆积
// 不同大小球体的空间填充
// 球心作为加权点,半径平方作为权重
// 正则三角化给出邻近关系6.3 各向异性网格生成
// 根据局部特征尺寸调整权重
// 生成自适应网格6.4 经济学与博弈论
// 权力指数计算
// 市场区域划分7. 总结
Regular_triangulation_2是Delaunay三角化在加权点上的自然推广:
- 统一的框架:当所有权重为0时退化为Delaunay三角化
- 隐藏点机制:自动处理被完全包含的加权点
- 幂图对偶:支持加权点的邻近查询和空间分解
- 广泛的应用:从分子生物学到经济学
正则三角化展示了CGAL三角化框架的灵活性,通过模板参数和继承机制,在保持接口一致性的同时支持复杂的几何概念。