Как найти все простые пути длиной не более k, начинающиеся в вершине ориентированного графа?

Я пытаюсь найти все простые пути заданной длины и работаю над использованием BFS для решения этой проблемы. Однако я не уверен насчет конкретного алгоритма, который следует использовать. Похоже, что BFS не может легко решить эту проблему. Все проблемы, которые я обнаружил, заключались в поиске путей между двумя заданными вершинами.

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

k длин означает k прыжков, а не веса путей.

Например, у нас k = 3, и, учитывая вершину 1, мы хотим найти пути длины k = 1, k = 2, k = 3.

когда k = 1, у нас есть пути 12 и 13; когда k = 2, у нас есть пути 132 и 134; когда k = 3, мы не находим подходящие пути.

Какой алгоритм можно использовать для решения проблемы?

@trincot Ваше предложение очень полезно. Я опубликую новый вопрос о добавленном контенте!

ivygrowing 29.06.2024 13:45
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
1
244
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Для этого вы должны использовать обход в глубину. Рекурсивная функция является естественным выбором для этого. Он возвращался на глубину k и затем воспроизводил путь, который прошел. При каждом рекурсивном вызове убедитесь, что вы не добавляете узел к пути, который уже находится на пути. И когда вы выходите из рекурсивного вызова, соответственно сократите путь.

Вот реализация на JavaScript, где рекурсивная функция принимает в качестве аргумента список смежности (для представления графа), текущий узел, значение k и путь в виде набора (упорядоченный хэш-набор).

function* genPaths(adj, node, k, path=new Set) {
    if (path.has(node)) return; // cycle detected!
    if (k == 0) return yield [...path, node]; // We have a result
    path.add(node);
    for (const neighbor of adj[node]) {
        yield* genPaths(adj, neighbor, k - 1, path);
    }
    path.delete(node); // backtrack
}

// Demo
const adj = {
    "1": ["2", "3"],
    "2": [],
    "3": ["2", "4"],
    "4": ["3"]
};

// Finding paths with k=1, k=2 and k=3:
for (let k = 1; k <= 3; k++) {
    console.info("results for k  = ", k);
    for (const path of genPaths(adj, "1", k)) {
        console.info("  ", ...path);
    }
}
console.info("done");

Продлить его...

Вышеупомянутая функция возвращает пути ровно k длины, и основной код использует ее для получения путей нескольких разных длин. Если вас всегда интересуют все более короткие пути, даже включая путь с размером 0 (только начальный узел), вы можете расширить функцию, чтобы сделать все это с помощью одного вызова верхнего уровня:

function* genPaths(adj, node, k, path=new Set) {
    if (path.has(node)) return; // cycle detected!
    yield [...path, node]; // We have a result (0 <= path size <= k)
    if (k == 0) return;
    path.add(node);
    for (const neighbor of adj[node]) {
        yield* genPaths(adj, neighbor, k - 1, path);
    }
    path.delete(node); // backtrack
}

// Demo
const adj = {
    "1": ["2", "3"],
    "2": [],
    "3": ["2", "4"],
    "4": ["3"]
};

// Finding paths with size <= 3:
for (const path of genPaths(adj, "1", 3)) {
    console.info("  ", ...path);
}
console.info("done");

Обратите внимание, что размер пути выражается количеством ребер на нем, поэтому [1] — это путь длиной 0, [1, 3] — это путь длиной 1 и т. д.

Если вы хотите исключить путь длиной 0, просто добавьте условие в оператор yield:

    if (path.length > 0) yield [...path, node];

Эй, спасибо! Это очень полезно! Мой график действительно большой. Если мы используем DFS/рекурсию, это может вызвать переполнение стека. Могу ли я использовать БФС?

ivygrowing 28.06.2024 03:52

Максимальная глубина рекурсии равна k — максимальной длине пути.

beaker 28.06.2024 04:03

@beaker Ты прав. Я понял! Спасибо!

ivygrowing 28.06.2024 04:18

Я считаю, что DFS более подходит для этого, чем BFS. Это в конечном итоге то, что вы ищете? Дайте мне знать, если у вас все еще есть сомнения...

trincot 28.06.2024 07:54

@trincot Я думаю, что главный недостаток DFS заключается в дублировании вычислений. Например, пути k = 1 являются префиксными путями k = 2. Если мы вычисляем пути отдельно для k = 1, 2 и 3, это не так эффективно. Верно?

ivygrowing 28.06.2024 08:46

