题目描述

:::fold-green 相关原题

✅ 判断一个点是否在三角形内部

牛客使用浮点坐标并输出 Yes 或 No,本文采用整数坐标并返回布尔值。

:::

给定三角形的三个顶点 a、b、c,以及待判断点 p,请判断 p 是否位于三角形内,返回布尔值。

这里整理为整数坐标版本:点位于边上或顶点上时返回 true;三个顶点共线、无法构成三角形时返回 false。

示例 1:

输入:a = [0,0], b = [4,0], c = [0,4], p = [1,1]
输出:true

示例 2:

输入:a = [0,0], b = [4,0], c = [0,4], p = [3,3]
输出:false

提示:

  • 点的位置由二维坐标表示,三角形顶点可以按顺时针或逆时针给出。
  • 坐标为整数,叉积计算中的乘积及差值在有符号 64 位整数范围内。
  • 来源文章使用浮点坐标;本文题面与下方代码统一采用整数坐标,不将整数实现当作浮点实现。

题意分析

给定三角形的三个顶点和一个待判断点,确定这个点是否位于三角形内部。本文把边上和顶点上的点也计为内部,三个顶点共线的退化情况返回 false。

实现使用整数坐标,按点相对三条有向边的位置判断,不需要计算斜率,因此竖直边也能正常处理。坐标范围需保证叉积中的乘积及最终差值能用有符号 64 位整数表示。

解法:向量叉积同号判断

核心思路

[!blue]

对起点为 o 的两条向量,叉积为 (a.x - o.x) × (b.y - o.y) - (a.y - o.y) × (b.x - o.x)。它的符号表示第二条向量位于第一条向量的哪一侧:正值在左侧,负值在右侧,零表示共线。

沿顶点顺序使用有向边 a → b、b → c、c → a。如果三个顶点逆时针排列,三角形内部同时位于三条边的左侧;顺时针排列时,内部同时位于三条边的右侧。三角形正是这三个内侧半平面的交集,因此只需检查待判断点是否同时满足三条边的侧向条件。

计算点对三条边的叉积。三个结果全非负或全非正时,点满足全部边的内部条件,返回真;同时出现正值和负值时,至少有一条边把它隔在外侧,返回假。统一允许两种符号方向,就不需要提前固定顶点必须按顺时针还是逆时针给出。

零叉积允许点落在边界,但仅与某一条边共线还不够,还要同时满足另外两条边的条件,才能排除延长线上的外部点。三角形自身若叉积为零,就不存在有效的内部区域,应先返回假,再进行点的判断。

计算时先把每个坐标转换为宽整数,再相减和相乘。若先在窄整数中求差,差值可能已经溢出,之后再转换类型也无法恢复正确结果。

解题步骤

  1. 计算三个顶点的叉积,等于零时返回 false。
  2. 分别计算 cross(a, b, p)、cross(b, c, p)、cross(c, a, p)。
  3. 记录三个结果中是否存在负值、是否存在正值。
  4. 两种符号同时出现则返回假,否则返回真,包括符合边界条件的零叉积情况。

代码实现

class Solution {
    static class Point {
        int x;
        int y;

        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    public boolean isPointInTriangle(Point a, Point b, Point c, Point p) {
        // 先排除三个顶点共线的退化三角形。
        if (cross(a, b, c) == 0) {
            return false;
        }

        // 三条有向边使用同一绕行方向,才能统一比较点位于哪一侧。
        long c1 = cross(a, b, p);
        long c2 = cross(b, c, p);
        long c3 = cross(c, a, p);

        // 只要相对三条边没有同时出现正负号,就在三角形内或边界上。
        boolean hasNeg = c1 < 0 || c2 < 0 || c3 < 0;
        boolean hasPos = c1 > 0 || c2 > 0 || c3 > 0;

        return !(hasNeg && hasPos);
    }

    private long cross(Point o, Point a, Point b) {
        // 先扩大每个坐标,再计算差与乘积,避免窄整数提前溢出。
        return ((long) a.x - o.x) * ((long) b.y - o.y) - ((long) a.y - o.y) * ((long) b.x - o.x);
    }
}
type Point struct {
    X int
    Y int
}

func isPointInTriangle(a, b, c, p Point) bool {
    // 先排除三个顶点共线的退化三角形。
    if cross(a, b, c) == 0 {
        return false
    }

    // 三条有向边使用同一绕行方向,才能统一比较点位于哪一侧。
    c1 := cross(a, b, p)
    c2 := cross(b, c, p)
    c3 := cross(c, a, p)

    // 只要相对三条边没有同时出现正负号,就在三角形内或边界上。
    hasNeg := c1 < 0 || c2 < 0 || c3 < 0
    hasPos := c1 > 0 || c2 > 0 || c3 > 0
    return !(hasNeg && hasPos)
}

func cross(o, a, b Point) int64 {
    // 先扩大每个坐标,再计算差与乘积,避免窄整数提前溢出。
    return (int64(a.X)-int64(o.X))*(int64(b.Y)-int64(o.Y)) - (int64(a.Y)-int64(o.Y))*(int64(b.X)-int64(o.X))
}

复杂度分析

  • 时间复杂度:$O(1)$。
  • 空间复杂度:$O(1)$。

关键点总结

[!green]

  • 三条边必须按同一绕行方向排列。
  • 零叉积对应边界,判断时保留等号的含义。

易错点总结

[!yellow]

  • 先用窄整数相减再转宽,无法修复已经发生的溢出。
  • 只接受全正或全负,会漏掉边界点。
  • 不排除共线顶点,会把退化情况误当作三角形。

相似题目

题目 难度 关联与区别
812. 最大三角形面积 简单 叉积给出有向面积,本题用各边的符号判断内外并把共线边界计为内部。
1232. 缀点成线 简单 共线判定对应叉积为0,本题还要比较三条有向边上的符号是否一致。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2022/19446425
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!