Welzl 算法的迭代版本

问题描述 投票:0回答:3

我正在使用 Welzl 算法来查找点云的最小外接圆 (2d) 或最小外接球 (3d)。不幸的是,该算法具有非常高的递归深度,即输入点数。这个算法有迭代版本吗?我找不到任何并且不知道如何将递归更改为循环。

我发现了一些迭代最小外接圆/球算法,但它们的工作方式完全不同,并且没有 Welzl 算法预期的线性运行时间。

algorithm
3个回答
2
投票

打乱点数组 P[0…n−1]。令 R = ∅ 且 i = 0。直到 i = n,

  • 如果 P[i] ∈ R 或 R 定义一个包含 P[i] 的圆,则设 i ← i+1.
  • 否则,设 R ← {P[i]} ∪ (R ∩ {P[i+1], …, P[n−1]})。如果 |R| < 3, then set i ← 0, else set i ← i+1.

返回R。

在 Python 3 中测试实现:

from itertools import combinations
from random import randrange, shuffle


def mag_squared(v):
    return v.real ** 2 + v.imag ** 2


def point_in_circle(p, circle):
    if not circle:
        return False
    if len(circle) == 1:
        (q,) = circle
        return q == p
    if len(circle) == 2:
        q, r = circle
        return mag_squared((p - q) + (p - r)) <= mag_squared(q - r)
    if len(circle) == 3:
        q, r, s = circle
        # Translate p to the origin.
        q -= p
        r -= p
        s -= p
        # Orient the triangle counterclockwise.
        qr = r - q
        qs = s - q
        a, b = qr.real, qr.imag
        c, d = qs.real, qs.imag
        determinant = a * d - b * c
        assert determinant != 0
        if determinant < 0:
            r, s = s, r
        # Test whether the origin lies in the circle.
        a, b, c = q.real, q.imag, mag_squared(q)
        d, e, f = r.real, r.imag, mag_squared(r)
        g, h, i = s.real, s.imag, mag_squared(s)
        determinant = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g)
        return determinant >= 0
    assert False, str(circle)


def brute_force(points):
    for k in range(4):
        for circle in combinations(points, k):
            if any(
                point_in_circle(p, circle[:i] + circle[i + 1 :])
                for (i, p) in enumerate(circle)
            ):
                continue
            if all(point_in_circle(p, circle) for p in points):
                return circle
    assert False, str(points)


def welzl_recursive(points, required=()):
    points = list(points)
    if not points or len(required) == 3:
        return required
    p = points.pop(randrange(len(points)))
    circle = welzl_recursive(points, required)
    if point_in_circle(p, circle):
        return circle
    return welzl_recursive(points, required + (p,))


def welzl(points):
    points = list(points)
    shuffle(points)
    circle_indexes = {}
    i = 0
    while i < len(points):
        if i in circle_indexes or point_in_circle(
            points[i], [points[j] for j in circle_indexes]
        ):
            i += 1
        else:
            circle_indexes = {j for j in circle_indexes if j > i}
            circle_indexes.add(i)
            i = 0 if len(circle_indexes) < 3 else i + 1
    return [points[j] for j in circle_indexes]


def random_binomial():
    return sum(2 * randrange(2) - 1 for i in range(100))


def random_point():
    return complex(random_binomial(), random_binomial())


def test(implementation):
    for rep in range(1000):
        points = [random_point() for i in range(randrange(20))]
        expected = set(brute_force(points))
        for j in range(10):
            shuffle(points)
            got = set(implementation(points))
            assert expected == got or (
                all(point_in_circle(p, expected) for p in got)
                and all(point_in_circle(p, got) for p in expected)
            )


def graphics():
    points = [random_point() for i in range(100)]
    circle = welzl(points)
    print("%!PS")
    print(0, "setlinewidth")
    inch = 72
    print(8.5 * inch / 2, 11 * inch / 2, "translate")
    print(inch / 16, inch / 16, "scale")
    for p in points:
        print(p.real, p.imag, 1 / 4, 0, 360, "arc", "stroke")
    for p in circle:
        print(p.real, p.imag, 1 / 4, 0, 360, "arc", "fill")
    print("showpage")


test(welzl_recursive)
test(welzl)
graphics()

1
投票

Welzl 的 Python 3.10+ 算法。这使用显式堆栈而不是递归。下面只是