@trincot О, после того, как я ответил вам, я сразу увидел последний отредактированный ответ! Это именно то, чего я хочу. Большое спасибо! Это очень мило с вашей стороны!

ivygrowing 28.06.2024 08:48

@trincot Я реализовал как BFS, так и DFS. Я обнаружил, что BFS намного быстрее, чем DFS!

ivygrowing 29.06.2024 11:45

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

trincot 29.06.2024 11:49

@trincot Привет, я только что добавил свой код к вопросу.

ivygrowing 29.06.2024 12:40

Вот псевдокод алгоритма Йена

IMPORT graph, source and sink
CLEAR vShortestPaths
calculate shortest path from source to sink, add to vShortestPaths
WHILE TRUE
   CLEAR vPotentialPaths
   SET work = graph
   LOOP PF over vShortestPaths
       LOOP spur over PF
            REMOVE out edge from spur in work used by PF
            calculate spurPath, shortest path from spur to sink in work
            IF spurPath exists
                CONTINUE to next iteration of LOOP spur
            SET newPath to combination of source to spur in PF and spurPath
            IF newPath NOT in vShortestPaths
                ADD newPath to vPotentialPaths
            END LOOP spur
        END LOOP PF
    IF vPotentialPaths NOT empty
        ADD shortest path in vPotentialPaths to vShortestPaths
    END WHILE TRUE 

Чтобы найти все пути в графе:

Set vertices into fixed order ( input order is fine )
LOOP I over vertices
   LOOP J over the (J+1)the to the last vertex
       Apply Yen to I,J vertex pair

Спасибо! Однако этот алгоритм учитывает веса путей. «Длина» в моем вопросе равна прыжкам пути, а не весам.

ivygrowing 28.06.2024 03:50

Установите веса на 1 для каждого ребра.

ravenspoint 28.06.2024 05:32

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

ivygrowing 28.06.2024 07:09

Переберите уникальные пары вершин и примените к каждой алгоритм Йена.

ravenspoint 28.06.2024 14:29

Алгоритм Йена используется для расчета k-кратчайших путей. Это совершенно другая проблема, чем та, которую ставит ФП.

beaker 28.06.2024 16:04

ОП хочет, чтобы все пути имели заданную длину. Йен возвращает пути между двумя вершинами в порядке возрастания длины. Отфильтруйте пути, которые длиннее требуемого, и переберите уникальные пары вершин. Тогда проблема ОП решена. Я предполагал, что эти шаги по применению алгоритма к этой конкретной проблеме будут очевидны. Это очень простой навык — применять стандартные алгоритмы к немного другим задачам, иначе вам всегда придется изобретать велосипед, чтобы строить разные вагоны.

ravenspoint 28.06.2024 16:43

Да, я вижу изменения, необходимые для того, чтобы это заработало, но мой вопрос: почему? Вы отвечаете на простую задачу, много раз решая более сложную задачу. Этот шаг «вычислить кратчайший путь», как он реализован? Наверное, с Дейкстрой, да? Таким образом, при каждом выполнении Дейкстры вы вычисляете кратчайший путь от одного источника ко всем остальным вершинам оставшегося графа, а затем отбрасываете их все, кроме одной. Затем повторное выполнение, пока путь не станет слишком длинным. Затем повторно выполняем все это для каждой раковины. Но что представляет собой алгоритм Дейкстры на невзвешенных графах? ДФС.

beaker 28.06.2024 19:10

Версия обхода DFS с возвратом*, используемая Goodrich, Tamassia & Goldwasser (2013) в разделах 14.3.1–14.3.2, может быть изменена и адаптирована для решения этой проблемы.

Мы также хотим отслеживать посещенные вершины и соответствующие ребра дерева (или ребра открытия) с помощью упорядоченной хэш-таблицы (что подразумевает, что ваша вершина должна быть хешируемой), что предотвращает циклический обход (с помощью карты смежности для представления Graph, мы можем запросить пройденные вершины за время O (1) и опустить задние ребра).

В конечном итоге это приведет к созданию (подразумеваемых) сильно связных компонентов (или связных компонентов в случае неориентированного графа), достижимых путем обхода не более k-1 ребер в графе путем вызова DFS в начальной вершине s. , а затем рекурсивно вызывая DFS для всех случайных вершин обнаружения в указанном k-диапазоне.

* We populate a concurrent path accumulator for every valid recursive DFS call (when traversed edges < k) , and then backtrack and pop the corresponding retraction edges (triggered when traversed edges >= k, until a new edge is traversed and accumulated again) as the recursion unwinds.


