Комбинаторика Python, часть 2

Это последующий вопрос к комбинаторика в Python

у меня есть дерево или направленный ациклический граф, если вы хотите со структурой как:

alt text

где r-корневые узлы, p-родительские узлы, c-дочерние узлы и b -гипотетическая филиалы. Корневые узлы не связаны напрямую с родительскими узлами, это только ссылка.

Я intressted В найти все комбинации веток под ограничения:

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

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

combo[0] = [b[0], b[1], b[2], b[3]]
combo[1] = [b[0], b[1], b[2], b[4]]

структура данных, такая как b-это список объектов ветви, которые имеют свойства r, c и p, например:

b[3].r = 1
b[3].p = 3
b[3].c = 2

4 ответов


эта проблема может быть решена в Python легко и элегантно, потому что есть модуль под названием "itertools".

допустим, у нас есть объекты типа HypotheticalBranch, которые имеют атрибуты r, p и C. Так же, как вы описали это в своем посте:

class HypotheticalBranch(object):
  def __init__(self, r, p, c):
    self.r=r
    self.p=p
    self.c=c
  def __repr__(self):
    return "HypotheticalBranch(%d,%d,%d)" % (self.r,self.p,self.c)

ваш набор гипотетических ветвей таким образом

b=[ HypotheticalBranch(0,0,0),
  HypotheticalBranch(0,1,1),
  HypotheticalBranch(1,2,1),
  HypotheticalBranch(1,3,2),
  HypotheticalBranch(1,4,2) ]

магическая функция, которая возвращает список всех возможных комбинаций ветвей, может быть записана следующим образом:

import collections, itertools

def get_combos(branches):
  rc=collections.defaultdict(list)
  for b in branches:
    rc[b.r,b.c].append(b)
  return itertools.product(*rc.values())

чтобы быть точным, это функция возвращает итератор. Получить список, повторяя его. Эти четыре строки кода распечатают все возможные комбо:

for combo in get_combos(b):
  print "Combo:"
  for branch in combo:
    print "  %r" % (branch,)

результатом этой программы является:

Combo:
  HypotheticalBranch(0,1,1)
  HypotheticalBranch(1,3,2)
  HypotheticalBranch(0,0,0)
  HypotheticalBranch(1,2,1)
Combo:
  HypotheticalBranch(0,1,1)
  HypotheticalBranch(1,4,2)
  HypotheticalBranch(0,0,0)
  HypotheticalBranch(1,2,1)

...что вы хотели.

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

надеюсь, я получил то, что вы на самом деле хотели.


второе ограничение означает, что вы хотите максимальные комбинации, т. е. все комбинации с длиной, равной величине сочетание.

Я бы подошел к этому, сначала пройдя через структуру " b "и создав структуру с именем" c", чтобы сохранить все ветви, приходящие к каждому дочернему узлу и классифицированные корневым узлом, который приходит к нему.

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

например, ваша структура " c " будет выглядеть так:

c[i][j] = [b_k0, ...]  
--> means c_i has b_k0, ... as branches that connect to root r_j)

для примера вы указали:

c[0][0] = [0]
c[0][1] = []
c[1][0] = [1]
c[1][1] = [2]
c[2][0] = []
c[2][1] = [3, 4]

это должно быть довольно легко кодировать его с помощью этого подхода. Вам просто нужно перебрать все ветви " b "и заполнить структуру данных для"c". Затем напишите небольшую рекурсивную функцию, которая проходит через все элементы внутри "с".

здесь это код (я ввел ваши данные образца вверху для тестирования):

class Branch:
  def __init__(self, r, p, c):
    self.r = r
    self.p = p
    self.c = c

b = [
    Branch(0, 0, 0),
    Branch(0, 1, 1),
    Branch(1, 2, 1),
    Branch(1, 3, 2),
    Branch(1, 4, 2)
    ]

total_b = 5   # Number of branches
total_c = 3   # Number of child nodes
total_r = 2   # Number of roots

c = []
for i in range(total_c):
  c.append([])
  for j in range(total_r):
    c[i].append([])

for k in range(total_b):
  c[b[k].c][b[k].r].append(k)

combos = []
def list_combos(n_c, n_r, curr):
  if n_c == total_c:
    combos.append(curr)
  elif n_r == total_r:
    list_combos(n_c+1, 0, curr)
  elif c[n_c][n_r]:
      for k in c[n_c][n_r]:
        list_combos(n_c, n_r+1, curr + [b[k]])
  else:
    list_combos(n_c, n_r+1, curr)

list_combos(0, 0, [])

print combos

здесь действительно две проблемы: во-первых, вам нужно разработать алгоритм, который вы будете использовать для решения этой проблемы, и, во-вторых, вам нужно его реализовать (в Python).


алгоритм

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

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

for each child node:
    for each root node:
        remember each permissible edge

find all combinations of permissible edges

код

>>> import networkx as nx
>>> import itertools
>>> 
>>> G = nx.DiGraph()
>>> G.add_nodes_from(["r0", "r1", "p0", "p1", "p2", "p3", "p4", "c0", "c1", "c2"])
>>> G.add_edges_from([("r0", "p0"), ("r0", "p1"), ("r1", "p2"), ("r1", "p3"),
...                   ("r1", "p4"), ("p0", "c0"), ("p1", "c1"), ("p2", "c1"),
...                   ("p3", "c2"), ("p4", "c2")])
>>> 
>>> combs = set()
>>> leaves = [node for node in G if not G.out_degree(node)]
>>> roots = [node for node in G if not G.in_degree(node)]
>>> for leaf in leaves:
...     for root in roots:
...         possibilities = tuple(edge for edge in G.in_edges_iter(leaf)
...                               if G.has_edge(root, edge[0]))
...         if possibilities: combs.add(possibilities)
... 
>>> combs
set([(('p1', 'c1'),), 
     (('p2', 'c1'),), 
     (('p3', 'c2'), ('p4', 'c2')), 
     (('p0', 'c0'),)])
>>> print list(itertools.product(*combs))
[(('p1', 'c1'), ('p2', 'c1'), ('p3', 'c2'), ('p0', 'c0')), 
 (('p1', 'c1'), ('p2', 'c1'), ('p4', 'c2'), ('p0', 'c0'))]

выше, кажется, работает, хотя я не проверял.


для каждого ребенка с, с гипотетическими родителями п(с) корни р(п(с)), выбрать ровно один родительский элемент P из P(C) для каждого корня р в р(п(с)) (таких, что R является корнем P) и включают в себя в сочетании, где б соединяет п-с (предполагая, что существует только один Б, то есть это не мультиграф). Число комбинаций будет произведением числа родителей, по которому каждый ребенок гипотетически связан с каждым корнем. Другими словами, размер набора комбинаций будет равно произведению гипотетических связей всех пар дочерних корней. В вашем примере все такие пары дочерних корней имеют только один путь, за исключением r1-c2, который имеет два пути, таким образом, размер набора комбинаций равен двум.

это удовлетворяет ограничению отсутствия комбинации, являющейся подмножеством другого, потому что, выбирая ровно один родитель для каждого корня каждого ребенка, мы максимизируем число соединений. Впоследствии добавление любого ребра b приведет к тому, что его корень будет подключен к его ребенка дважды, что не допускается. А так как мы выбираем ровно одну, то все комбинации будут точно одинаковой длины.

реализация этого выбора рекурсивно даст нужные комбинации.