凸包算法入门

一、什么是凸包(Convex Hull)?

把一些点钉在木板上,然后用一根橡皮筋把它们套起来,松开后橡皮筋形成的形状就是凸包。

给定平面上一组点,凸包就是一个凸多边形,它刚好把所有点包围在内部或边界上,而且是面积最小的那种凸多边形。


二、常见的几种算法

  • Graham Scan

  • Andrew 算法

项目Graham ScanAndrew 算法
复杂度O(n log n)O(n log n)
是否极角排序✅ 是❌ 否
代码结构稍复杂更直观
适合手写✅ 比较好✅ 非常好

三、用到的数学知识

1、叉积(Cross Product)

在二维空间中,叉积通常被定义为两个向量的“伪叉积”,其结果是一个标量(而非三维空间中的向量),可用于判断向量方向或计算平行四边形面积。

数学公式:设向量 a ⃗ = ( x 1 , y 1 ) \vec{a} = (x_1, y_1) a =(x1,y1),向量 b ⃗ = ( x 2 , y 2 ) \vec{b} = (x_2, y_2) b =(x2,y2),则二者的叉积为:

a ⃗ × b ⃗ = x 1 y 2 − x 2 y 1 \vec{a} \times \vec{b} = x_1 y_2 - x_2 y_1 a ×b =x1y2x2y1

简单讲解:

  • 结果的正负表示两向量的相对方向:正号表示 a ⃗ \vec{a} a b ⃗ \vec{b} b 顺时针方向,负号则相反。
  • 结果的绝对值等于两向量构成的平行四边形面积(也等于以两向量为邻边的三角形面积的 2 倍)。

2、欧几里得距离(Euclidean Distance)

二维空间中,欧几里得距离即两点之间的直线距离,是最常用的距离度量方式。

数学公式:设平面上两点 A ( x 1 , y 1 ) A(x_1, y_1) A(x1,y1) B ( x 2 , y 2 ) B(x_2, y_2) B(x2,y2),则两点间的欧几里得距离为:

d ( A , B ) = ( x 2 − x 1 ) 2 + ( y 2 − y 1 ) 2 d(A, B) = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2} d(A,B)=(x2x1)2+(y2y1)2


三、Graham Scan

📌步骤一:找最低点

  • 找到 y 最小的点,若 y 相同,取 x 最小的。

  • 这个点作为 起点 P0

📌步骤二:按极角排序

  • 把其他点按相对于 P0 的 极角(从 x 轴逆时针旋转的角度)排序

📌步骤三:遍历所有点,构建凸包

  • 用一个栈来存凸包点。

  • 每加一个点时,检查是否左转

    • 若不是左转(即右转或共线),就 弹出栈顶;

    • 一直这样弹,直到形成左转。

Java 代码实现

三个点一起看,就很好理解

import java.util.*;

public class ConvexHull {
    static class Point {
        int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
    }

    public static List<Point> convexHull(Point[] points) {
        if (points.length <= 1) return Arrays.asList(points);

        // 1. 找到最低点
        Point start = points[0];
        for (Point p : points) {
            if (p.y < start.y || (p.y == start.y && p.x < start.x)) {
                start = p;
            }
        }

        // 2. 按极角排序(从小到大)
	    // 正数,b在前
        Arrays.sort(points, (a, b) -> {
            int cp = cross(start, a, b);
            if (cp == 0) return dist(start, a) - dist(start, b);
            return -cp;
        });

        // 3. 构建凸包
        Stack<Point> stack = new Stack<>();
        for (Point p : points) {
            while (stack.size() >= 2 &&
                   cross(stack.get(stack.size() - 2), stack.peek(), p) <= 0) {
                stack.pop(); // 右转 or 共线
            }
            stack.push(p);
        }

        return new ArrayList<>(stack);
    }

	// 计算的是向量 ab × ac 的二维伪叉积
	// > 0:左转(保持凸性,保留)
	// = 0:共线(可能去掉)
	// < 0:右转(破坏凸性,需要弹出)
    
    static int cross(Point a, Point b, Point c) {
        return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
    }

	// 由于只需要正负,计算两点之间的距离的平方
    static int dist(Point a, Point b) {
        return (b.x - a.x)*(b.x - a.x) + (b.y - a.y)*(b.y - a.y);
    }
}

推荐观看:Graham算法构造凸包(b站上的视频,有图示)


四、Andrew 算法(也叫Monotone Chain)

📌步骤一:按照 x 再 y 排序

Arrays.sort(points, (a, b) -> a.x == b.x ? a.y - b.y : a.x - b.x);

从左到右,从下到上排。

📌步骤二:构造下凸包

Stack<Point> lower = new Stack<>();
// 构建下凸壳
for (Point p : points) {
	while (lower.size() >= 2 &&
		   cross(lower.get(lower.size() - 2), lower.peek(), p) <= 0) {
		lower.pop();
	}
	lower.push(p);
}

📌步骤三:构造上凸包(注意从后往前)

Stack<Point> upper = new Stack<>();
// 构建上凸壳
for (int i = n - 1; i >= 0; i--) {
	Point p = points[i];
	while (upper.size() >= 2 &&
		   cross(upper.get(upper.size() - 2), upper.peek(), p) <= 0) {
		upper.pop();
	}
	upper.push(p);
}

📌步骤四:拼接上下凸包

lower.pop(); // 去掉最高点 
upper.pop(); // 去掉最低点 
while (!upper.isEmpty()) lower.push(upper.pop());

Java 代码实现

import java.util.*;

public class ConvexHull {
    static class Point {
        int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
    }

    public static List<Point> convexHull(Point[] points) {
        int n = points.length;
        if (n <= 1) return Arrays.asList(points);

        // 1. 先按照 x 升序,若 x 相同则按 y 升序排列
        Arrays.sort(points, (a, b) -> a.x == b.x ? a.y - b.y : a.x - b.x);

        Stack<Point> lower = new Stack<>();
        // 构建下凸壳
        for (Point p : points) {
            while (lower.size() >= 2 &&
                   cross(lower.get(lower.size() - 2), lower.peek(), p) <= 0) {
                lower.pop();
            }
            lower.push(p);
        }

        Stack<Point> upper = new Stack<>();
        // 构建上凸壳
        for (int i = n - 1; i >= 0; i--) {
            Point p = points[i];
            while (upper.size() >= 2 &&
                   cross(upper.get(upper.size() - 2), upper.peek(), p) <= 0) {
                upper.pop();
            }
            upper.push(p);
        }

        // 合并上下壳(去除重复的首尾点)
        lower.pop(); // 去掉最高点
        upper.pop(); // 去掉最低点
        while (!upper.isEmpty()) lower.push(upper.pop());

        return new ArrayList<>(lower);
    }

    // 计算叉积:向量 ab × ac
    static int cross(Point a, Point b, Point c) {
        return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
    }
}

推荐观看:凸包 Andrew算法(b站上的视频,有图示)


五、练习题建议(LeetCode / 牛客网)

  1. LeetCode 587. 安装栅栏

  2. 牛客网 / 洛谷:凸包模板题


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值