1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

# теория · шаг 3 из 7

Делоне и Вороной: двойственность, параболоид и что лежит внутри Делоне

Коротко

Триангуляция Делоне и диаграмма Вороного - одно и то же, нарисованное по-разному. Две точки соединены ребром Делоне ровно тогда, когда их ячейки Вороного имеют общее ребро (в общем положении); треугольник Делоне - вершина Вороного, центр его описанной окружности. Ещё один взгляд: подними точки на параболоид z = x² + y² - и триангуляция Делоне окажется проекцией нижней выпуклой оболочки поднятых точек, а определитель «в окружности» - поворотом в трёх измерениях. И Делоне содержит полезные графы: каждая точка соединена в ней со своим ближайшим соседом, а минимальное остовное дерево точек лежит целиком среди рёбер Делоне.

Если ты знаешь, как Делоне двойственна Вороному, почему Делоне - нижняя оболочка точек на параболоиде и почему минимальное остовное дерево лежит в Делоне, листай до «Теперь сам».

Как думаешь, какие рёбра нужны дереву

Двадцать точек - и надо соединить их сетью наименьшей суммарной длины: кабелями, трубами, дорогами. Это минимальное остовное дерево, и алгоритм Крускала из главы о графах строит его по списку рёбер. Только рёбер-кандидатов здесь - все пары точек, 190 штук, а для миллиона точек - полтриллиона.

Ответь себе до чтения: может быть, многие пары можно выбросить заранее, не рискуя потерять ребро дерева? Какие? Ответ урока: достаточно 49 рёбер Делоне - дерево получится то же. В общем случае 3n − 3 − h (h - точек на оболочке) вместо n²/2.

На что это похоже

Карта стран и карта столиц. Карта Вороного - это страны: у каждой столицы своя территория. Делоне - это сеть дорог между столицами соседних стран: если у двух стран есть общая граница, между их столицами проложена дорога. По одной карте другую восстановить можно сразу: границы знают, кто сосед, а дороги знают, где проходит граница - посередине и поперёк.

Механизм: двойственность и подъём на параболоид

Двойственность. Ребро Делоне pq существует, если есть пустая окружность через p и q. Центр такой окружности равноудалён от p и q и не ближе ни к кому - то есть лежит на ребре Вороного между ячейками p и q. Треугольник Делоне pqr - пустая окружность через три точки, а её центр - вершина Вороного. Отсюда построить Вороного из Делоне - это посчитать центры описанных окружностей и соединить центры соседних треугольников. Всё это точно в общем положении: у вершин квадрата оба треугольника дают одну вершину Вороного, а диагональ соединяет ячейки, которые касаются лишь в точке.

A B C D E
Пять отделений из урока о Вороном: сплошные линии - Делоне, пунктир - Вороной. Ребро Делоне соединяет центры соседних ячеек и перпендикулярно их общей границе; четыре треугольника - четыре вершины Вороного.

Параболоид. Поднимем каждую точку (x, y) в (x, y, x² + y²). Окружность на плоскости x² + y² = αx + βy + γ поднимается в сечение параболоида плоскостью z = αx + βy + γ, и точка внутри окружности поднимается под эту плоскость. Значит, «окружность через p, q, r пуста» означает: плоскость через поднятые p, q, r оставляет все остальные поднятые точки над собой - это грань нижней выпуклой оболочки. Триангуляция Делоне - проекция нижней оболочки поднятых точек. Отсюда два следствия: определитель «в окружности» - это поворот в трёх измерениях, а Делоне в d измерениях - это оболочка в d + 1. Так её и строит scipy.spatial.Delaunay: через оболочку в библиотеке Qhull.

Что лежит в Делоне. Ближайший сосед q точки p: окружность с диаметром pq пуста - иначе внутри нашлась бы точка ближе q. Значит, pq - ребро Делоне. Ребро минимального остовного дерева pq: если бы в круге с диаметром pq лежала точка r, то pr и rq обе короче pq, и замена pq на одно из них дала бы дерево легче. Значит, круг пуст, и pq - ребро Делоне. При попарно разных расстояниях цепочка вложений: граф ближайших соседей ⊆ остовное дерево ⊆ граф Габриеля (рёбра с пустым кругом на диаметре) ⊆ Делоне.

Правило

Делоне двойственна Вороному: рёбра Делоне соединяют соседние ячейки, треугольники соответствуют вершинам. Делоне - проекция нижней оболочки точек, поднятых на z = x² + y², а «в окружности» - поворот в трёх измерениях. Делоне содержит граф ближайших соседей и минимальное остовное дерево, поэтому дерево строится Крускалом по 3n − 3 − h её рёбрам за O(n · log n).

Разбор: определитель окружности - это поворот в пространстве

Проверим тождество на двухстах случайных четвёрках точек: определитель «в окружности» и трёхмерный поворот поднятых точек.

import random

