Ответ 1
Эта проблема уже опубликована в этих соревнованиях:
- Проблема SPOJ BOYSCOUT
- Проблема USACO DEC08 "Самый большой забор" (последняя проблема на странице)
И его решение (O (N 3)) можно найти на этой странице: "Забота о безопасности USACO DEC08" "Анализ" Брюса Мерри и Якова Штайнхардта.
Ниже приведена попытка объяснить этот алгоритм. Также вот моя реализация этого алгоритма в С++ 11. Этот код работает для N <= 250 и целочисленных координат в диапазоне 0.. 1023. Три точки не должны находиться в одной строке. Вот Python script, который генерирует тестовые данные, удовлетворяющие этим требованиям.
O (N 2) алгоритм для упрощенной задачи
Упрощенная задача: найдите наибольшее подмножество точек, которые образуют выпуклый многоугольник и содержат самую левую точку данного множества (или если есть несколько левых точек, этот многоугольник должен содержать самые верхние из них).
Чтобы решить эту проблему, мы могли бы подключить каждую пару точек по направленному отрезку линии, затем (неявно) рассматривать каждый сегмент как график node, как показано на диаграмме:
Здесь родительский node и соответствующий сегмент имеют синий цвет. Линейный сегмент, соответствующий его левому дочернему элементу (красный цвет), начинается в конце "родительского" сегмента, и это сегмент линии делает наименьший возможный левый поворот относительно направления "родительского" сегмента. Сегмент линии, соответствующий его правому дочернему элементу (зеленый цвет), начинается с начала "родительского" сегмента, а также является сегментом линии, который делает наименьший возможный левый поворот относительно направления "родительского" сегмента.
Любой сегмент, заканчивающийся в самой левой точке, соответствует "листу" node, то есть он не имеет дочерних узлов. Такая конструкция графика гарантирует отсутствие циклов, другими словами, этот граф является DAG.
Теперь, чтобы найти наибольшее выпуклое подмножество точек, достаточно найти самый длинный путь на этом графе. И самый длинный путь в DAG можно найти с помощью алгоритма динамического программирования во времени O (E), где E - количество ребер на графике. Поскольку каждый node имеет только 2 исходящих ребра, каждый из которых соответствует паре точек, эта проблема может быть решена в O (N 2) времени.
Все это можно реализовать, если мы предварительно обрабатываем входные данные, сортируя направленные сегменты линии, начиная с одной и той же точки по углу. Это позволяет выполнить обход глубины в графе. Мы должны запомнить самый длинный путь, начиная с каждого обработанного node, так что каждый край графика посещается не более одного раза, и мы имеем алгоритм времени O (E) = O (N 2) (игнорируя предварительную обработку время на данный момент). Требования к пространству также O (N 2), потому что нам нужно сохранять отсортированные направления для каждой пары точек, а запоминание использует одно значение за node (которое также является парой точек).
Вот реализация этого алгоритма динамического программирования на С++:
unsigned dpStep(ind_t i_left, ind_t from, ind_t to_ind)
{
ind_t child = sorted[from][to_ind];
while (child < i_left)
child = sorted[from][++to_ind];
if (child == i_left)
return 1;
if (auto v = memorize[from][child])
return v;
const auto x = max(dpStep(i_left, child, lefts[from][child]) + 1,
dpStep(i_left, from, static_cast<ind_t>(to_ind + 1)));
memorize[from][child] = static_cast<ind_t>(x);
return x;
}
Входные параметры - самая левая точка и пара точек, образующих текущий сегмент линии (где начальная точка сегмента задана непосредственно, а конечная точка задается как индекс в отсортированном по угловому массиву точек). Слово "левый" в этом фрагменте кода немного злоупотребляется: это означает как самую левую точку (i_left
), так и предварительно обработанный массив, содержащий левые дочерние элементы для графика (lefts[][]
).
O (N 3) алгоритм
Общая проблема (где крайняя левая точка не фиксирована) может быть решена следующим образом:
- Сортировка точек в левом и правом направлениях
- Предварительно обработать точки, чтобы получить два массива: (a) каждая точка, отсортированная по направлению относительно друг друга, и (b) позиция в первом массиве конца сегмента линии, делая наименьший возможный левый поворот относительно направления "родительский" "сегмент.
- Используйте каждую точку как самую левую точку и найдите самый длинный выпуклый многоугольник с "упрощенным" алгоритмом.
- Периодически обрезать все точки слева от "самой левой" точки из предварительно обработанных массивов.
- Возьмите самый длинный путь, найденный на шаге 3.
Этап 4 не является обязательным. Это не улучшает сложность времени O (N 3). Но это немного улучшает скорость алгоритма DP, исключая ненужные точки. В моих тестах это дает 33% ускорение скорости.
Существует несколько альтернатив для предварительной обработки. Мы могли вычислять каждый угол (с помощью atan2
) и сортировать их, как это делается в примере кода Брюса Мерри и Якова Штайнхардта. Это самый простой способ, но atan2
может быть слишком дорогим, особенно для небольших наборов точек.
Или мы могли бы исключить atan2
и отсортировать тангенсы вместо углов, как это сделано в моей реализации. Это немного сложнее, но быстрее.
Оба этих варианта требуют O (N 2 log N) времени (если мы не используем некоторую сортировку распределения). Третья альтернатива - использовать известные методы вычислительной геометрии (механизмы и двойственность) для получения предварительной обработки O (N 2). Но я не думаю, что он полезен для нашей проблемы: он, вероятно, слишком медленный для небольших наборов точек из-за большого постоянного коэффициента, так как для больших точечных множеств он может дать некоторое улучшение скорости, но слишком незначительный, поскольку шаг 3 значительно перевешивает шаг 2. Также его гораздо сложнее реализовать.
Некоторые другие результаты: я попытался реализовать итеративный DP вместо рекурсии; это заметно не изменило скорость. Также я попытался выполнить сразу два поиска DP, чередуя шаги каждого поиска (что-то похожее на волокна или HyperThreading, реализованное в программном обеспечении); это может помочь, поскольку DP состоит в основном из доступа к памяти с высокой задержкой и предотвращения полного использования пропускной способности процессора; результаты не очень впечатляют, только около 11% ускорения скорости, скорее всего, с реальной HyperThreading было бы намного быстрее.