обманчиво простая реализация топологической сортировки в Python

извлеченные из здесь мы получили минимальную итеративную процедуру dfs, я называю ее минимальной, потому что вы вряд ли можете упростить код дальше:

def iterative_dfs(graph, start, path=[]):
    q = [start]
    while q:
        v = q.pop(0)
        if v not in path:
            path = path + [v]
            q = graph[v] + q

    return path

graph = {
    'a': ['b', 'c'],
    'b': ['d'],
    'c': ['d'],
    'd': ['e'],
    'e': []
}
print(iterative_dfs(graph, 'a'))

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

Не спрашивая о версии топологической сортировки, которая не является крошечной модификацией вышеупомянутой процедуры, я уже видел несколько из них. Вопрос не в том, "как реализовать топологическую сортировку в python", а в том, чтобы найти наименьший возможный набор настроек вышеуказанного кода, чтобы стать topological_sort.

ДОПОЛНИТЕЛЬНЫЕ КОММЕНТАРИИ

в оригинальной статье автор говорит:

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

цель этого вопроса не оптимизирует iterative_dfs но вместо этого придумывает минимальную версию topological_sort, полученную из него (просто ради того, чтобы узнать больше об алгоритмах теории графов). На самом деле, я думаю, что более общий вопрос может быть чем-то вроде набора минимальных алгоритмов, {iterative_dfs, recursive_dfs, iterative_bfs, recursive_dfs}, что бы их деривации topological_sort? Хотя это сделало бы вопрос более длинным / сложным, поэтому выяснение topological_sort из iterative_dfs достаточно хорошо.

2 ответов


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

во-первых, вот немного улучшенная версия вашего кода (она намного эффективнее и не намного сложнее):

def iterative_dfs_improved(graph, start):
    seen = set()  # efficient set to look up nodes in
    path = []     # there was no good reason for this to be an argument in your code
    q = [start]
    while q:
        v = q.pop()   # no reason not to pop from the end, where it's fast
        if v not in seen:
            seen.add(v)
            path.append(v)
            q.extend(graph[v]) # this will add the nodes in a slightly different order
                               # if you want the same order, use reversed(graph[v])

    return path

вот как я бы изменил этот код, чтобы сделать топологический сортировка:

def iterative_topological_sort(graph, start):
    seen = set()
    stack = []    # path variable is gone, stack and order are new
    order = []    # order will be in reverse order at first
    q = [start]
    while q:
        v = q.pop()
        if v not in seen:
            seen.add(v) # no need to append to path any more
            q.extend(graph[v])

            while stack and v not in graph[stack[-1]]: # new stuff here!
                order.append(stack.pop())
            stack.append(v)

    return stack + order[::-1]   # new return value!

часть, которую я прокомментировал с "новым материалом здесь", - это часть, которая вычисляет порядок, когда вы двигаетесь вверх по стеку. Он проверяет, является ли найденный новый узел дочерним для предыдущего узла (который находится в верхней части стека). Если нет, он всплывает в верхней части стека и добавляет значение order. Пока мы делаем DFS,order будет в обратном топологическом порядке, начиная с последнего значения. Мы обращаем его в конце функции и объединяем его с остальные значения в стеке (которые удобно расположены уже в правильном порядке).

потому что этот код должен проверить v not in graph[stack[-1]] кучу раз, это будет гораздо эффективнее, если значения в graph словарь-это наборы, а не списки. График обычно не заботится о порядке сохранения его ребер, поэтому такое изменение не должно вызывать проблем с большинством других алгоритмов, хотя код, который создает или обновляет график, может нуждаться в исправлении. Если вы когда-нибудь намеревались расширьте свой код графика для поддержки взвешенных графиков, вы, вероятно, в конечном итоге измените списки на сопоставление словарей с узла на вес, и это будет работать так же хорошо для этого кода (поиск словаря O(1) так же, как set lookups). В качестве альтернативы, мы могли бы построить наборы, которые нам нужны, если graph не может быть изменен напрямую.

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

def recursive_dfs(graph, node):
    result = []
    seen = set()

    def recursive_helper(node):
        for neighbor in graph[node]:
            if neighbor not in seen:
                result.append(neighbor)     # this line will be replaced below
                seen.add(neighbor)
                recursive_helper(neighbor)

    recursive_helper(node)
    return result

def recursive_topological_sort(graph, node):
    result = []
    seen = set()

    def recursive_helper(node):
        for neighbor in graph[node]:
            if neighbor not in seen:
                seen.add(neighbor)
                recursive_helper(neighbor)
        result.insert(0, node)              # this line replaces the result.append line

    recursive_helper(node)
    return result

вот именно! Одна строка удаляется, а аналогичная добавляется в другом месте. Если вы заботитесь о производительности, вы, вероятно, должны сделать result.append во второй вспомогательной функции тоже, и do return result[::-1] на верхнем уровне . Но используя insert(0, ...) более незначительных изменениях.

также стоит отметить, что если вы хотите топологический порядок всего графика, вам не нужно указывать начальный узел. Действительно, может не один узел, который позволяет вам пройти весь график, поэтому вам может потребоваться сделать несколько обходов, чтобы добраться до всего. Простой способ сделать это в итеративной топологической сортировке-инициализировать q to list(graph) (список всех ключей графика) вместо списка только с одним начальным узлом. Для рекурсивной версии, замените вызов recursive_helper(node) С циклом, который вызывает вспомогательную функцию на каждом узле графика, если он еще не в seen.


моя идея основана на двух ключевых замечаний:

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

оба они помогают нам пересекать график точно так же, как рекурсивные dfs. Как отмечалось в другом ответе, это важно для этой конкретной проблемы. Остальное должно быть легко.

def iterative_topological_sort(graph, start,path=set()):
    q = [start]
    ans = []
    while q:
        v = q[-1]                   #item 1,just access, don't pop
        path = path.union({v})  
        children = [x for x in graph[v] if x not in path]    
        if not children:              #no child or all of them already visited
            ans = [v]+ans 
            q.pop()
        else: q.append(children[0])   #item 2, push just one child

    return ans

q вот наш стек. В основной цикл, мы "доступ" наш текущий узел v из стека. "доступ", а не "pop", потому что мы должны иметь возможность вернуться к этому узлу снова. Мы узнаем всех незваных детей нашего текущего узла. и нажмите только первый, чтобы стек (q.append(children[0])), не все вместе. Опять же, это именно то, что мы делаем с рекурсивными ДПП.

если подходящий ребенок не найден (if not children), мы посетили все поддерево под ним. Поэтому она готова быть загнанным в ans. И вот когда ... мы действительно его открываем.

(само собой разумеется,это не отличная производительность. Вместо того, чтобы генерировать всех незваных детей в children переменная, мы должны просто создать первый, стиль генератора, возможно, используя фильтр. Мы также должны обратить это ans = [v] + ans, а вызов reverse on ans в конце. Но эти вещи опускаются из-за настойчивости опа в простоте.)