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

# теория · шаг 1 из 8

Где я нахожусь: локализация точки полосами и трапециями

Коротко

Локализация точки - по разбиению плоскости на области (районы, зоны, грани карты) и точке запроса назвать область, где точка лежит. Областей и запросов много, поэтому разбиение один раз превращают в структуру, а запросы отвечают за логарифм. Метод полос: провести вертикали через все вершины, внутри каждой полосы отрезки упорядочены по высоте, запрос - два двоичных поиска, O(log n). Цена - память: в худшем случае n². Трапецоидная карта, построенная вставкой отрезков в случайном порядке, даёт ожидаемо O(n) памяти и O(log n) на запрос.

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

Как думаешь, как ответить быстро

Карта города разбита на районы, границы - отрезки. Каждую секунду приходят тысячи вызовов такси с координатами, и для каждого надо назвать район. Проверять точку в каждом многоугольнике лучом из главы о примитивах - это O(n) на запрос, где n - число рёбер всех районов.

Ответь себе до чтения: если карта не меняется, а запросов миллионы, сколько можно заплатить один раз за подготовку, чтобы каждый запрос стоил логарифм? И чем за это расплачиваются? Ответ урока: двумя двоичными поисками, а расплачиваются памятью. На «лестнице» из 60 отрезков структура хранит 1860 записей.

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

Библиотека, где книги разложены по стеллажам, а на каждом стеллаже - по полкам. Чтобы найти книгу, не перебирают все книги: сначала по каталогу находят стеллаж, потом на нём - полку. Два поиска вместо перебора. Но если одна огромная книга занимает полку на всех стеллажах сразу, её карточку приходится класть в каталог каждого стеллажа - и каталог разбухает.

Полосы - стеллажи, отрезки внутри полосы - полки, а длинный отрезок, пересекающий много полос, - та огромная книга.

Механизм: полосы и упорядоченные отрезки

Разбиение задано отрезками-границами, которые не пересекаются (кроме общих концов). Область, где лежит точка, определяется отрезком прямо под ней: он и область над ним - одно и то же, если у каждого отрезка записано, какая область над ним (массив «номер отрезка → район»). Точка ровно на границе - договор: в программе ниже «выше» строгое, и такая точка относится к району под границей, как будто лежит чуть ниже её. Если под точкой отрезков нет вовсе, она во внешней области; обычно карту обносят рамкой, и тогда снизу отрезок есть всегда.

Метод полос (Добкин и Липтон, 1976). Через каждый конец отрезка проводится вертикаль. Между двумя соседними вертикалями - полоса, в которой нет ни одного конца. Значит, каждый отрезок либо пересекает полосу целиком, либо не касается её, и отрезки внутри полосы не пересекаются - их можно упорядочить снизу вверх.

запрос q:
  1. двоичный поиск по x вертикалей - полоса, где q        O(log n)
  2. двоичный поиск по отрезкам полосы: поворот            O(log n)
     «q выше отрезка?» - последний отрезок ниже q
q полоса находится двоичным поиском по x, отрезок под q - вторым поиском по высоте внутри полосы: в ней лежат только отрезки, пересекающие её целиком
Полоса - между соседними различными x концов, и в каждой хранятся все отрезки, пересекающие её целиком. Запрос q попадает в закрашенную полосу, и вторым поиском находится выделенный отрезок под ним.

Почему память квадратична. Полос до 2n, и в каждой может оказаться порядка n отрезков - если длинные отрезки тянутся над множеством концов коротких. Тогда записей порядка n².

