Я пытаюсь определить наиболее эффективный по времени алгоритм для выполнения задачи, описанной ниже.
У меня есть набор рекордов. Для этого набора записей у меня есть данные соединения, которые показывают, как пары записей из этого набора соединяются друг с другом. Это в основном представляет собой неориентированный граф, в котором записи являются вершинами, а данные соединения - ребрами.
Все записи в наборе имеют информацию о соединении (т. Е. Отсутствуют потерянные записи; каждая запись в наборе соединяется с одной или несколькими другими записями в наборе).
Я хочу выбрать любые две записи из набора и иметь возможность показать все простые пути между выбранными записями. Под «простыми путями» я подразумеваю пути, которые не имеют повторяющихся записей в пути (т.е. только конечные пути).
Примечание: две выбранные записи всегда будут разными (т.е. начальная и конечная вершины никогда не будут одинаковыми; циклов нет).
Например:
Если у меня есть следующие записи: A, B, C, D, E и следующее представляет соединения: (А, В), (А, С), (В, А), (В, D), (В, Е), (В, F), (С, А), (С, Е), (C, F), (D, В), (Е, С), (Е, F), (Р, В), (Р, С), (F, Е) [где (A, B) означает, что запись A подключается к записи B]
Если бы я выбрал B в качестве начальной записи и E в качестве конечной записи, я бы хотел найти все простые пути через соединения записей, которые соединяли бы запись B с записью E.
Все пути, соединяющие B и E: B-> E B-> F-> Е B-> F-> С-> Е В-> А-> С-> Е В-> А-> С-> F-> Е
Это пример, на практике у меня могут быть наборы, содержащие сотни тысяч записей.
источник
Ответы:
Похоже, что это может быть выполнено с помощью поиска в глубину графика. Поиск в глубину найдет все нециклические пути между двумя узлами. Этот алгоритм должен быть очень быстрым и масштабироваться до больших графов (структура данных графа разреженная, поэтому он использует столько памяти, сколько необходимо).
Я заметил, что указанный вами график имеет только одно направленное ребро (B, E). Это опечатка или это действительно ориентированный граф? Это решение работает независимо. Извините, я не смог сделать это на C, я немного слаб в этой области. Я надеюсь, что вы сможете перевести этот Java-код без особых проблем.
Graph.java:
Search.java:
Программный вывод:
источник
Онлайн-словарь алгоритмов и структур данных Национального института стандартов и технологий (NIST) перечисляет эту проблему как « все простые пути» и рекомендует поиск в глубину . CLRS предоставляет соответствующие алгоритмы.
Умная техника с использованием сетей Петри находится здесь
источник
Вот псевдокод, который я придумал. Это не какой-то конкретный диалект псевдокода, но он должен быть достаточно простым для понимания.
Кто угодно хочет разобрать это на части.
[p] - это список вершин, представляющих текущий путь.
[x] - это список путей, соответствующих критериям
[s] - исходная вершина
[d] - конечная вершина
[c] - текущая вершина (аргумент подпрограммы PathFind)
Предположим, есть эффективный способ поиска соседних вершин (строка 6).
источник
Поскольку существующая нерекурсивная реализация DFS, приведенная в этом ответе, кажется неработающей, позвольте мне предоставить ту, которая действительно работает.
Я написал это на Python, потому что нахожу его довольно читаемым и не загроможденным деталями реализации (а также потому, что в нем есть удобный
yield
ключевое слово для реализации генераторов ), но его должно быть довольно легко перенести на другие языки.Этот код поддерживает два параллельных стека: один содержит более ранние узлы в текущем пути, а другой содержит индекс текущего соседа для каждого узла в стеке узлов (так что мы можем возобновить итерацию по соседям узла, когда мы вытолкнем его обратно. стек). Я мог бы с таким же успехом использовать один стек пар (узел, индекс), но я решил, что метод с двумя стеками будет более читабельным и, возможно, более легким для реализации для пользователей других языков.
В этом коде также используется отдельный
visited
набор, который всегда содержит текущий узел и все узлы в стеке, чтобы я мог эффективно проверить, является ли узел уже частью текущего пути. Если ваш язык имеет структуру данных «упорядоченного набора», которая обеспечивает как эффективные, подобные стеку, операции push / pop и эффективные запросы членства, вы можете использовать ее для стека узлов и избавиться от отдельногоvisited
набора.В качестве альтернативы, если вы используете настраиваемый изменяемый класс / структуру для своих узлов, вы можете просто сохранить логический флаг в каждом узле, чтобы указать, был ли он посещен как часть текущего пути поиска. Конечно, этот метод не позволит вам выполнить два поиска на одном и том же графике параллельно, если вы по какой-то причине захотите это сделать.
Вот тестовый код, демонстрирующий, как работает приведенная выше функция:
Запуск этого кода на приведенном примере графа дает следующий результат:
Обратите внимание, что хотя этот пример графа неориентированный (то есть все его ребра идут в обе стороны), алгоритм также работает для произвольных ориентированных графов. Например, удаление
C -> B
края (путем удаленияB
из списка соседейC
) дает тот же результат, за исключением третьего пути (A -> C -> B -> D
), который больше невозможен.Ps. Легко построить графики, для которых простые алгоритмы поиска, подобные этому (и другим, приведенным в этом потоке), работают очень плохо.
Например, рассмотрим задачу поиска всех путей от A до B на неориентированном графе, где у начального узла A есть два соседа: целевой узел B (у которого нет других соседей, кроме A) и узел C, который является частью клики из n +1 узлов, например:
Легко видеть, что единственный путь между A и B является прямым, но наивная DFS, запущенная с узла A, будет тратить O ( n !) Времени на бесполезное исследование путей внутри клики, хотя (для человека) очевидно, что ни один из этих путей не может привести к B.
Можно также создать группы DAG с аналогичными свойствами, например, если начальный узел A соединит целевой узел B и два других узла C 1 и C 2 , оба из которых подключаются к узлам D 1 и D 2 , оба из которых подключаются к E 1 и E 2 и так далее. Для n слоев узлов, расположенных таким образом, наивный поиск всех путей от A до B приведет к потере O (2 n ) времени на изучение всех возможных тупиков, прежде чем отказаться.
Конечно, добавляя край к целевому узлу B от одного из узлов в клике (кроме С), или из последнего слоя DAG, будет создавать экспоненциально большое число возможных путей от A до B, а чисто локальный алгоритм поиска не может заранее сказать, найдет он такое ребро или нет. Таким образом, в некотором смысле низкая чувствительность к выходным данным таких наивных поисков связана с отсутствием понимания глобальной структуры графа.
Хотя существуют различные методы предварительной обработки (такие как итеративное удаление листовых узлов, поиск одноузловых разделителей вершин и т. Д.), Которые можно использовать, чтобы избежать некоторых из этих «тупиков экспоненциального времени», я не знаю ни одного общего трюк с предварительной обработкой, который может устранить их во всех случаях. Общим решением было бы проверять на каждом этапе поиска, доступен ли целевой узел (с помощью подпоиска), и рано возвращаться, если это не так, но, увы, это значительно замедлит поиск (в худшем случае пропорционально размеру графа) для многих графов, не содержащих таких патологических тупиков.
источник
for path in find_simple_paths(graph, "A", "D"): print(" -> ".join(path))
,print
отсутствовала скобка.Вот логически более красивый рекурсивный вариант по сравнению со вторым этажом.
Программный вывод
источник
Решение в коде C. Он основан на DFS, который использует минимум памяти.
источник
Это может быть поздно, но вот та же версия C # алгоритма DFS на Java от Кейси для обхода всех путей между двумя узлами с использованием стека. Читаемость лучше с рекурсивным, как всегда.
источник
neighbours.Reverse()
? Это такList<T>.Reverse
?Недавно я решил аналогичную проблему, вместо всех решений, которые меня интересовали только самые короткие.
Я использовал итеративный поиск «в ширину», в котором использовалась очередь состояний, каждый из которых содержал запись, содержащую текущую точку на графике и путь, пройденный для ее достижения.
вы начинаете с единственной записи в очереди, которая имеет начальный узел и пустой путь.
Каждая итерация кода снимает элемент с заголовка списка и проверяет, является ли он решением (полученный узел - это тот, который вам нужен, если это так, мы закончили), в противном случае он создает новый элемент очереди с узлами, подключенными к текущему узлу, и измененные пути, основанные на пути предыдущего узла, с новым переходом, присоединенным в конце.
Теперь вы можете использовать что-то подобное, но когда вы найдете решение, вместо того, чтобы останавливаться, добавьте это решение в свой «список найденных» и продолжайте.
Вам нужно отслеживать список посещенных узлов, чтобы никогда не отступать от себя, иначе у вас будет бесконечный цикл.
если вы хотите немного больше псевдокода, напишите комментарий или что-то в этом роде, и я уточню.
источник
Я думаю, вам следует описать свою настоящую проблему, стоящую за этим. Я говорю это, потому что вы просите что-то эффективное по времени, но ответ на проблему, кажется, растет экспоненциально!
Поэтому я не ожидал бы лучшего алгоритма, чем экспоненциальный.
Я бы сделал обратный поиск и просмотрел весь график. Во избежание циклов сохраняйте все посещенные узлы по пути. Когда вы вернетесь, снимите отметку с узла.
Используя рекурсию:
Или это неправильно?
edit: О, и я забыл: вы должны устранить рекурсивные вызовы, используя этот стек узлов
источник
Основной принцип - вам не нужно беспокоиться о графиках. Это стандартная проблема, известная как проблема динамического подключения. Существуют следующие типы методов, с помощью которых вы можете добиться, чтобы узлы были подключены или нет:
Вот код C, который я пробовал с минимальной временной сложностью O (log * n). Это означает, что для списка ребер 65536 требуется 4 поиска, а для 2 ^ 65536 - 5 поисков. Делюсь своей реализацией из алгоритма: Курс алгоритмов Принстонского университета
СОВЕТ: Вы можете найти решение для Java по приведенной выше ссылке с соответствующими пояснениями.
источник
find_paths [s, t, d, k]
Это старый вопрос, на который уже дан ответ. Однако ни один из них не показывает, возможно, более гибкого алгоритма для достижения того же результата. Так что я брошу шляпу на ринг.
Я лично считаю
find_paths[s, t, d, k]
полезным алгоритм формы , где:Использование бесконечной формы вашего языка программирования для
d
иk
даст вам все пути§.§ очевидно, если вы используете ориентированный граф и хотите, чтобы между ними были все неориентированные пути,
s
иt
вам придется выполнить это в обоих направлениях:Вспомогательная функция
Мне лично нравится рекурсия, хотя иногда это может быть сложно, в любом случае сначала давайте определим нашу вспомогательную функцию:
Основная функция
Если это не так, основная функция проста:
Во-первых, отметим несколько моментов:
[]
- это неинициализированный список, замените его эквивалентом для выбранного вами языка программирования.paths_found
передается по ссылке . Понятно, что функция рекурсии ничего не возвращает. Обращайтесь с этим соответствующим образом.graph
предполагается некоторая формаhashed
структуры. Есть множество способов реализовать граф. В любом случае,graph[vertex]
вы получите список смежных вершин в направленном графе - настройте соответствующим образом.источник
Вот такая мысль пришла мне в голову:
источник
Насколько я могу судить, решения, данные Райаном Фоксом ( 58343 , Кристиан ( 58444 ) и вами ( 58461 ), примерно так хороши, насколько это возможно. Я не верю, что обход в ширину поможет в этом случае, как вы не получить все пути. Так , например, с краями
(A,B)
,(A,C)
,(B,C)
,(B,D)
и(C,D)
вы получите путьABD
иACD
, но неABCD
.источник
Я нашел способ перечислить все пути, включая бесконечные, содержащие циклы.
http://blog.vjeux.com/2009/project/project-shortest-path.html
Поиск атомных путей и циклов
Что мы хотим сделать, так это найти все возможные пути, идущие от точки A к точке B. Поскольку существуют циклы, вы не можете просто пройти и перечислить их все. Вместо этого вам придется найти атомарный путь, который не зацикливается, и минимально возможные циклы (вы не хотите, чтобы ваш цикл повторялся).
Первое определение атомарного пути, которое я взял, - это путь, который не проходит через один и тот же узел дважды. Однако я обнаружил, что не использовал все возможности. После некоторого размышления я понял, что узлы не важны, а ребра важны! Таким образом, атомарный путь - это путь, который не проходит через одно и то же ребро дважды.
Это определение удобно, оно также работает для циклов: атомарный цикл точки A - это атомарный путь, который идет от точки A и заканчивается в точку A.
Реализация
Чтобы получить весь путь, начинающийся из точки A, мы собираемся рекурсивно пройти по графу из точки A. Проходя через дочерний элемент, мы собираемся сделать ссылку child -> parent, чтобы знать все ребра, которые мы уже перешли. Прежде чем перейти к этому дочернему элементу, мы должны пройти по этому связанному списку и убедиться, что указанное ребро еще не пройдено.
По прибытии в пункт назначения мы можем сохранить найденный путь.
Проблема возникает, когда вы хотите освободить связанный список. По сути, это дерево, скованное в обратном порядке. Решением было бы дважды связать этот список и, когда все атомарные пути были найдены, освободить дерево от начальной точки.
Но разумное решение - использовать подсчет ссылок (вдохновленный сборкой мусора). Каждый раз, когда вы добавляете ссылку на родительский объект, вы добавляете одну к его счетчику ссылок. Затем, когда вы подходите к концу пути, вы идете назад и освобождаетесь, пока счетчик ссылок равен 1. Если он больше, вы просто удаляете один и останавливаетесь.
Искать атомарный цикл A - это то же самое, что искать атомарный путь от A до A. Однако мы можем сделать несколько оптимизаций. Во-первых, когда мы прибываем в точку назначения, мы хотим сохранить путь только в том случае, если сумма стоимости ребер отрицательна: мы хотим пройти только циклы поглощения.
Как вы видели ранее, при поиске атомарного пути обходится весь граф. Вместо этого мы можем ограничить область поиска компонентом сильной связности, содержащим A. Для нахождения этих компонентов требуется простой обход графа с помощью алгоритма Тарьяна.
Объединение атомных путей и циклов
На данный момент у нас есть все атомарные пути, которые идут от A к B, и все атомные циклы каждого узла, оставленные нам, чтобы организовать все, чтобы получить кратчайший путь. С этого момента мы будем изучать, как найти лучшую комбинацию атомных циклов в атомном пути.
источник
Как хорошо описано в некоторых других плакатах, проблема в двух словах заключается в использовании алгоритма поиска в глубину для рекурсивного поиска на графе всех комбинаций путей между взаимодействующими конечными узлами.
Сам алгоритм начинается с присваиваемого ему начального узла, проверяет все его исходящие ссылки и продвигается, расширяя первый дочерний узел появившегося дерева поиска, ища все глубже и глубже, пока не будет найден целевой узел или пока он не встретит узел. у которого нет детей.
Затем поиск возвращается к самому последнему узлу, который он еще не изучил.
Совсем недавно я писал об этой теме в блоге , публикуя в процессе пример реализации C ++.
источник
В дополнение к ответу Кейси Уотсон, вот еще одна реализация Java. Инициализация посещаемого узла стартовым узлом.
источник