二维凸包算法 Julia实现

问题描述:给定平面上 n n n 个点的集合 Q Q Q,求其子集 P P P 构成 Q Q Q 的凸包,即 ∀ p ∈ Q , ∃ p 0 , p 1 , p 2 ∈ P \forall p \in Q, \exist p_0, p_1, p_2 \in P pQ,p0,p1,p2P 使得点 p p p 在以点 p 0 , p 1 , p 2 p_0, p_1, p_2 p0,p1,p2 构成的三角形中或边上。

朴素枚举

初始设 P : = Q P := Q P:=Q,枚举 p 0 , p 1 , p 2 , p p_0, p_1, p_2, p p0,p1,p2,p,判断 p p p 是否在以点 p 0 , p 1 , p 2 p_0, p_1, p_2 p0,p1,p2 构成的三角形中,是则删去 p p p,最后剩下的即为凸包。注意到纵坐标最小的点必在结果中,可将 p 0 p_0 p0 固定为之。

算法时间复杂度 O ( n 3 ) O(n^3) O(n3)。代码:

cross(p, q) = p[1] * q[2] - q[1] * p[2]

function NE(Q)
    function inΔ(p0, p1, p2, p)
        v0, v1, v2 = p .- p0, p1 .- p0, p2 .- p0
        c0, c1, c2 = cross(v1, v2), cross(v0, v1), cross(v0, v2)
        return c0 * c1 < 0 && c0 * c2 > 0 && abs(c2 - c1) < abs(c0)
    end
    m = argmin(last.(Q))
    p0, Q = Q[m], vcat(Q[begin : m - 1], Q[m + 1 : end])
    for i = length(Q) : -1 : 1
        f = false
        for j = 2 : length(Q)
            if i == j continue end
            for k = 1 : j - 1
                if i == k continue end
                if inΔ(p0, Q[j], Q[k], Q[i])
                    f = true; break
                end
            end
            if f break end
        end
        if f
            deleteat!(Q, i)
        end
    end
    sort!(Q, lt = (p, q) -> cross(p .- p0, q .- p0) > 0)
    return vcat(p0, Q)
end

Jarvis March 算法

按逆时针对 P P P 排序, p 0 p_0 p0 固定为纵坐标最小的点,将问题转化成已知 p 0 , p 1 , . . . , p k p_0, p_1, ..., p_k p0,p1,...,pk 如何求 p k + 1 p_{k+1} pk+1

此时 Q − { p k } Q - \{p_k\} Q{pk} 中所有点都在向量 p k − p k − 1 p_k - p_{k-1} pkpk1 的左侧,即向量集 { p − p k ∣ p ∈ Q − { p k } } \{ p - p_k | p \in Q - \{ p_k \} \} {ppkpQ{pk}} 可在叉积运算的正负性上构成偏序集,最右的向量即为 p k + 1 − p k p_{k+1} - p_k pk+1pk。直白点说,为了使得 Q − { p k + 1 } Q - \{p_{k+1}\} Q{pk+1} 中所有点都在向量 p k + 1 − p k p_{k+1} - p_k pk+1pk 的左侧,就要从向量集 { p − p k ∣ p ∈ Q − { p k } } \{ p - p_k | p \in Q - \{ p_k \} \} {ppkpQ{pk}} 中选最右的向量作为 p k + 1 − p k p_{k+1} - p_k pk+1pk。当 p k + 1 = p 0 p_{k+1} = p_0 pk+1=p0 时算法终止。

时间复杂度 O ( n h ) O(nh) O(nh) h h h 为凸包的点数。代码:

function Jarvis(Q)
    function march(A, cmp)
        r = A[begin]
        for i in A[2 : end]
            if cmp(i, r)
                r = i
            end
        end
        return r
    end
    P = [march(Q, (p, q) -> last(p) < last(q))]
    while true
        q = march(Q, (p, q) -> cross(p .- P[end], q .- P[end]) > 0)
        if P[begin] == q
            break
        end
        push!(P, q)
    end
    return P
end

Graham Scan 算法

p 0 p_0 p0 固定为纵坐标最小的点。按与 p 0 p_0 p0 构成向量的极角序对 Q Q Q 排序,将问题转化为已知 Q Q Q 中前 k k k 个点的凸包 P k P_k Pk,如何求 Q Q Q 中前 k + 1 k + 1 k+1 个点的凸包 P k + 1 P_{k+1} Pk+1

不妨设 P k = { p 0 , p 1 , . . , p k } P_k = \{ p_0, p_1, .., p_k \} Pk={p0,p1,..,pk},注意到 p k + 1 p_{k+1} pk+1 的在 p k − p 0 p_k - p_0 pkp0 的左边,而 P k P_k Pk 中其它点在 p k − p 0 p_k - p_0 pkp0 的右边,故一定有 p k + 1 ∈ P k + 1 p_{k+1} \in P_{k+1} pk+1Pk+1,设 P k + 1 = P k + { p k + 1 } P_{k+1} = P_k + \{ p_{k+1} \} Pk+1=Pk+{pk+1}。判断 p k + 1 p_{k+1} pk+1 p k − p k − 1 p_k - p_{k-1} pkpk1 的哪边,若在右边则从凸包 P k + 1 P_{k+1} Pk+1 中抛出 p k p_k pk,继续迭代判断 p k + 1 p_{k+1} pk+1 p k − 1 − p k − 2 p_{k-1} - p_{k-2} pk1pk2 的哪边,直到在左边则结束迭代,最后剩下的即为凸包 P k + 1 P_{k+1} Pk+1 P ∣ Q ∣ P_{\left| Q \right|} PQ 即为所求的 P P P