Circle
类的一种方法。完整代码位于其下面的下一个代码视图中。

    @staticmethod
    def from_points(points: list[Point]) -> 'Circle':
        random.shuffle(points)

        # Algorithm stages.
        First   = 0
        Second  = 1

        stack   = [(First, [], len(points))]
        results = []

        while stack:
            params = stack.pop()
            if params[0] is First:
                r = params[1]
                n = params[2]
                if n == 0 or len(r) == 3:
                    match len(r):
                        case 3: c1 = Circle.from_3_points(r[0], r[1], r[2])
                        case 2: c1 = Circle.from_2_points(r[0], r[1])
                        case 1: c1 = Circle(r[0], 0)
                        case 0: c1 = Circle(Point(0, 0), 0)
                        case _: raise ValueError("Invalid number of points")
                    results.append(c1)
                else:
                    idx = random.randint(0, n - 1)
                    p1  = points[idx]
                    points[idx], points[n - 1] = points[n - 1], points[idx]
                    stack.append((Second, r.copy(), n, p1))
                    stack.append((First, r, n - 1))
            elif params[0] is Second:
                r  = params[1]
                n  = params[2]
                p1 = params[3]
                c1 = results.pop()
                if c1.contains_point(p1):
                    results.append(c1)
                else:
                    r.append(p1)
                    stack.append((First, r, n - 1))
            else:                
                raise ValueError("Invalid stage")

        return results.pop()

完整代码:


from dataclasses import dataclass
import random

@dataclass
class Point:
    x: float
    y: float

    def distance(self, other: 'Point') -> float:
        return ((self.x - other.x)**2 + (self.y - other.y)**2)**0.5

@dataclass
class Circle:
    center: Point
    radius: float

    @staticmethod
    def from_points(points: list[Point]) -> 'Circle':
        """Return the smallest circle that encloses the given points.

        This function uses Welzl's algorithm to find the smallest circle that
        encloses the given points.

        Args:
            points: list of points

        Side effects:
            The order of the points in the list is randomized.

        Returns:
            Circle: the smallest circle that encloses the given points.
        """
        random.shuffle(points)

        # Algorithm stages.
        First   = 0
        Second  = 1

        stack   = [(First, [], len(points))]
        results = []

        while stack:
            params = stack.pop()
            if params[0] is First:
                r = params[1]
                n = params[2]
                if n == 0 or len(r) == 3:
                    match len(r):
                        case 3: c1 = Circle.from_3_points(r[0], r[1], r[2])
                        case 2: c1 = Circle.from_2_points(r[0], r[1])
                        case 1: c1 = Circle(r[0], 0)
                        case 0: c1 = Circle(Point(0, 0), 0)
                        case _: raise ValueError("Invalid number of points")
                    results.append(c1)
                else:
                    idx = random.randint(0, n - 1)
                    p1  = points[idx]
                    points[idx], points[n - 1] = points[n - 1], points[idx]
                    stack.append((Second, r.copy(), n, p1))
                    stack.append((First, r, n - 1))
            elif params[0] is Second:
                r  = params[1]
                n  = params[2]
                p1 = params[3]
                c1 = results.pop()
                if c1.contains_point(p1):
                    results.append(c1)
                else:
                    r.append(p1)
                    stack.append((First, r, n - 1))
            else:                
                raise ValueError("Invalid stage")

        return results.pop()

    @staticmethod
    def from_3_points(p1, p2, p3):
        """Calculate the circle passing through 3 points."""  
        p12 = Point(p2.x - p1.x, p2.y - p1.y)
        p13 = Point(p3.x - p1.x, p3.y - p1.y)

        c12  = p12.x * p12.x + p12.y * p12.y
        c13  = p13.x * p13.x + p13.y * p13.y
        c123 = p12.x * p13.y - p12.y * p13.x

        cx = (p13.y * c12 - p12.y * c13) / (2. * c123)
        cy = (p12.x * c13 - p13.x * c12) / (2. * c123)

        center = Point(cx + p1.x, cy + p1.y)

        return Circle(center, center.distance(p1))
    
    @staticmethod
    def from_2_points(p1: Point, p2: Point) -> 'Circle':
        """Return the circle passing through the two points."""
        center_x = (p1.x + p2.x) / 2
        center_y = (p1.y + p2.y) / 2
        center = Point(center_x, center_y)
        radius = center.distance(p1)

        return Circle(center, radius)
    
    def contains_point(self, point: Point) -> bool:
        """Return True if the circle contains the given point."""
        return self.center.distance(point) <= self.radius

0
投票

你可以这样理解迭代版本:

  • 你可以用集合中的两个或三个点形成一个非平凡的最小外接圆;

  • 发现位于该圆内部或之上的任何点都可以从进一步搜索中丢弃,因为它不能与这两个或三个形成更小的圆。

因此可以使用以下程序:

  • 选择一个点;

  • A) 选择第二个点并构建包围这两个点的最小圆;

  • B)如果所有剩余点都是内部的,则完成;

  • 取一个外点并构建包围这三个点的最小圆;如果这个圆经过两点,则转到 B

  • 如果所有剩余点都是内部的,则完成;

  • 取一个外部点并前往A。

© www.soinside.com 2019 - 2024. All rights reserved.