Трапецоидная карта (Сейдел, 1991; Малмулей, 1990) чинит память. Из каждого конца отрезка проводят вертикаль не через всю плоскость, а только до ближайших отрезков сверху и снизу. Плоскость режется на трапеции, и их не больше 3n + 1: левая стенка каждой трапеции проходит через конец отрезка; левый конец задаёт не больше двух трапеций, правый - одну, и ещё одна самая левая. Высоты отрезков в полосе программа сравнивает как дроби с положительными знаменателями - перекрёстным умножением. Отрезки вставляют по одному в случайном порядке, обновляя карту и граф поиска - ориентированный граф проверок «левее / правее конца» и «выше / ниже отрезка». Ожидаемая память O(n), ожидаемое время запроса O(log n), построение O(n · log n). Случайный порядок здесь не украшение: для любого фиксированного порядка найдётся вход, где граф поиска глубокий, а при случайном порядке вставки плохой граф получается с малой вероятностью - для любого входа.

Правило

Локализация: подготовить разбиение один раз, отвечать на запрос за логарифм. Метод полос - вертикали через все концы, в полосе отрезки по высоте, запрос - два двоичных поиска за O(log n), память до O(n²). Трапецоидная карта со случайным порядком вставки - ожидаемо O(n) памяти и O(log n) на запрос.

Разбор: карта и лестница

Программа строит полосы и отвечает на 200 случайных запросов, сверяя каждый ответ с перебором всех отрезков. Концы отрезков стоят на чётных x, запросы - на нечётных: так запрос не попадает на вертикаль, где отрезок начинается или кончается, и спора «принадлежит ли он полосе» не возникает.

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

def vyshe_v(s, t, x2):
    # выше ли отрезок s отрезка t в точке x = x2 / 2 (точно, в целых)
    a, b = s
    c, d = t
    ys = a[1] * 2 * (b[0] - a[0]) + (x2 - 2 * a[0]) * (b[1] - a[1])
    zs = 2 * (b[0] - a[0])
    yt = c[1] * 2 * (d[0] - c[0]) + (x2 - 2 * c[0]) * (d[1] - c[1])
    zt = 2 * (d[0] - c[0])
    return ys * zt > yt * zs

def postroit(otrezki):
    xs = []
    for s in otrezki:
        xs.append(s[0][0])
        xs.append(s[1][0])
    xs = sorted(set(xs))
    polosy = []
    for k in range(len(xs) - 1):
        x2 = xs[k] + xs[k + 1]
        v_polose = []
        for i in range(len(otrezki)):
            a, b = otrezki[i]
            if a[0] <= xs[k] and b[0] >= xs[k + 1]:
                # вставка по высоте в середине полосы
                j = len(v_polose)
                v_polose.append(i)
                while j > 0:
                    if not vyshe_v(otrezki[v_polose[j - 1]], [a, b], x2):
                        break
                    v_polose[j] = v_polose[j - 1]
                    j = j - 1
                v_polose[j] = i
        polosy.append(v_polose)
    return [xs, polosy]

def nayti(struktura, otrezki, q, schet):
    # номер отрезка прямо под точкой q или -1
    xs, polosy = struktura
    lo = 0
    hi = len(xs) - 1
    if q[0] <= xs[0] or q[0] >= xs[-1]:
        return -1
    while hi - lo > 1:
        schet[0] = schet[0] + 1
        mid = (lo + hi) // 2
        if xs[mid] <= q[0]:
            lo = mid
        else:
            hi = mid
    sp = polosy[lo]
    a = 0
    b = len(sp)
    while a < b:
        schet[0] = schet[0] + 1
        m = (a + b) // 2
        s = otrezki[sp[m]]
        if povorot(s[0], s[1], q) > 0:
            a = m + 1
        else:
            b = m
    if a == 0:
        return -1
    return sp[a - 1]
def perebor(otrezki, q):
    luchshij = -1
    for i in range(len(otrezki)):
        a, b = otrezki[i]
        if a[0] < q[0] < b[0] and povorot(a, b, q) > 0:
            if luchshij == -1:
                luchshij = i
            elif vyshe_v([a, b], otrezki[luchshij], 2 * q[0]):
                luchshij = i
    return luchshij