def v_kruge(a, b, c, d):
    m = []
    for p in [a, b, c]:
        dx = p[0] - d[0]
        dy = p[1] - d[1]
        m.append([dx, dy, dx * dx + dy * dy])
    return (m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
            - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
            + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]))

def podnyat(p):
    # точка плоскости - на параболоид z = x² + y²
    return [p[0], p[1], p[0] * p[0] + p[1] * p[1]]

def povorot3(a, b, c, d):
    # знак: по какую сторону от плоскости abc лежит d
    m = []
    for p in [a, b, c]:
        m.append([p[0] - d[0], p[1] - d[1], p[2] - d[2]])
    return (m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
            - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
            + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]))

random.seed(9)
sovpalo = 0
vnutri = 0
for t in range(200):
    ch = []
    for k in range(4):
        ch.append([random.randint(-50, 50), random.randint(-50, 50)])
    a, b, c, d = ch
    x = v_kruge(a, b, c, d)
    y = povorot3(podnyat(a), podnyat(b), podnyat(c), podnyat(d))
    if x == y:
        sovpalo = sovpalo + 1
    if x > 0:
        vnutri = vnutri + 1
print(sovpalo, vnutri)
print(v_kruge([0, 5], [10, 0], [20, 5], [10, 10]))
print(povorot3(podnyat([0, 5]), podnyat([10, 0]), podnyat([20, 5]),
               podnyat([10, 10])))
200 90
15000
15000

Двести из двухсот - значения совпали до единицы. Это не совпадение знаков, а равенство чисел: в столбце определителя «в окружности» стоит (ax − dx)² + (ay − dy)², а у поднятых точек - (ax² + ay²) − (dx² + dy²). Разница между ними - −2dx(ax − dx) − 2dy(ay − dy), то есть сумма первых двух столбцов с коэффициентами, и определитель от этого не меняется. Девяносто положительных значений - те четвёрки, где d внутри окружности при обходе a, b, c против часовой стрелки или снаружи при обходе по часовой. Последние две строки - ромб из прошлого урока: те же 15 000.

Разбор: остовное дерево по рёбрам Делоне

Двадцать точек из прошлых уроков, Делоне переворотами, затем Крускал дважды: по рёбрам Делоне и по всем парам.

import random

def povorot(a, b, c):
    return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])

def obolochka(tochki):
    t = []
    for i in range(len(tochki)):
        t.append([tochki[i][0], tochki[i][1], i])
    t.sort()
    niz = []
    for p in t:
        while len(niz) >= 2 and povorot(niz[-2], niz[-1], p) <= 0:
            niz.pop()
        niz.append(p)
    verh = []
    for p in t[::-1]:
        while len(verh) >= 2 and povorot(verh[-2], verh[-1], p) <= 0:
            verh.pop()
        verh.append(p)
    otvet = []
    for p in niz[:-1] + verh[:-1]:
        otvet.append(p[2])
    return otvet

def triangulirovat(tochki):
    H = obolochka(tochki)
    treug = []
    for k in range(1, len(H) - 1):
        treug.append([H[0], H[k], H[k + 1]])
    for i in range(len(tochki)):
        if i in H:
            continue
        p = tochki[i]
        for k in range(len(treug)):
            a, b, c = treug[k]
            if povorot(tochki[a], tochki[b], p) > 0:
                if povorot(tochki[b], tochki[c], p) > 0:
                    if povorot(tochki[c], tochki[a], p) > 0:
                        treug[k] = [a, b, i]
                        treug.append([b, c, i])
                        treug.append([c, a, i])
                        break
    return [H, treug]
def v_kruge(a, b, c, d):
    m = []
    for p in [a, b, c]:
        dx = p[0] - d[0]
        dy = p[1] - d[1]
        m.append([dx, dy, dx * dx + dy * dy])
    return (m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
            - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
            + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]))

def perevernut_odno(tochki, treug):
    rebro = {}
    for k in range(len(treug)):
        a, b, c = treug[k]
        rebro[a * 10000 + b] = k
        rebro[b * 10000 + c] = k
        rebro[c * 10000 + a] = k
    for k in range(len(treug)):
        for s in range(3):
            u = treug[k][s]
            v = treug[k][(s + 1) % 3]
            x = treug[k][(s + 2) % 3]
            klyuch = v * 10000 + u
            if klyuch not in rebro:
                continue
            m = rebro[klyuch]
            w = treug[m][0] + treug[m][1] + treug[m][2] - u - v
            if v_kruge(tochki[u], tochki[v], tochki[x], tochki[w]) > 0:
                treug[k] = [u, w, x]
                treug[m] = [w, v, x]
                return True
    return False

def delone_perevorotami(tochki, treug):
    perevorotov = 0
    while perevernut_odno(tochki, treug):
        perevorotov = perevorotov + 1
    return perevorotov
def kv(p, q):
    return (p[0] - q[0]) ** 2 + (p[1] - q[1]) ** 2

