Как эффективно найти все пути, образованные числом k узлов в ориентированном ациклическом графе?

У меня есть DAG, который выглядит следующим образом: Пример DAG

Я хочу извлечь все пути, состоящие из 4 узлов в этом графе.

Мой ожидаемый результат должен выглядеть так:

N1 -> N2 -> N3 -> N4

N1 -> N2 -> N3 -> N5

N1 -> N3 -> N4 -> N5

N2 -> N3 -> N4 -> N5

Моя текущая попытка выглядит так

def path_finder(n1):
    paths = []
    if DAG.has_node(n1):
        for n2 in DAG.successors(n1):
            for n3 in DAG.successors(n2):
                for n4 in DAG.successors(n3):
                    paths.append([n1, n2, n3, n4])
    return paths

Я вызываю эту функцию для каждого узла. DAG — это глобальная переменная, точнее, это объект networkx (DAG = networkx.DiGraph()). Эта наивная функция ужасно медленная. Есть ли более эффективная стратегия для этого?

Я просмотрел вопрос 20262712, но был решен самостоятельно автор вопроса довольно неясным образом.

Спасибо

ОБНОВИТЬ:

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


person Parashar    schedule 27.08.2016    source источник
comment
Примечание. Откат, описанный в ответе на вопрос, который вы связали, в основном использует тот факт, что после того, как вы вычислили все пути от узла, вам не нужно делать это снова, если вы снова столкнетесь с узлом (если вы сохранили эти данные). Мой ответ использует это по-другому.   -  person Joel    schedule 27.08.2016
comment
Не могли бы вы немного рассказать о том, для чего вам это нужно? Вы уверены, что вам нужен список, а не, скажем, генератор?   -  person Joel    schedule 27.08.2016
comment
Это часть более крупного алгоритма, который я пытаюсь разработать для поиска конкретных повторяющихся последовательностей в геноме человека (который, по сути, представляет собой большую строку, состоящую из четырех букв A, T, G, C). Каждый узел здесь отмечает положение конкретного повтора и границы их расстояний. узлы соединяются только тогда, когда их расстояние меньше определенного значения. Теперь я хочу определить блоки этих повторов, поскольку они могут иметь смысл в любой комбинации из четырех повторов.   -  person Parashar    schedule 27.08.2016
comment
Я хотел бы сбросить все пути в файл HDF5. Я надеюсь, что это не будет быстрым процессом, учитывая, что у меня может быть до 100 миллионов узлов. Следовательно, мне нужно сбросить после всего дорогостоящего обхода графа.   -  person Parashar    schedule 27.08.2016
comment
Я не вижу хорошего решения. Вы должны проверить это, но я подозреваю, что во время выполнения доминирует paths.append([n1, n2, n3, n4]). Если это так, вы мало что сможете с этим поделать.   -  person Joel    schedule 27.08.2016
comment
Джоэл, ты прав, добавление к списку мне дорого обошлось. На графе с 10 000 узлами поиск путей занимает 16 секунд с «добавкой», а если я заменю оператор на проход, время выполнения сократится до 3,5 секунд. Я думаю об использовании очередей здесь. Любую высокопроизводительную библиотеку Python, которую вы можете использовать для этого?   -  person Parashar    schedule 28.08.2016
comment
Вы пытались запустить его с другими реализациями Python, такими как PyPy? Для простых кодов ускорение обычно в несколько раз.   -  person Ante    schedule 06.09.2016


Ответы (3)


Вот функция, которая возвращает пути заданной длины между всеми узлами в графе. Он перебирает все наборы узлов и использует networkx.all_simple_paths для получения путей.

import networkx as nx

g = nx.DiGraph()

g.add_nodes_from(['A','B','C','D','E'])

g.add_path(['A','B','C','D'])
g.add_path(['A','B','C','E'])
g.add_path(['A','C','D','E'])
g.add_path(['B','C','D','D'])

def find_paths(graph, number_nodes=4):
    paths = []
    for source in graph.nodes_iter():
        for target in graph.nodes_iter():
            if not source==target:
                p_source_target = nx.all_simple_paths(graph, 
                                                      source, 
                                                      target, 
                                                      cutoff=number_nodes-1)
                paths.extend([p for p in p_source_target if len(p)==number_nodes])
    return paths

find_paths(g)
# output:
[['B', 'C', 'D', 'E'],
 ['A', 'C', 'D', 'E'],
 ['A', 'B', 'C', 'E'],
 ['A', 'B', 'C', 'D']]
