thedeemon: (Default)
[personal profile] thedeemon
Долго колебался, стоит ли об этом писать, но после того, как знающий человек похвалил выбор алгоритма, решил все же рассказать. Интересная часть экстремистской задачи из прошедшего конкурса ПФП про вырезание московской области из России заключалась в том, чтобы определить для заданной точки, входит ли она в фигуру, заданную набором ломанных (набор невыпуклых многоугольников, в которых могут быть дырки из других многоугольников). Сперва меня терзали сомнения, можно ли использовать подходы из планиметрии в задаче, где координаты точек даны в терминах долготы и широты на совсем не плоской планете. Ведь если посмотреть на карту в районе полюса, становится хорошо заметно, что прямые в этой системе координат не такие уж прямые. Но потом забил на эти сомнения и стал решать планиметрически.

Основная идея решения в том, что из заданной точки пускается луч в северном направлении, и смотрится, сколько отрезков из полигонов он пересечет. Если нечетное количество, то точка внутри фигуры, иначе снаружи.



Для начала определим тип для отрезков:

type line = { x1 : float; y1: float; x2 : float; y2 : float } 

Когда структура целиком состоит из флоатов, они хранятся компактно, unboxed, что хорошо для производительности.

Чтобы проверить, пересекает ли луч вверх из точки некоторый отрезок, сначала смотрим, лежит ли х-координата точки между х-координатами концов отрезка, и если да, то вычисляем у-координату точки, лежащей на отрезке и имеющей ту же х-координату. Если она больше исходной точки, то точка под отрезком и луч его пересекает.

let intersects x y line = 
  if x < line.x1 || x >= line.x2 then 0 else 
  let ly = (x -. line.x1) /. (line.x2 -. line.x1) *. (line.y2 -. line.y1) +. line.y1 in 
  if ly >= y then 1 else 0


Функция возвращает не bool, а 0 или 1, чтобы результаты по всем отрезкам можно было просуммировать и узнать, четно их количество или нет. Граница московской области содержит около 1500 отрезков, и если проверять их все, то получается довольно медленно - за секунду получается проверить около 20000 точек, в то время как на карте России их около 10 миллионов. Кто-то на Common Lisp'е решал эту проблему путем кодогенерации через макросы, что уменьшает время одного шага, но не количество шагов. Я же решил переходом от линейного алгоритма к логарифмическому. Для этого при чтении отрезков фигуры их х-координаты собирались в массив, он сортировался, повторы выкидывались и получался набор интервалов, на которые проекции точек фигуры разбивают ось Х (см. картинку). Для каждого такого интервала запоминался набор невертикальных отрезков, проекции которых на Х с ним пересекаются. Эти данные помещались в сбалансированное дерево, реализованное с помощью входящего в стандартную библиотеку Окамла функтора Map. Функторами в Окамле называются параметризованные модули, по сути это функции над модулями (где модуль - набор типов и функций). Map требует на вход модуль, описывающий некий тип, для которого задана операция сравнения. Нужно было лишь задать такой тип и операцию для интервалов:

type interval = { left : float; right : float } 
module Interval = struct  
  type t = interval  
  let compare a b =  
    if a.right <= b.left then -1 else 
    if b.right <= a.left then 1 else 0 
end   
module M = Map.Make(Interval)


Применение функтора Map.Make к модулю Interval дает модуль M, который реализует деревья с интервалами в качестве ключей и произвольным типом в качестве значений. В моем случае это был массив из отрезков (точнее, фактически, ссылок на них). Поиск в таком дереве - операция логарифмической сложности. В результате функция проверки принадлежности точки фигуре выглядела так:

let is_inside figure x y = 
  try 
    let a = M.find { left = x; right = x } figure in 
    let n =  DynArray.enum a |> Enum.map (intersects x y) |> Enum.fold (+) 0 in 
    n mod 2 > 0 
  with Not_found -> false


Вот и вся реализация. Она уже работала со скоростью около 2 миллионов точек в секунду, т.е. вся карта России могла быть обработана за 5 секунд. Дальше все упиралось лишь в скорость чтения и разбора карты и записи результата.
По-моему, гораздо элегантней, чем кодогенерация или разбиение карты на клетки и заливка floodfill'ом, как у кого-то.
This account has disabled anonymous posting.
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

Profile

thedeemon: (Default)
Dmitry Popov

May 2025

S M T W T F S
    123
45678910
11 121314151617
18192021222324
25262728293031

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Jun. 9th, 2025 12:34 am
Powered by Dreamwidth Studios