Алгоритм поиска с возвратом является хоть и затратным, но зато достаточно универсальным методом. Связан он с перебором вершин графа. Возникает вопрос: можно ли интерпретировать ребра в качестве вершин? Вот эта идея и реализуется.

Код позволяет найти, с учетом стартовой вершины, цикл Эйлера или хотя бы его маршрут (полу-Эйлер). По сути, за вершину в такой интерпретации берется множество из двух соседних вершин. Реализовано в dict_to_setEdge. Что касается dfs, то это стандартная реализация обхода в глубину с возвратом.

рис. из интернета
рис. из интернета
# не Эйлер
G1 = {
    1: [2, 3, 5],
    2: [1, 4, 5],
    3: [1, 4, 5],
    4: [2, 3, 5],
    5: [1, 2, 3, 4]
}

# полу-Эйлер
G2 = {
    1: [2, 3, 5],
    2: [1, 4, 5, 6],
    3: [1, 4, 5],
    4: [2, 3, 5, 6],
    5: [1, 2, 3, 4],
    6: [2, 4]
}

# Эйлер
G3 = {
    1: [2, 3, 5, 7],
    2: [1, 4, 5, 6],
    3: [1, 4, 5, 7],
    4: [2, 3, 5, 6],
    5: [1, 2, 3, 4],
    6: [2, 4],
    7: [1, 3]
}

def dfs(g, v, path):
    #print("path =", path)
    if len(path)-1 == len(edgeSet): 
        return path
    for w in g[v]:
        if not {v, w} in path: 
            path.append({v, w})
            newpath = dfs(g, w, path) 
            if newpath: 
                return newpath 
    path.pop()
   
def dict_to_setEdge(G):
    s = []
    for v in G:
        for z in G[v]:
            if not {v, z} in s:
                s.append({v, z})
    return s

   
v = [1, 2]

for i,g in enumerate([G1, G2, G3]):
    edgeSet = dict_to_setEdge(g)
    print("==== граф ===", i+1)
    for s in v:
        print(" для вершины =", s)
        p = dfs(g, s, [{s, None}])
        if not p:
            print("Эйлер и полу-Эйлер не найдены")
        elif len({s, None}.intersection(p[-1])):
            print("Эйлер найден", p[1:])
        else: print("Полу-Эйлер найден", p[1:])
        print()

Теперь модифицируем формат данных так, чтобы была возможность обработки мульти-графов. Для этого, во-первых, добавим именованные ребра. Во-вторых, к двум смежным вершинам (3,7) добавим еще одно ребро (для G5) или еще два ребра (для G4). Соответственно, G4 Эйлеровым графом останется.

# Эйлер
G4 = {
    1: [(2,1), (3,1), (5,1), (7,1)],
    2: [(1,1), (4,1), (5,1), (6,1)],
    3: [(1,1), (4,1), (5,1), (7,1), (7,2), (7,3)],
    4: [(2,1), (3,1), (5,1), (6,1)],
    5: [(1,1), (2,1), (3,1), (4,1)],
    6: [(2,1), (4,1)],
    7: [(1,1), (3,1), (3,2), (3,3)]
}

# не Эйлер
G5 = {
    1: [(2,1), (3,1), (5,1), (7,1)],
    2: [(1,1), (4,1), (5,1), (6,1)],
    3: [(1,1), (4,1), (5,1), (7,1), (7,2)],
    4: [(2,1), (3,1), (5,1), (6,1)],
    5: [(1,1), (2,1), (3,1), (4,1)],
    6: [(2,1), (4,1)],
    7: [(1,1), (3,1), (3,2)]
}

def dfs(g, v, path):
    #print("path =", path)
    if len(path)-1 == len(edgeSet):
        return path
    
    for w in g[v]:
        if not ({v, w[0]}, w[1]) in path: 
            path.append(({v, w[0]}, w[1]))
            newpath = dfs(g, w[0], path) 
            if newpath: 
                return newpath 
    path.pop()
   
def dict_to_setEdge(G):
    s = []
    for v in G:
        for z in G[v]:
            if not ({v, z[0]}, z[1]) in s:
                s.append(({v, z[0]}, z[1]))
    return s

    
v = [1, 2]

for i,g in enumerate([G4, G5]):
    edgeSet = dict_to_setEdge(g)
    #print(edgeSet); input()
    print("\n==== граф ===", i+1)
    for s in v:
        print("для вершины =", s)
        p = dfs(g, s, [{s, None}])
        if not p:
            print("Эйлер и полу-Эйлер не найден")
        elif len({s, None}.intersection(p[-1][0])):
            print("Эйлер найден", p[1:])
        else: print("Полу-Эйлер найден", p[1:])
        print()

Вот и все.