В следующем решении используется представление графика на карте смежности, представленное Goodrich et al. (стр. 635-637). Для краткости, реализацию Graph, которая на 100% совместима с этим алгоритмом с нулевыми зависимостями, можно найти здесь: graph.py .

Все методы Graph.insert_vertex(), Graph.insert_edge(), Edge.opposite(), Edge.endpoints() выполняются за время O(1), за исключением Graph.incident_edges(v), который занимает время O(исходящая степень(v)).

Первоначально klen_dfs() должно занимать время O(n) в худшем случае (когда для поиска вершины, достижимой в k ребрах, требуется обход всего графа). Однако сохранение каждого уникального пути (с помощью строки path_array.append(p[:])) вызывает некоторые необходимые повторные вычисления, накапливающие приблизительно операции суммирования (k) для каждого пройденного пути длины k, то есть nk × суммирование (k), где nk - количество вершин достижима, пройдя k ребер. В целом, klen_dfs() в худшем случае занимает O((nk× k2) + n) времени.

from collections import OrderedDict
from typing import List
from graph import Graph


def init_graph():
    G = Graph(directed=True)

    v1 = G.insert_vertex(1)
    v2 = G.insert_vertex(2)
    v3 = G.insert_vertex(3)
    v4 = G.insert_vertex(4)

    G.insert_edge(v1, v2)
    G.insert_edge(v1, v3)
    G.insert_edge(v3, v2)
    G.insert_edge(v3, v4)
    G.insert_edge(v4, v3)

    return G, v1


def klen_dfs(G: Graph, s: Graph.Vertex, k: int):
    """Report all paths (edge lists) less than length k.
    G - Adjacency Map Graph ADT
    s - Starting vertex
    k - Path length limit (actual limit = k-1)
    """
    discovery_edges: OrderedDict[Graph.Vertex, Graph.Edge] = OrderedDict()
    path_accummulator: List[Graph.Edge] = [ ]
    path_array: List[List[Graph.Edge]] = [ ]

    def dfs(g: Graph, u: Graph.Vertex, discovered, klen, p):
        """Traditional DFS with addtional params.
        u - A vertex in G
        p - Concurrent path array of less than k edges
        """
        if klen < k:
            for e in g.incident_edges(u, outgoing=True):
                v = e.opposite(u)
                if not discovered.get(v):   # Cyclic traversal prevention
                    discovered[v] = e
                    p.append(e)
                    path_array.append(p[:])
                    dfs(g, v, discovered, klen + 1, p)
                    p.pop()
                    discovered.pop(v)

    dfs(G, s, discovery_edges, 1, path_accummulator)
    return path_array


if __name__ == "__main__":
    # For graph produced by init_graph(), all k >= 3 has same results
    for K in range(1,4):
        digraph, start = init_graph()
        paths = klen_dfs(digraph, start, K)
        formatted_paths = []
        for path in paths:
            formatted_path = []
            for edge in path:
                endpoints = edge.endpoints()
                formatted_path.append((
                    endpoints[0].element(), endpoints[1].element()))
            formatted_paths.append(formatted_path)
        print(f"ALL paths when k = {K} (i.e of length {K-1} < k length)")
        print(*formatted_paths, sep = "\n")
        print()

Выход:

ALL paths when k = 1 (i.e of length 0 < k length)

ALL paths when k = 2 (i.e of length 1 < k length)
[(1, 2)]
[(1, 3)]

ALL paths when k = 3 (i.e of length 2 < k length)
[(1, 2)]
[(1, 3)]
[(1, 3), (3, 2)]
[(1, 3), (3, 4)]

Визуализация простых допустимых путей длиной меньше k=3 при s=1

Другие соображения: пути строго k-длины.

После изменений в ответе Тринкота в противном случае мы можем выбрать сообщать только те пути, длина которых имеет размер k, заменив строку во вложенной функции dfs(): path_array.append(p[:]) на if klen == k-1: path_array.append(p[:]).

Это уменьшает временную сложность с O((nk× k2) + n) до O((nk× k) + n) и дает следующий результат:

ALL paths when k = 1 (i.e of length 0 < k length)

ALL paths when k = 2 (i.e of length 1 < k length)
[(1, 2)]
[(1, 3)]

ALL paths when k = 3 (i.e of length 2 < k length)
[(1, 3), (3, 2)]
[(1, 3), (3, 4)]

Большое спасибо! Мне нужно время, чтобы понять код, потому что я давно не использую Python.

ivygrowing 29.06.2024 11:48

Другие вопросы по теме