分治算法(Divide and Conquer)是一种解决问题的算法设计策略,它将一个大问题分解成若干个规模较小且相互独立的子问题,然后将这些子问题的解合并起来,从而得到原问题的解。
分治算法通常包括以下三个步骤:
给定⼆维平面上的n个点集 S = ( x i , y i ) ∣ i = 1 , 2 , . . . , n S={(x_{i},y_{i})|i=1,2,...,n} S=(xi,yi)∣i=1,2,...,n ,找到其凸包
- 假设没有两点具有相同的横坐标
- 假设没有两点具有相同的纵坐标
- 假设没有三点共线
//点结构
struct Point {
int x, y;
};
//边结构
struct Line {
Point start, end;
};
//判断点和线段的方向关系
int crossProduct(Point A, Point B, Point C) {
return (B.x - A.x) * (C.y - B.y) - (B.y - A.y) * (C.x - B.x);
}
由于凸包是点集S最小的凸多边形的顶点或边,我们可以从几何的角度进行观察,对于每⼀个点,如果它在其他任意三点构成的三角形中,那么它就被凸包所围,换句话说,它不可能是构成最小凸包的点。而那些构成凸包的点则不会处在任意三角形的内部。
bool isInsideTriangle(Point A, Point B, Point C, Point P) {
int crossABP = crossProduct(A, B, P);
int crossBCP = crossProduct(B, C, P);
int crossCAP = crossProduct(C, A, P);
return (crossABP >= 0 && crossBCP >= 0 && crossCAP >= 0) ||
(crossABP <= 0 && crossBCP <= 0 && crossCAP <= 0);
}
根据凸包定义,我们也可以通过两个点组成的边来判断其是否属于凸包:
bool isConvexHullEdge(vector<Point>& points, Line edge) {
//遍历点
for (Point p : points) {
if (p != edge.start && p != edge.end) {
int crossProductValue = crossProduct(edge.start, edge.end, p);
if (crossProductValue > 0) {
return false;
}
}
}
return true;
}
对于点集的分解目前有两种分解策略:
- 分解:把n个点分成A、B两部分,其中A有n -1个点,B只有1个点
- 选取一个特别的点q放入B: 最右边的点 (横坐标最大的点)
- 该点一定属于新的凸包
- 递归求解A的凸包CH(A)
- 基本情况: 三个点,直接计算
- 合并CH(A) 和 q
- 算法时间复杂度: T ( n ) = T ( n − 1 ) + T m e r g e T(n)= T( n - 1)+ T_{merge} T(n)=T(n−1)+Tmerge
- T m e r g e = O ( n 2 ) ⇒ T ( n ) = O ( n 3 ) T_{merge} = O(n^{2}) \Rightarrow T(n) = O(n^{3}) Tmerge=O(n2)⇒T(n)=O(n3)
- T m e r g e = O ( n ) ⇒ T ( n ) = O ( n 2 ) T_{merge} = O(n) \Rightarrow T(n) = O(n^{2}) Tmerge=O(n)⇒T(n)=O(n2)
- T m e r g e = O ( log n ) ⇒ T ( n ) = O ( n log n ) T_{merge} = O(\log{n}) \Rightarrow T(n) = O(n\log{n}) Tmerge=O(logn)⇒T(n)=O(nlogn)
可以看到,降低插入凸包的复杂度关键是降低Merge时的复杂度。
- 凸包的支撑线: 与凸包仅相交于一点的直线
- 过凸包外一点有且仅有两条该凸包的支撑线 ( q p 3 qp_{3} qp3和 q p 5 qp_{5} qp5)
- 两个交点 ( p 3 p_{3} p3和 p 5 p_{5} p5)将凸包边界分成两个链: 近链和远链
- 新的凸包由远链和点q决定
- 从左支撑线开始,在凸包边界上顺时针前进直到右支撑线
如何求左右支撑线:
暴力求解:
- O ( n ) O(n) O(n)条候选支撑线 q p i qp_{i} qpi;
- 检查所有其他点是否在 q p i qp_{i} qpi同一侧: O ( n ) O(n) O(n)时间
- 共需 O ( n 2 ) O(n^{2}) O(n2)时间
优化搜索测略:
- 寻找左支撑线
- 从距离 q q q最近的 p i p_{i} pi开始,检查 p ( i − 1 ) % n p_{(i-1)\%n} p(i−1)%n和 p ( i + 1 ) % n p_{(i+1)\%n} p(i+1)%n是否都在 q p i qp_{i} qpi右侧
- 右侧寻找右支撑线
- 从距离 q q q最近的 p i p_{i} pi开始,检查 p ( i − 1 ) % n p_{(i-1)\%n} p(i−1)%n和 p ( i + 1 ) % n p_{(i+1)\%n} p(i+1)%n是否都在 q p i qp_{i} qpi左侧
- T ( n ) = T ( n − 1 ) + O ( n ) ⇒ T ( n ) = O ( n 2 ) T(n) = T(n - 1) + O(n)\Rightarrow T(n) = O(n^{2}) T(n)=T(n−1)+O(n)⇒T(n)=O(n2)
function ConvexHull(S):
if |S| <= 3:
return ComputeConvexHull(S)
else:
Divide S into A and B
q = rightmost point in B
CH_A = ConvexHull(A)
Merge CH_A with q to form the new convex hull CH
function Merge(CH_A, q):
Find left and right support lines for CH_A and q
Traverse the boundary of CH_A from left support line to right support line and update CH
function ComputeConvexHull(S):
// Implement a convex hull algorithm for small inputs (e.g., Gift Wrapping)
// Initial call to ConvexHull with the entire set of points S
ConvexHull(S)
- 分解: 把n个点分成A、B两部分,其中A、B各有n/2个点
- 预处理: 将所有点按横坐标排序
- 递归求解A和B的凸包,记为P和O
- 基本情况:是不是三个点?
- 合并P和Q
- 算法时间复杂度: T ( n ) = 2 T ( n / 2 ) + T m e r g e T(n) = 2T(n/2)+ T_{merge} T(n)=2T(n/2)+Tmerge
IF T m e r g e = O ( n ) ⇒ T ( n ) = O ( n l o g n ) T_{merge} = O(n) \Rightarrow T(n) = O(nlogn) Tmerge=O(n)⇒T(n)=O(nlogn)
寻找上下桥
- 按照x轴排序后选择最大和最小两个极端点a和b (一定属于凸包)
- a b → \overrightarrow{ab} ab将整个点集分成两部分: a b → \overrightarrow{ab} ab左边的点集A、 a b → \overrightarrow{ab} ab右边的点集B
- A中哪些点一定属于凸包?
- 距离 a b → \overrightarrow{ab} ab 最远的点c: O(1)时间可确定c到 a b → \overrightarrow{ab} ab的距离
- △ a b c \bigtriangleup abc △abc 中的点一定不属于凸包
- △ a b c \bigtriangleup abc △abc 外的点?
- b c → \overrightarrow{bc} bc右边的点: 递归求解哪些点属于凸包
- c a → \overrightarrow{ca} ca右边的点: 递归求解哪些点属于凸包
- B中哪些点一定属于凸包?
quick_hull( S S S) :
( a , b ) (a, b) (a,b) ← extreme_points( S S S)
A A A ← right_of ( S , b a → ) (S, \overrightarrow{ba}) (S,ba)
B B B ← right_of ( S , a b → ) (S, \overrightarrow{ab}) (S,ab)
Q A Q_{A} QA ← quick_half_hull ( A , b a → ) (A, \overrightarrow{ba}) (A,ba)
Q B Q_{B} QB ← quick_half_hull ( A , a b → ) (A, \overrightarrow{ab}) (A,ab)
return { a } ∪ Q A ∪ { b } ∪ Q B \{a\} ∪ Q_{A} ∪ \{b\} ∪ Q_{B} {a}∪QA∪{b}∪QB
quick_half_hull ( S , b a → ) (S, \overrightarrow{ba}) (S,ba):
if ( s = ⊘ ) (s=\oslash ) (s=⊘) return ⊘ \oslash ⊘
c ← furthest ( S , b a → ) (S, \overrightarrow{ba}) (S,ba)
A ← right_of ( S , b c → ) (S, \overrightarrow{bc}) (S,bc)
B ← right_of ( S , c a → ) (S, \overrightarrow{ca}) (S,ca)
Q A Q_{A} QA ← quick_half_hull ( S , b c → ) (S, \overrightarrow{bc}) (S,bc)
Q B Q_{B} QB ← quick_half_hull ( S , c a → ) (S, \overrightarrow{ca}) (S,ca)
return Q A ∪ { c } ∪ Q B Q_{A} ∪ \{c\} ∪ Q_{B} QA∪{c}∪QB
时间复杂度分析
令 ∣ S ∣ = n , ∣ A ∣ = α , ∣ B ∣ = β , α + β ≤ n − 1 |S| =n,|A|=\alpha,|B|=\beta,\alpha+\beta\le n-1 ∣S∣=n,∣A∣=α,∣B∣=β,α+β≤n−1
- quick_half_hull
- T ( n ) = T ( α ) + T ( β ) + O ( n ) T(n)=T({\alpha})+T(\beta)+O(n) T(n)=T(α)+T(β)+O(n)
1. α = β = n 2 ⇒ T ( n ) = O ( n l o g n ) \alpha=\beta=\frac{n}{2} \Rightarrow T(n)=O(nlogn) α=β=2n⇒T(n)=O(nlogn)
2. α = 0 , β = n − 1 ⇒ T ( n ) = O ( n 2 ) \alpha=0,\beta=n-1 \Rightarrow T(n)=O(n^{2}) α=0,β=n−1⇒T(n)=O(n2)- quick_hull
- 预排序计算极端点: O ( n l o g n ) O(nlogn) O(nlogn)
- 计算A和B: O ( n ) O(n) O(n)
- 递归求解A和B: < 2 T ( n ) <2T(n) <2T(n)
- 最坏情况下也是 O ( n 2 ) O(n^{2}) O(n2)
#include
#include
using namespace std;
struct Point {
int x, y;
};
//判断C在AB的哪一侧
int crossProduct(Point A, Point B, Point C) {
return (B.x - A.x) * (C.y - B.y) - (B.y - A.y) * (C.x - B.x);
}
//计算点C到直线AB的距离
double distanceToLine(Point A, Point B, Point P) {
int crossProductValue = abs((B.x - A.x) * (A.y - P.y) - (A.x - P.x) * (B.y - A.y));
double lengthAB = sqrt(pow(B.x - A.x, 2) + pow(B.y - A.y, 2));
return crossProductValue / lengthAB;
}
//找到两个极端点
vector<Point> findExtremePoints(vector<Point>& S) {
Point leftmost = S[0], rightmost = S[0];
for (Point p : S) {
if (p.x < leftmost.x)
leftmost = p;
if (p.x > rightmost.x)
rightmost = p;
}
return {leftmost, rightmost};
}
//计算ab直线的右侧点点集,需要注意的是ab和ba的统计效果相反
vector<Point> rightOf(vector<Point>& S, Point a, Point b) {
vector<Point> result;
for (Point p : S) {
if (crossProduct(a, b, p) < 0)
result.push_back(p);
}
return result;
}
//计算点集最远端点
Point findFurthestPoint(vector<Point>& S, Point a, Point b) {
Point furthest = S[0];
int maxDist = -1;
for (Point p : S) {
double dist = distanceToLine(a, b, p);
if (dist > maxDist) {
maxDist = dist;
furthest = p;
}
}
return furthest;
}
vector<Point> quickHalfHull(vector<Point>& S, Point a, Point b) {
if (S.empty())
return {};
Point c = findFurthestPoint(S, a, b);
vector<Point> A = rightOf(S, b, c);
vector<Point> B = rightOf(S, c, a);
vector<Point> QA = quickHalfHull(A, b, c);
vector<Point> QB = quickHalfHull(B, c, a);
vector<Point> result = QA;
result.push_back(c);
result.insert(result.end(), QB.begin(), QB.end());
return result;
}
vector<Point> quickHull(vector<Point>& S) {
vector<Point> extremePoints = findExtremePoints(S);
Point a = extremePoints[0];
Point b = extremePoints[1];
vector<Point> A = rightOf(S, b, a);
vector<Point> B = rightOf(S, a, b);
vector<Point> QA = quickHalfHull(A, b, a);
vector<Point> QB = quickHalfHull(B, a, b);
vector<Point> result = QA;
result.push_back(a);
result.insert(result.end(), QB.begin(), QB.end());
return result;
}
int main() {
// Example usage
vector<Point> points = {{0, 0}, {1, 1}, {2, 2}, {3, 3}, {0, 3}};
vector<Point> convexHull = quickHull(points);
cout << "Convex Hull Points: ";
for (Point p : convexHull) {
cout << "(" << p.x << ", " << p.y << ") ";
}
cout << endl;
return 0;
}