person James    schedule 27.08.2016
comment
Это найдет все пути между всеми парами узлов. Затем он выбирает длину 4 пути. Вы можете значительно ускорить это, установив отсечку на 4, чтобы она останавливалась, когда путь длиннее 4. - person Joel; 27.08.2016
comment
Спасибо, Джеймс. Я боюсь, что сложность этого подхода может быть O ^ 2 или хуже, учитывая, что вы дважды перебираете все узлы. Я проверил ваш код, и он намного медленнее, чем моя наивная стратегия и ее рекурсивный аналог, предложенный Джоэлом выше. Я ценю идею использования all_simple_paths. Думаем, как на этом лучше построить. - person Parashar; 27.08.2016
comment
Чтобы быть более конкретным, я попытался запустить ваш код на Graph с узлами 1K, и это заняло около 4 минут. Приведенная выше стратегия Джоэла заняла около 2,7 с, а моя — около 3,5 с на том же графике. - person Parashar; 27.08.2016

Часть вашей проблемы может заключаться в том, что если вы столкнетесь с узлом u как вторым узлом пути, то вы выполните все вычисления, чтобы найти все пути длины 3. Но затем, если вы снова встретите u как второй узел, вы повторяете все эти расчеты.

Поэтому постарайтесь избежать этого. Мы сделаем это рекурсивно, сначала вычислив все пути длины 3 (что требует вычисления путей длины 2).

def get_paths(G, n):
    '''returns a dict, paths, such that paths[u] is a list of all paths 
       of length n that start from u'''
    if n == 1: #base case, return a dict so that D[u] is a
               #list of all length 1 paths starting from u.
               #it's a boring list.
        return {u: [[u]] for u in G.nodes()}
    #if we get to here n>1 (unless input was bad)
    subpath_dict = get_paths(G,n-1)  #contains all length n-1 paths, 
                                     #indexed by first node
    path_dict = {}
    for u in G:
        path_dict[u] = []  
        for v in G.successors(u):
            path_dict[u].extend([[u]+subpath for subpath in subpath_dict[v]])
    return(path_dict)

G=nx.DiGraph()
G.add_path([1,2,3,4,5,6])
G.add_path([1,3,6,8,10])

path_dict = get_paths(G,4)
path_list = []
for paths in path_dict.values():
    path_list.extend(paths)
person Joel    schedule 27.08.2016
comment
Спасибо, Джоэл. Использование рекурсии здесь очень продуманно и уместно. Однако, когда я тестировал этот код, я не смог найти никакого прироста производительности по сравнению с моей наивной стратегией. Более того, у меня большая сеть с миллионами узлов. Я хотел бы отслеживать ход поиска пути, и рекурсия делает его неразрешимым. Можем ли мы еще улучшить это? - person Parashar; 27.08.2016
comment
Это меня удивило, но при более подробном рассмотрении это начинает обретать смысл. Я должен сделать [u]+subpath для каждого пути каждого преемника u. Вы входите и вызываете цикл for для каждого преемника ni. Вероятно, у них аналогичные затраты. Я немного продолжу в комментариях к вашему вопросу для уточнения, но я не знаю, что можно улучшить. - person Joel; 27.08.2016

Количество последовательностей имеет порядок |V|*d^3, где d — средняя степень выхода узла. Исходя из того, как создается граф, d ограничено. Я полагаю, что d не очень мало (например, ‹ 5). Это означает, что для графа узлов 5M есть пути> 1G.

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

Одной из идей является алгоритм, который проходит DAG в обратном топологическом порядке и делает две вещи:

  • для узла сохранить все пути, которые начинаются с этого узла длины 3,
  • используя последующие пути длины 3, выведите все пути длины 4.

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

Другая идея состоит в том, чтобы сделать простой алгоритм более оптимизированным. В вашем решении есть три цикла for для каждого узла. Это означает четыре цикла for для всех путей. Обратите внимание, что каждый цикл проходит через узлы. Можно соединить первые два цикла, перебирая ребра. Это связано с тем, что каждый путь должен начинаться с одного ребра. Алгоритм такой:

for n1, n2 in DAG.edges():
  for n3 in DAG.successors(n2):
    for n4 in DAG.successors(n3):
      paths.append([n1, n2, n3, n4])

Или еще проще, сначала выбрав средний край:

for n2, n3 in DAG.edges():
  for n1, n4 in itertools.product(DAG.predecessors(n2), DAG.successors(n3)):
    paths.append([n1, n2, n3, n4])

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

person Ante    schedule 06.09.2016