def kraskal(n, rebra, tochki):
    # дерево Крускала: рёбра по длине, лес предков против циклов;
    # сравниваем квадраты длин - дерево то же, вес - сумма квадратов
    spisok = []
    for u, v in rebra:
        spisok.append([kv(tochki[u], tochki[v]), u, v])
    spisok.sort()
    roditel = list(range(n))
    ves = 0
    vybrano = []
    for d, u0, v0 in spisok:
        u = u0
        v = v0
        while roditel[u] != u:
            u = roditel[u]
        while roditel[v] != v:
            v = roditel[v]
        if u != v:
            roditel[u] = v
            ves = ves + d
            vybrano.append([u0, v0])
    return [ves, vybrano]

random.seed(4)
tochki = []
while len(tochki) < 20:
    p = [random.randint(0, 100), random.randint(0, 100)]
    if p not in tochki:
        tochki.append(p)
n = len(tochki)
H, treug = triangulirovat(tochki)
delone_perevorotami(tochki, treug)
rebra_d = {}
for a, b, c in treug:
    for u, v in [[a, b], [b, c], [c, a]]:
        rebra_d[min(u, v) * 1000 + max(u, v)] = [min(u, v), max(u, v)]
vse = []
for u in range(n):
    for v in range(u + 1, n):
        vse.append([u, v])
print(len(rebra_d), len(vse))
po_delone = kraskal(n, list(rebra_d.values()), tochki)
polnoe = kraskal(n, vse, tochki)
print(po_delone[0], len(po_delone[1]), po_delone == polnoe)
# ближайший сосед каждой точки - ребро Делоне?
v_delone = 0
for u in range(n):
    luchshij = -1
    for v in range(n):
        if v != u:
            d = kv(tochki[u], tochki[v])
            if luchshij == -1 or d < d_min:
                luchshij = v
                d_min = d
    if min(u, luchshij) * 1000 + max(u, luchshij) in rebra_d:
        v_delone = v_delone + 1
print(v_delone, n)
49 190
5704 19 True
20 20

Рёбер Делоне 49, пар 190. Деревья совпали ребро в ребро (True - списки выбранных рёбер равны): 19 рёбер, сумма квадратов длин 5704. Крускал сравнивает квадраты, дерево от этого не меняется, потому что квадрат возрастает вместе с длиной. И у каждой из двадцати точек ребро до ближайшего соседа оказалось ребром Делоне.

Сколько это экономит. Для миллиона точек пар 5 · 10¹¹, рёбер Делоне около 3 · 10⁶. Делоне строится за n · log n, Крускал по её рёбрам - тоже n · log n. Граница приёма: он только для евклидовых расстояний на плоскости (или в малой размерности). Если длина ребра - цена по дорогам, вложение в Делоне не выполняется.

Что ломается без этого

Вороной из Делоне без учёта бесконечных ячеек. Треугольники дают только конечные вершины; рёбра, уходящие в бесконечность, соответствуют рёбрам оболочки Делоне, и их направление - перпендикуляр к ребру оболочки наружу.

Остовное дерево по «близким парам» с порогом расстояния. Порог либо теряет рёбра дерева между дальними кластерами, либо оставляет почти все пары. Делоне - точный способ ничего не потерять.

Перенос на дорожную сеть. На графе дорог Делоне не поможет: расстояние другое, и вложение держится только на геометрии кругов.

Зачем это в работе

Минимальное дерево и кластеризация: разрезать самые длинные рёбра остовного дерева - это одиночная связь в иерархической кластеризации, и Делоне делает её быстрой. Восстановление поверхности по облаку точек и объёмные сетки. И подъём на параболоид - общий приём: задачу про окружности на плоскости он превращает в задачу про плоскости в пространстве, где работают уже известные алгоритмы оболочки.

Теперь сам

  1. Сколько вершин у диаграммы Вороного 100 точек, если их Делоне имеет 190 треугольников?
  2. Почему ребро между точкой и её ближайшим соседом - всегда ребро Делоне?
  3. Поднимем точки (0, 0), (2, 0), (0, 2) и (1, 1) на параболоид. Где окажется (1, 1) относительно плоскости через три первые - выше или ниже - и что это значит?

Ответ. 1. 190 - по вершине на треугольник (в общем положении: четырёх точек на одной окружности нет).

  1. Окружность с диаметром от точки до её ближайшего соседа пуста: точка внутри неё была бы ближе соседа. Пустая окружность через две точки - и есть условие ребра Делоне.

  2. Поднятые точки: (0, 0, 0), (2, 0, 4), (0, 2, 4). Плоскость через них z = 2x + 2y, над точкой (1, 1) её высота 4, а сама точка поднимается в (1, 1, 2) - ниже плоскости. Значит, (1, 1) внутри окружности через три точки (это окружность с центром (1, 1) - точка в самом центре).

Главное

Делоне, Вороной и нижняя оболочка точек на параболоиде - три записи одного объекта. Поэтому «в окружности» - это поворот в пространстве, а пустые окружности Делоне удерживают внутри себя и ближайших соседей, и минимальное остовное дерево.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.