Комментарии (8)


  1. wataru
    29.03.2024 15:33
    +1

    Во-первых, стоит в начале статьи дать определение эйлеровому пути.

    Во-вторых, где у вас тут "ребра вместо вершин". У вас похоже самый обычный алгоритм, основанный на вычеркивании циклов.

    И главное, реализация у вас очень-очень неэффективная. Из-за того, что вы проходите каждое ребро в каждом рекурсивном вызове, да еще и ищите его в списке каждый раз, ваш dfs работает за O(E^3), Хотя должен рабоать за O(E). Реализация есть по ссылке выше. Надо для каждой вершины хранить счетчик уже пройденных ребер и в каждой итерации цикла брать первое неиспользованное ребро, вместо цикла по всем элементам массива исходящих ребер (обратите внимание, оно может измениться в рекурсивном вызове). Фактически, это эффективная реализация удаления ребра из графа, как описано в оригинальном алгоритме.

    Ну, и не надо пытаться запускать dfs от каждой вершины. Тут даже правильная реализация рекурсивного алгоритма будет работать за квадрат на графах, где эйлерового пути/цикла нет. А ваша реализация будет работать вообще за O(VE^3). Более неэффективную реализацию придумать сложно, даже если попытаться.
    Надо посчитать степени вершин (сколько ребер из них выходит) и найти все с нечетными степенями. Если таких 0, то цикл найдется из любой вершины. Если их 2, то можно запуститься из любой из них и найдется путь. В противном случае никаких эйлеровых циклов/путей в графе нет. Ну, еще, наверно, стоит проверить, что найденный путь содержит нужное количество ребер, ибо если граф несвязен, то вы найдете лишь путь по одной компоненте.


    1. bvv2311 Автор
      29.03.2024 15:33

      ваш dfs работает за O(E^3)

      Разве была претензия на более низкую алгоритмическую сложность по сравнению с существующими алгоритмами? Справочнику - привет.

      где у вас тут "ребра вместо вершин"

      Именно "ребра вместо вершин". Возьмите обычный поиск с возвратом, который работает именно с вершинами.

      
      graph = {'A': ['B', 'C'],
                   'B': ['C', 'D'],
                   'C': ['D'],
                   'D': ['C'],
                   'E': ['F'],
                   'F': ['C']}
      
      #
      print('\n  __find_path')             
      def find_path(graph, start, end, path=[]):
              path = path + [start]
              if start == end:
                  return path
              if not start in graph:
                  return None
              for node in graph[start]:
                  if node not in path:
                      newpath = find_path(graph, node, end, path)
                      if newpath: return newpath
              return None
              
      print(find_path(graph, 'A', 'D')) # ['A', 'B', 'C', 'D']

      Ну, и не надо пытаться запускать dfs от каждой вершины

      Задача ведь не только в том, чтобы определить является граф Эйлеровым или полу-Эйлеровым. Задача показать из каких стартовых вершин Эйлеровый путь находится, а из каких нет. Не заметили?


      1. wataru
        29.03.2024 15:33

        Разве была претензия на более низкую алгоритмическую сложность по сравнению с существующими алгоритмами?

        Тем не менее, факт что ваша реализация известного алгоритма весьма неэффективна, стоит указать. Чтобы никто из читающих случайно не стал использовать вашу реализацию.

        Задача показать из каких стартовых вершин Эйлеровый путь находится, а из каких нет. Не заметили?

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

        Все еще не надо запускаться из каждой вершины, особенно чтобы понять, что пути в графе нет.


  1. ReadOnlySadUser
    29.03.2024 15:33

    Откуда у вас edgeSet в контексте функции dfs?


    1. bvv2311 Автор
      29.03.2024 15:33

      Почему так странно использую? Есть варианты с разными выборками (и с решением других задач) из setEdge (без обсчёта ({v, w[0]}, w[1]). Но суть, применительно к статье, все равно одна.


  1. wataru
    29.03.2024 15:33

    И кстати, у вас тут не backtracking. У вас тут никакого отката назад нет - однажды пройденные ребра больше никогда не пройдутся.


    1. bvv2311 Автор
      29.03.2024 15:33

      Backtrack(x) if x is not a solution return false if x is a new solution add to list of solutions backtrack(expand x)


      1. wataru
        29.03.2024 15:33

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

        def dfs(g, v, path, used, reverse_id):   
            while used[v] < len(g[v]):
                used[v] = used[v] + 1
                u = g[v][used[v]-1]
                reverse_num = reverse_id[v][used[v]-1]
                if reverse_num >= used[u]:  # Обратное ребро еще не использовано.
                    dfs(g, u, path, used, reverse_id)
            path.append(v);

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

        def ConstructReverse(g):
            # Для каждой строки, для каждого значения считаем на каких
            # индексах это значение встречается.
            where = {v:{} for (v, e) in g.items()}
            for v in g:
                for (idx, e) in enumerate(g[v]):
                    if e not in where[v]: where[v][e] = []
                    where[v][e].append(idx)
            # Используя where, считаем для каждого ребра, где в списке
            # для второй вершины лежит его обратное.
            reverse = {v: [0]*len(e) for (v, e) in g.items()}            
            for v in g:
                for (idx, e) in enumerate(g[v]):
                    reverse[v][idx] = where[e][v][-1]
                    where[e][v].pop()
            return reverse

        Ну и вызывать это все удовольствие надо так:

        def FindEulerPath(g):
            good_starts = [v for (v,edges) in g.items() if len(edges) % 2 == 1]
            if good_starts and len(good_starts) != 2: return []
            start = good_starts[0] if good_starts else next(iter(g.keys()), None)
        
            path = []
            used = [0]*(len(g)+1)
            dfs(g, start, path, used, ConstructReverse(g))
            return path
        
        G3 = {
            1: [2, 3, 5, 7],
            2: [1, 4, 5, 6],
            3: [1, 4, 5, 7],
            4: [2, 3, 5, 6],
            5: [1, 2, 3, 4],
            6: [2, 4],
            7: [1, 3]
        }    
        
        print (FindEulerPath(G3))