Реализация алгоритма грубой силы для обнаружения самопересекающегося многоугольника

Первоначально я реализовал алгоритм Hoey-Shamos, однако он слишком сложен для поддержки в будущем (у меня нет права голоса в этом), и он не дает правильных отчетов, поэтому я собираюсь использовать оптимизированный алгоритм грубой силы.

Мой вопрос: как я могу оптимизировать этот код, чтобы его можно было использовать?

В нынешнем виде мой код содержит вложенный цикл for, дважды повторяющий один и тот же список.

РЕДАКТИРОВАТЬ: Превратил строки в HashSet и использовал два цикла foreach ... сократил около 45 секунд сканирования 10 000. Этого все еще недостаточно.

foreach (Line2D g in lines)
{                   
foreach (Line2D h in lines)
{
    if (g.intersectsLine(h))
    {
        return false;
    }
}                  

 } // end 'lines' for each loop

Если я заставлю свой метод «intersectsLine()» возвращать false (в целях тестирования), сканирование 10 000 записей все равно займет 8 секунд (у меня 700 000 записей). Это слишком долго, поэтому мне нужно оптимизировать этот фрагмент кода.

Я попытался удалить строки из списка после того, как он был сравнен со всеми другими строками, однако есть проблема с точностью (не знаю, почему), и увеличение скорости едва заметно.

Вот мой метод intersectsLine. Я нашел альтернативный подход здесь, но он выглядит как это было бы медленнее со всеми вызовами методов и еще чем-то. Мне не кажется, что вычисление наклона требует слишком много вычислений (поправьте меня, если я ошибаюсь?)

public bool intersectsLine(Line2D comparedLine)
{

//tweakLine(comparedLine);
if (this.Equals(comparedLine) ||
    P2.Equals(comparedLine.P1) ||
    P1.Equals(comparedLine.P2))
{
    return false;
}

double firstLineSlopeX, firstLineSlopeY, secondLineSlopeX, secondLineSlopeY;

firstLineSlopeX = X2 - X1;
firstLineSlopeY = Y2 - Y1;

secondLineSlopeX = comparedLine.X2 - comparedLine.X1;
secondLineSlopeY = comparedLine.Y2 - comparedLine.Y1;

double s, t;
s = (-firstLineSlopeY * (X1 - comparedLine.X1) + firstLineSlopeX * (Y1 - comparedLine.Y1)) / (-secondLineSlopeX * firstLineSlopeY + firstLineSlopeX * secondLineSlopeY);
t = (secondLineSlopeX * (Y1 - comparedLine.Y1) - secondLineSlopeY * (X1 - comparedLine.X1)) / (-secondLineSlopeX * firstLineSlopeY + firstLineSlopeX * secondLineSlopeY);

if (s >= 0 && s <= 1 && t >= 0 && t <= 1)
{
    //console.WriteLine("s = {0}, t = {1}", s, t);
    //console.WriteLine("this: " + this);
    //console.WriteLine("other: " + comparedLine);
    return true;
}

return false; // No collision */

}

РЕДАКТИРОВАНИЕ: основным узким местом являются большие полигоны! Я исключил работающие полигоны с более чем 100 линиями, и все 700 000+ полигонов были обработаны за 5:10. Что находится в допустимом диапазоне! Наверняка есть способ проверить, стоит ли вычислять многоугольник перед запуском этого кода? У меня есть доступ к свойствам XMin, Xmax, YMin и YMax, поможет ли это?

Запустил еще один тест, ограничив количество полигонов до 1000 строк каждый. Это заняло чуть больше часа.

Я удалил все ограничения полигонов, и он работает уже 36 часов... до сих пор никаких результатов.

У меня есть несколько идей:

-Когда я генерирую свой хэш-набор строк, у меня есть другой хэш-набор/список, в котором есть кандидаты на пересечение. Я бы добавил линии в этот список только в том случае, если есть вероятность пересечения. Но как мне исключить/добавить возможности? Если бы было только три линии, которые могли бы пересекаться с другими, я бы сравнивал 4000 строк с 3 вместо 4000. Одно это может иметь ОГРОМНОЕ значение.

-Если одна и та же точка встречается дважды, за исключением первой и последней точки, не запускайте вложенный цикл for.

Редактировать:


Информация о полигонах: всего 700 000

