Преобразование циклического DiGraph в ациклический DiGraph (DAG)

Как я могу удалить циклы из моего ориентированного графа? Это большой граф (более 100 тысяч узлов и более 200 тысяч ребер), поэтому метод должен быть эффективным. Мне нужно сделать орграф ацикличным, чтобы использовать такие функции, как networkx.topological_generations.

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

неудачная попытка (никогда не законченная; неэффективная)

def remove_cycles_from_G(G: nx.DiGraph):
    search_for_cycles = True
    while search_for_cycles:
        for cycle_path in nx.simple_cycles(G):
            try:
                G.remove_edge(cycle_path[-1], cycle_path[0])
            except nx.NetworkXError:
                # edge has already been disjointed by a previous edge removal.
                # Restart cycle generator.
                search_for_cycles = (
                    False  # Temporary condition which will be reversed.
                )
                break
        search_for_cycles = not (search_for_cycles)

Я также разработал более сложный эвристический подход, основанный на демонстрациях в этом проекте, но даже этот метод не работает с орграфом такого размера (после часа работы моя память была исчерпана).

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

РЕДАКТИРОВАНИЕ: воспроизводимые входные данные

Вот пример networkx DiGraph с множеством циклов. Моя ситуация включает в себя еще больше, но это демонстрирует суть:

import networkx as nx
import random

def induce_cycles(g: nx.DiGraph, cycles) -> None:
    cycles_added = 0
    while cycles_added < cycles:
        node = random.choice(list(g))
        non_parent_ancestors = nx.ancestors(g, node).difference(g.predecessors(node))
        if non_parent_ancestors:
            g.add_edge(node, random.choice(list(non_parent_ancestors)))
            cycles_added += 1

g = nx.balanced_tree(3, 6, create_using=nx.DiGraph())
induce_cycles(g, len(g.edges()) * 5)

# Efficiently remove cycles from g...
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
0
121
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

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

Здесь другой подход. Я пытаюсь имитировать топологическую сортировку, ЗА ИСКЛЮЧЕНИЕМ того, что я использую подход, заключающийся в том, чтобы всегда находить следующий лучший вариант.

from sortedcontainers import SortedSet

def topological_remove_cycles (g):
    # Dictionary of sets of incoming edges. We want to pick nodes with few of them.
    incoming = {}
    for node in g.nodes:
        incoming[node] = set()
    for node in g.nodes():
        for next_node in g.neighbors(node):
            incoming[next_node].add(node)

    # Sorted set of (count_incoming, -count_outgoing, node) triplets.
    # The first item in the set will have as few incoming nodes as it can, and as many outgoing.
    # In other words, it is a greedy choice for a good node to get rid of cycles.
    todo = SortedSet()
    for node, to_node in incoming.items():
        todo.add((len(to_node), -len(g.adj[node]), node))

    # And now let's start removing cycles.
    while 0 < len(todo):
        # Get the best node.
        _, _, node = todo.pop(0)
        to_node = incoming[node]
        for prev_node in to_node:
            # Each of these edges is in, or comes from, a cycle.
            if prev_node != node:
                # Do bookkeeping in todo.
                len_in = len(incoming[prev_node])
                len_out = len(g.adj[prev_node])
                todo.remove((len_in, -len_out, prev_node))
                todo.add((len_in, -len_out + 1, prev_node))
            g.remove_edge(prev_node, node)

        for next_node in g.neighbors(node):
            # Do bookkeeping in todo for forgetting about node.
            len_in = len(incoming[next_node])
            len_out = len(g.adj[next_node])
            todo.remove((len_in, -len_out, next_node))
            todo.add((len_in - 1, -len_out, next_node))
            incoming[next_node].remove(node)
        # And now we've guaranteed that node is cycle free and gone from our bookkeeping.

И, конечно же, давайте проведем небольшой тест, в котором предыдущий код дал сбой.

data = {0: [1, 2, 11], 1: [3, 4, 10], 2: [5, 6], 3: [7, 8, 6, 9], 4: [9, 10], 5: [11, 12, 3], 6: [13, 14, 10, 5, 7], 7: [1], 8: [7, 14], 9: [10], 10: [14], 11: [10], 12: [], 13: [], 14: [1, 2]}
g = nx.from_dict_of_lists(data, create_using=nx.DiGraph())

print(nx.to_dict_of_lists(g))
print(g)
print("Before removing, there are " + str(len(list(nx.simple_cycles(g)))) + " cycles")
topological_remove_cycles(g)
print(g)
print("After removing, there are " + str(len(list(nx.simple_cycles(g)))) + " cycles")
print(nx.to_dict_of_lists(g))

И более масштабный тест. Обратите внимание, что он настолько хорош в распутывании вещей, что обычно находит способ удалить меньше ребер, чем было добавлено при создании циклов!

g = nx.balanced_tree(3, 6, create_using=nx.DiGraph())
induce_cycles(g, 100)
print(g)
topological_remove_cycles(g)
print(g)
print("After removing, there are " + str(len(list(nx.simple_cycles(g)))) + " cycles")

Спасибо за демонстрацию этого метода. Но fast_remove_cycles работает не во всех случаях. Попробуйте случай с induce_cycles(g, 100) (или даже большее количество циклов, если ваш компьютер справится с этим), и вы увидите, что после его запуска еще остались циклы. Я пытался просмотреть ваш код, чтобы выяснить причину, но не могу определить, в чем может быть проблема.

russhoppa 22.04.2024 23:36

@russhoppa Теперь есть исправленная версия, которая должна работать намного лучше. Он проходит O(n log(n)). Но за это вы получаете жадную попытку найти небольшой минимальный набор дуг обратной связи.

btilly 23.04.2024 02:31

Очень впечатляюще. Спасибо, что собрали это вместе. Использование этого типа SortedSet великолепно. Я не знал об этой библиотеке.

russhoppa 23.04.2024 18:10

@russhoppa Да, это здорово. Мне нужно перестать использовать более медленные способы сделать это. Кстати, я избавился от некоторых прошлых неприятностей из поста. Просто оставляю полезный ответ на будущее.

btilly 24.04.2024 21:03

Спасибо, выглядит великолепно. И я прошу прощения за это. У меня есть один вопрос о вашем методе, который я не могу понять. Где вы пишете комментарий: # Each of these edges is in, or comes from, a cycle. как вы можете предположить, что узлы, отсортированные с наименьшим количеством предшественников и наибольшим количеством последователей, находятся в цикле? Тот факт, что у узла мало предшественников и много преемников, еще не означает, что он является частью цикла, не так ли? Или это просто наивное предположение об алгоритме, которое в большинстве случаев оказывается верным?

russhoppa 25.04.2024 21:37

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

btilly 26.04.2024 00:09

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

btilly 26.04.2024 00:12

Я понимаю. Это эффективный вид.

russhoppa 26.04.2024 20:39

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