import random
random.seed(6)
# 1) карта из коротких отрезков: каждый в своей клетке сетки 6 × 6
karta = []
for i in range(6):
    for j in range(6):
        # концы на чётных x, запросы - на нечётных:
        # запрос никогда не встаёт на границу полосы
        x1 = 100 * i + 2 * random.randint(1, 15)
        x2 = 100 * i + 2 * random.randint(30, 49)
        y1 = 100 * j + random.randint(1, 99)
        karta.append([[x1, y1], [x2, 100 * j + random.randint(1, 99)]])
# 2) лестница: 30 длинных отрезков и 30 коротких под ними
lestnica = []
for k in range(30):
    lestnica.append([[0, 100 + 10 * k], [1000, 101 + 10 * k]])
for k in range(30):
    lestnica.append([[10 + 30 * k, 0], [20 + 30 * k, 5]])
for otrezki in [karta, lestnica]:
    s = postroit(otrezki)
    pamyat = 0
    for p in s[1]:
        pamyat = pamyat + len(p)
    sovpalo = 0
    schet = [0]
    for t in range(200):
        q = [2 * random.randint(0, 499) + 1, random.randint(1, 599)]
        if nayti(s, otrezki, q, schet) == perebor(otrezki, q):
            sovpalo = sovpalo + 1
    print(len(otrezki), len(s[1]), pamyat, sovpalo, schet[0] / 200)
36 58 174 200 4.69
60 61 1860 200 10.765

Столбцы: число отрезков, число полос, записей во всех полосах, совпавших с перебором ответов, шагов поиска на запрос. Карта из 36 коротких отрезков: 58 полос и 174 записи - по три отрезка на полосу, память почти линейна. Запрос - около пяти шагов поиска.

Разбор: лестница - и квадратичная память

Вторая строка - «лестница»: 30 длинных отрезков через весь экран и 30 коротких под ними. Концы коротких нарезали 61 полосу, и в каждой лежат все 30 длинных отрезков: 1860 записей на 60 отрезков. Удвой число ступенек - записей станет вчетверо больше: это n². Шагов на запрос около одиннадцати - log₂ 61 + log₂ 31 ≈ 6 + 5, логарифм держится, разбухает только память.

На такой лестнице трапецоидная карта хранит не больше 3 · 60 + 1 = 181 трапеции: вертикаль из конца короткого отрезка упирается в первый же длинный отрезок над ним и не идёт выше. Именно поэтому в геоинформационных системах полосы на практике не строят, а используют трапецоидные карты или деревья с ограничивающими прямоугольниками.

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

Запрос на вертикали полосы. Отрезок, начавшийся ровно в x запроса, одна функция считает принадлежащим полосе, другая - нет. Нужен договор: полуоткрытые полосы, как полуоткрытые интервалы в главе о заметающей прямой.

Порядок отрезков в полосе по высоте в float. Два близких отрезка могут поменяться местами, и запрос вернёт не тот район. Сравнение в середине полосы делается в целых - удвоением координаты середины.

Фиксированный порядок вставки в трапецоидную карту. Например, по длине или по x. Для такого порядка существует вход, где граф поиска вырождается в цепочку, и запрос стоит n.

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

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

Теперь сам

  1. Отрезков 1000, и все они короткие и не перекрываются по x. Сколько примерно записей в полосах?
  2. Почему для запроса хватает отрезка прямо под точкой, а не всего многоугольника района?
  3. Зачем трапецоидной карте случайный порядок вставки, если результат - сама карта - от порядка не зависит?

Ответ. 1. Каждый отрезок пересекает только свою полосу: около 1000 записей на почти 2000 полос.

  1. Отрезок под точкой - граница, над которой лежит ровно одна область. Если у каждого отрезка записана область сверху, отрезок и даёт ответ.

  2. Карта от порядка действительно не зависит, а граф поиска - зависит. Его глубина - время запроса, и только для случайного порядка она в ожидании логарифмическая.

Главное

Локализация - плата один раз за структуру ради логарифма на каждый запрос. Полосы дают логарифм двумя поисками, но могут съесть n² памяти; трапецоидная карта со случайным порядком вставки сохраняет логарифм и возвращает память к O(n).

Комментарии

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

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

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