Существует более четырех тысяч полигонов с 1000 и более точками.

Есть 2 полигона с 70 000 и более точек


Я думаю, что можно сократить это время до пятнадцати минут или около того, проявив немного творчества.


person Evan Parsons    schedule 22.08.2013    source источник
comment
Поскольку это сайт программирования, есть некоторые вопросы, связанные с тем, какой тип данных вы планируете использовать: FP или целое число?   -  person chux - Reinstate Monica    schedule 22.08.2013
comment
Не совсем понял, что вы имеете в виду? У меня есть функция внутри объекта, которая возвращает true/false. Функция вернет false, если многоугольник имеет самопересекающиеся линии.   -  person Evan Parsons    schedule 22.08.2013
comment
Являются ли точки ваших полигонов целым числом или типом с плавающей запятой?   -  person chux - Reinstate Monica    schedule 22.08.2013
comment
Они двойные и имеют несколько знаков после запятой. Это данные ArcGIS.   -  person Evan Parsons    schedule 23.08.2013
comment
Неполный ответ, но вы можете использовать QuadTrees (en.wikipedia.org/wiki/Quadtree ). Эта хорошая статья blogs.msdn.com/b/kaelr /archive/2009/05/21/priorityquadtree.aspx имеет классную PriorityQuatree<T> реализацию.   -  person Simon Mourier    schedule 01.10.2013
comment
Есть ли какая-то конкретная часть этого, которая могла бы мне помочь? Из того, что я читал, похоже, что это для рисования фигур на экране. @SimonMourier   -  person Evan Parsons    schedule 01.10.2013
comment
Quadtrees в основном используются для обнаружения пересечений. Проверьте источник на наличие таких методов, как HasItemsIntersecting, GetItemsIntersecting, GetItemsInside и т. д.   -  person Simon Mourier    schedule 01.10.2013
comment
Я предполагаю, что мне придется внести некоторые изменения, я почитаю, спасибо.   -  person Evan Parsons    schedule 01.10.2013
comment
Можете ли вы предоставить образец/тестовый набор данных?   -  person RBarryYoung    schedule 02.10.2013
comment
Позвольте мне посмотреть, правильно ли я понимаю ограничения здесь. Вы не можете использовать лучший алгоритм (Hoey-Shamos), потому что он слишком сложен. Вы хотите использовать Brute-Force, но это O (n ^ 2), и на самом деле никакая простая оптимизация кода не сделает 490 000 000 000 сравнений управляемыми. Итак, что вам действительно нужно, так это алгоритм + реализация, которая быстрее, чем грубая сила, но достаточно проста, чтобы выдать себя за оптимизированную грубую силу. Это примерно так?   -  person RBarryYoung    schedule 02.10.2013
comment
Да, вы правы на 100%. К сожалению, я не контролирую требования.   -  person Evan Parsons    schedule 02.10.2013
comment
Рассматривали ли вы возможность использования хорошо известных высокооптимизированных реализаций (функция IsSimple), таких как GDAL Lib (имеет хорошие и быстрые привязки SWIG C#) или даже PostGIS (без хранилища, только с использованием SQL/MM Spatial Engine)?   -  person SalientBrain    schedule 02.10.2013
comment
Не уверен насчет PostGIS, так как ни одна из моих обработок не выполняет SQL. Какой из упомянутых имеет функцию isSimple? Я ничего не вижу на сайте GDal Lib.   -  person Evan Parsons    schedule 02.10.2013
comment
Что ж, у меня есть кое-что, что, я думаю, может сработать, но мне нужны некоторые образцы данных, которые я могу проверить, чтобы убедиться, что это работает правильно.   -  person RBarryYoung    schedule 03.10.2013
comment
Я могу поделиться выводом для этих фиктивных данных, которые я создал в ArcMap, но, к сожалению, это все :( sharetext.org/b0uL   -  person Evan Parsons    schedule 03.10.2013
comment
Хорошо, это какие-то многоугольники странной формы. Если большие реальные полигоны выглядят так, я не уверен, что моя оптимизация сильно поможет. Что представляют собой эти многоугольники и почему это имеет значение, если они пересекаются друг с другом?   -  person RBarryYoung    schedule 03.10.2013
comment
Код вверху — это то, что я использую для разделения колец. (Это данные ArcGIS). Многоугольники важны. Я сканирую данные о собственности. Я собираюсь обновить свой пост с дополнительной информацией.   -  person Evan Parsons    schedule 03.10.2013
comment
Ну, во-первых, вы, кажется, делаете все возможные сравнения дважды, а также сравниваете каждую строку с самой собой. Устранение этих проблем должно сократить время выполнения примерно вдвое.   -  person Jon    schedule 03.10.2013
comment
Хорошо. Участки собственности ведут себя достаточно хорошо, поэтому моя идея должна работать достаточно хорошо.   -  person RBarryYoung    schedule 04.10.2013


Ответы (3)


Ваш текущий алгоритм грубой силы - O (n ^ 2). Только для ваших двух полигонов по 70 000 линий это почти 10 миллиардов операций, не говоря уже о 700 000 других полигонов. Очевидно, что простой оптимизации кода будет недостаточно, поэтому вам нужна какая-то алгоритмическая оптимизация, которая может снизить это O (n ^ 2) без чрезмерной сложности.

O(n^2) исходит из вложенных циклов в алгоритме грубой силы, каждый из которых ограничен n, что делает его O(n*n). Самый простой способ улучшить это — найти способ уменьшить внутренний цикл, чтобы он не был связан или зависел от n. Итак, нам нужно найти какой-то способ упорядочить или переупорядочить внутренний список строк для проверки внешней строки, чтобы сканировать нужно было только часть полного списка.

Подход, который я использую, использует тот факт, что если два отрезка пересекаются, то диапазон их значений X должен перекрывать друг друга. Имейте в виду, это не означает, что они пересекаются, но если их диапазоны X не перекрываются, то они не могут пересекаться, поэтому нет необходимости проверять их на друг с другом. (это верно и для диапазонов значений Y, но вы можете использовать только одно измерение за раз).

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

Итак, что конкретно мы делаем, так это определяем пользовательский класс (endpointEntry), который представляет значения High или Low X двух точек линии. Все эти конечные точки помещаются в одну и ту же структуру списка, а затем сортируются на основе их значений X.

Затем мы реализуем внешний цикл, в котором мы сканируем весь список так же, как в алгоритме грубой силы. Однако наш внутренний цикл значительно отличается. Вместо того, чтобы повторно сканировать весь список в поисках строк для проверки на пересечение, мы скорее начнем сканирование отсортированного списка конечных точек от конечной точки с высоким значением X строки внешнего цикла и закончим его, когда пройдем ниже этой конечная точка с низким значением X той же строки. Таким образом, мы проверяем только те строки, диапазон значений X которых перекрывает строку внешнего цикла.

Хорошо, вот класс С#, демонстрирующий мой подход:

class CheckPolygon2
{
    // internal supporting classes
    class endpointEntry
    {
        public double XValue;
        public bool isHi;
        public Line2D line;
        public double hi;
        public double lo;
        public endpointEntry fLink;
        public endpointEntry bLink;
    }
    class endpointSorter : IComparer<endpointEntry>
    {
        public int Compare(endpointEntry c1, endpointEntry c2)
        {
            // sort values on XValue, descending
            if (c1.XValue > c2.XValue) { return -1; }
            else if (c1.XValue < c2.XValue) { return 1; }
            else // must be equal, make sure hi's sort before lo's
                if (c1.isHi && !c2.isHi) { return -1; }
                else if (!c1.isHi && c2.isHi) { return 1; }
                else { return 0; }
        }
    }

    public bool CheckForCrossing(List<Line2D> lines)
    {
        List<endpointEntry> pts = new List<endpointEntry>(2 * lines.Count);

        // Make endpoint objects from the lines so that we can sort all of the
        // lines endpoints.
        foreach (Line2D lin in lines)
        {
            // make the endpoint objects for this line
            endpointEntry hi, lo;
            if (lin.P1.X < lin.P2.X)
            {
                hi = new endpointEntry() { XValue = lin.P2.X, isHi = true, line = lin, hi = lin.P2.X, lo = lin.P1.X };
                lo = new endpointEntry() { XValue = lin.P1.X, isHi = false, line = lin, hi = lin.P1.X, lo = lin.P2.X };
            }
            else
            {
                hi = new endpointEntry() { XValue = lin.P1.X, isHi = true, line = lin, hi = lin.P1.X, lo = lin.P2.X };
                lo = new endpointEntry() { XValue = lin.P2.X, isHi = false, line = lin, hi = lin.P2.X, lo = lin.P1.X };
            }
            // add them to the sort-list
            pts.Add(hi);
            pts.Add(lo);
        }

        // sort the list
        pts.Sort(new endpointSorter());

        // sort the endpoint forward and backward links
        endpointEntry prev = null;
        foreach (endpointEntry pt in pts)
        {
            if (prev != null)
            {
                pt.bLink = prev;
                prev.fLink = pt;
            }
            prev = pt;
        }

        // NOW, we are ready to look for intersecting lines
        foreach (endpointEntry pt in pts)
        {
            // for every Hi endpoint ...
            if (pt.isHi)
            {
                // check every other line whose X-range is either wholly 
                // contained within our own, or that overlaps the high 
                // part of ours.  The other two cases of overlap (overlaps 
                // our low end, or wholly contains us) is covered by hi 
                // points above that scan down to check us.

                // scan down for each lo-endpoint below us checking each's 
                // line for intersection until we pass our own lo-X value
                for (endpointEntry pLo = pt.fLink; (pLo != null) && (pLo.XValue >= pt.lo); pLo = pLo.fLink)
                {
                    // is this a lo-endpoint?
                    if (!pLo.isHi)
                    {
                        // check its line for intersection
                        if (pt.line.intersectsLine(pLo.line))
                            return true;
                    }
                }
            }
        }

        return false;
    }
}

Я не уверен, какова истинная сложность выполнения этого алгоритма, но подозреваю, что для большинства непатологических полигонов она будет близка к O(n*SQRT(n)), что должно быть достаточно быстрым.


Объяснение логики внутреннего цикла:

Внутренний цикл просто просматривает список endPoints в том же порядке сортировки, что и внешний цикл. Но он начнет сканирование с того места, где внешний цикл находится в данный момент в списке (точка hiX некоторой строки), и будет сканировать только до тех пор, пока значения endPoint не упадут ниже соответствующего значение loX той же строки.

Что здесь сложно, так это то, что это нельзя сделать с помощью перечислителя (foreach(..in pts) внешнего цикла), потому что нет способа ни перечислить подсписок списка, ни начать перечисление на основе другой текущей позиции перечисления. Поэтому вместо этого я использовал свойства прямых и обратных ссылок (fLink и bLink) для создания структуры двусвязного списка, которая сохраняет порядок сортировки списка, но которую я могу постепенно сканировать без перечисления списка:

for (endpointEntry pLo = pt.fLink; (pLo != null) && (pLo.XValue >= pt.lo); pLo = pLo.fLink)

Разбивая это на части, спецификатор цикла for в старом стиле состоит из трех частей: инициализация, условие и увеличение-уменьшение. Выражение инициализации endpointEntry pLo = pt.fLink; инициализирует pLo прямой ссылкой текущей точки в списке. То есть следующая точка в списке в порядке убывания.

Затем выполняется тело внутреннего цикла. Затем применяется инкремент-декремент pLo = pLo.fLink, который просто устанавливает текущую точку внутреннего цикла (pLo) на следующую более низкую точку, используя прямую ссылку (pLo.fLink), тем самым продвигая цикл.

Наконец, он зацикливается после проверки условия цикла (pLo != null) && (pLo.XValue >= pt.lo), которое зацикливается до тех пор, пока новая точка не равна нулю (что означало бы, что мы были в конце списка) и пока XValue новой точки все еще больше или равно к нижнему значению X текущей точки внешнего цикла. Это второе условие гарантирует, что внутренний цикл будет рассматривать только те строки, которые перекрываются с линией конечной точки внешнего цикла.


Что мне теперь яснее, так это то, что я, вероятно, мог бы обойти всю эту неуклюжесть fLink-bLink, рассматривая список endPoints как массив:

  1. Заполните список конечными точками
  2. Сортировка списка по XValue
  3. Внешний цикл Перебирает список в порядке убывания, используя итератор индекса (i) вместо перечислителя foreach.
  4. (A) Внутренний цикл по списку с использованием второго итератора (j), начиная с i и заканчивая, когда он проходит ниже pt.Lo.

Это, я думаю, было бы намного проще. Я могу опубликовать упрощенную версию, если хотите.

person RBarryYoung    schedule 04.10.2013
comment
В общей сложности для запуска всех 700 000 полигонов требуется 15 минут! Это очень хорошо. Мне просто нужно проверить, ничего ли он не пропустил. - person Evan Parsons; 04.10.2013
comment
@EvanParsons Как прошло тестирование? У вас сработал этот алгоритм? - person RBarryYoung; 06.10.2013
comment
Я на 99% уверен, что он делает то, что я хочу. Единственная проблема заключается в том, что я еще не полностью это понимаю (хотя у меня есть довольно хорошее представление о том, что он делает), я собираюсь посвятить немного времени завтра и убедиться, что я его понимаю. Я внес некоторые коррективы в свой метод пересечения lineSegment и несколько других областей и сократил время до 8:47. Это в основном так же быстро, как и моя первоначальная реализация алгоритма Хои-Шамоса. Огромное спасибо! - person Evan Parsons; 06.10.2013
comment
Ладно, думаю, я в целом понимаю, что происходит. Но что происходит во внутреннем цикле? Я никогда раньше не видел такого цикла for. - person Evan Parsons; 09.10.2013
comment
@EvanParsons Я добавил объяснение этому в конец своего ответа. - person RBarryYoung; 10.10.2013

есть 2 вещи, которые нужно проверить:

  1. closed polygon consists of cyclic sequence of points
    • if there is the same point in this sequence more than once than it is self intersecting polygon.
    • имейте в виду, что первая и последняя точка могут быть одинаковыми (отличаются в вашем представлении полигона), в этом случае эта точка должна быть там более чем в два раза.
  2. check all not neighbouring lines of your polygon for intersection
    • not neighbouring lines do not share any point
    • если есть пересечение, то многоугольник самопересекающийся
person Spektre    schedule 16.09.2013
comment
нет,... я использую C++, и в большинстве случаев (в моей работе) сторонние библиотеки не подходят по многим причинам. Кстати, обнаружение пересечения в моих pgms происходит достаточно быстро. Я думаю, что ваш комментарий лучше подходит для вопроса, а не для ответа. Кстати, это ответ на двойной вопрос, для ясности посмотрите здесь stackoverflow.com/questions/18512815/ - person Spektre; 02.10.2013
comment
Ага, поспешил. я переместил комментарий - person SalientBrain; 02.10.2013

Простая оптимизация, которая должна вдвое сократить время для полигонов, которые не пересекаются:

int count = lines.Count();
for (int l1idx = 0;       l1idx < count-1; l1idx++) 
for (int l2idx = l1idx+1; l2idx < count;   l2idx++)
{
  Line2D g = lines[l1idx];
  Line2D h = lines[l2idx];
  if (g.intersectsLine(h))
  {
    return false;
  }
}
person weston    schedule 04.10.2013
comment
Не используйте Count() в списке. Это метод расширения linq, который создает Enumerator и MovesNext, увеличивая счетчик каждого элемента. Для 700000 он выполнит counter++ 700000 раз. В списке‹› есть поле Count, и все должны его использовать. Это простое значение, метод Add(T) использует Count для вставки нового элемента прямо в конец списка. Также лучше использовать массив для ValueTypes, чтобы избежать упаковки/распаковки. - person Gleb Sevruk; 24.01.2015
comment
@GlebSevruk Вы ошибаетесь насчет подсчета, расширение linq возвращает значение свойства, если это ICollection. См. stackoverflow.com/a/7969468. Вы также ошибаетесь в отношении общей логики бокса, это было бы верно для java, но не для c#. Редко вы захотите использовать массив stackoverflow.com/a/434765. - person weston; 24.01.2015
comment
Спасибо за указание. Я до сих пор использую 3.5 понял, что это было добавлено только в .NET 4. Теперь я понял суть с боксом, это будет происходить только в старом .Net 2 нетипизированный список. так как Ints хранятся в массиве объектов и должны быть переданы туда и обратно. Единственная проблема, оставшаяся с List‹T› для хранения простых значений, — это динамическое изменение размера, когда вы добавляете новый элемент. Начальный список‹T› создается с буфером на 8 или 16 (int[16]). После достижения этого предела выделяется новый массив, удвоенный по размеру (int[32]), и данные перемещаются в памяти. Я не вижу большого интереса к new List<T>(SIZE) у разработчиков - person Gleb Sevruk; 25.01.2015