用栈存储中间过程的凸包计算, Q Q Q 中每个点仅会进出栈一次,故扫描为线性时间,主要耗时在排序。时间复杂度 O ( n log ⁡ n ) O(n \log n) O(nlogn)。代码:

function GrahamScan(Q)
    P = Q[1 : 3]
    for q in Q[4 : end]
        while cross(q .- P[end], P[end] .- P[end - 1]) > 0
            pop!(P)
        end
        push!(P, q)
    end
    return P
end

function Graham(Q)
    m = argmin(last.(Q))
    p0, Q = Q[m], vcat(Q[begin : m - 1], Q[m + 1 : end])
    Q = sort(Q, lt = (p, q) -> cross(p .- p0, q .- p0) > 0)
    return GrahamScan(vcat(p0, Q))
end

分治算法

将当前点集均分为左右两部分,分别用分治算法得到各自的凸包,再将两个凸包合并成一个凸包。

合并过程是,取一边凸包上某一点作为基点,求另一边凸包上极角最大和最小的点,则一边凸包的点按基点的极角序有序,另一边凸包的点分为两条从极角最小到极角最大的极角序有序点集,将三个极角序有序点集按三路归并成一个极角序有序的点集,并进行 Graham Scan 算法流程。

时间函数 T ( n ) = 2 T ( n 2 ) + O ( n ) T(n) = 2 T(\frac{n}{2}) + O(n) T(n)=2T(2n)+O(n),得时间复杂度 O ( n log ⁡ n ) O(n \log n) O(nlogn),但常数很大。代码:

function DC(Q)
    function argMaxMin(A, cmp)
        u, v = 1, 1
        for i = 2 : length(A)
            if cmp(A[i], A[u]) > 0
                u = i
            end
            if cmp(A[i], A[v]) < 0
                v = i
            end
        end
        return u, v
    end
    function merge3(A, B, C, cmp)
        ia, ib, ic = length(A), length(B), length(C)
        fa, fb, fc, R::typeof(A) = ia > 0, ib > 0, ic > 0, []
        while fa || fb || fc
            if fa && (!fb || cmp(A[ia], B[ib]) < 0) && (!fc || cmp(A[ia], C[ic]) < 0)
                push!(R, A[ia]); ia -= 1; fa = ia > 0
            elseif fb && (!fc || cmp(B[ib], C[ic]) < 0)
                push!(R, B[ib]); ib -= 1; fb = ib > 0
            else
                push!(R, C[ic]); ic -= 1; fc = ic > 0
            end
        end
        return reverse(R)
    end
    function CH(Q)
        if length(Q) <= 3
            m = argmin(last.(Q))
            p0, Q = Q[m], vcat(Q[begin : m - 1], Q[m + 1 : end])
            if length(Q) == 2 && cross(Q[1] .- p0, Q[2] .- p0) < 0
                Q = reverse(Q)
            end
            return vcat(p0, Q)
        end
        m = length(Q) ÷ 2
        L, R = CH(Q[begin : m]), CH(Q[m + 1 : end])
        if L[1][2] > R[1][2]
            L, R = R, L
        end
        p0, L = L[1], L[2 : end]
        u, v = argMaxMin(R, (p, q) -> cross(p .- p0, q .- p0))
        slice(A, l, r) = l <= r ? A[l : r] : vcat(A[l : end], A[begin : r])
        R0, R1 = slice(R, u, v), slice(R, v, u)[end - 1 : -1 : begin + 1]
        return GrahamScan(vcat(p0, merge3(L, R0, R1, (p, q) -> cross(p .- p0, q .- p0))))
    end
    Q = sort(Q, by = first)
    return CH(Q)
end

实验测试

随机在 { ( x , y ) ∣ 0 ≤ x , y < 100 } \{ (x, y) | 0 ≤ x, y < 100 \} {(x,y)∣0x,y<100} 区域生成 3000 个点,测试各算法凸包结果和效率。代码:

using Random
function genData(n)
    Random.seed!(2024)
    return [q for q in zip(100 * rand(n), 100 * rand(n))]
end
Q = genData(3000)
@time P1 = NE(Q)
@time P2 = Jarvis(Q)
@time P3 = Graham(Q)
@time P4 = DC(Q)
@show length(P1)
@show length(P2), P2 == P1
@show length(P3), P3 == P1
@show length(P4), P4 == P1

结果:

  0.684359 seconds (585.47 k allocations: 40.423 MiB, 8.73% gc time, 53.56% compilation time)
  0.047316 seconds (27.23 k allocations: 2.719 MiB, 97.94% compilation time)
  0.175105 seconds (142.39 k allocations: 9.483 MiB, 3.61% gc time, 96.17% compilation time)
  0.412407 seconds (423.40 k allocations: 25.049 MiB, 98.20% compilation time)
length(P1) = 19
(length(P2), P2 == P1) = (19, true)
(length(P3), P3 == P1) = (19, true)
(length(P4), P4 == P1) = (19, true)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值