Алгоритм построения Эйлерова цикла — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
м
м (См. также)
(не показано 14 промежуточных версий 4 участников)
Строка 1: Строка 1:
== Описание алгоритма ==
+
== Алгоритм ==
 +
=== Описание алгоритма ===
 
Алгоритм находит [[Эйлеров цикл, Эйлеров путь, Эйлеровы графы, Эйлеровость орграфов|Эйлеров цикл]] как в [[Ориентированный граф|ориентированном]], так и в [[Основные определения теории графов#Неориентированные графы|неориентированном графе]]. Перед запуском алгоритма необходимо [[Эйлеров цикл, Эйлеров путь, Эйлеровы графы, Эйлеровость орграфов|проверить граф на эйлеровость]]. Чтобы построить [[Эйлеров цикл, Эйлеров путь, Эйлеровы графы, Эйлеровость орграфов|Эйлеров путь]], нужно запустить алгоритм из вершины с нечетной степенью.<br>
 
Алгоритм находит [[Эйлеров цикл, Эйлеров путь, Эйлеровы графы, Эйлеровость орграфов|Эйлеров цикл]] как в [[Ориентированный граф|ориентированном]], так и в [[Основные определения теории графов#Неориентированные графы|неориентированном графе]]. Перед запуском алгоритма необходимо [[Эйлеров цикл, Эйлеров путь, Эйлеровы графы, Эйлеровость орграфов|проверить граф на эйлеровость]]. Чтобы построить [[Эйлеров цикл, Эйлеров путь, Эйлеровы графы, Эйлеровость орграфов|Эйлеров путь]], нужно запустить алгоритм из вершины с нечетной степенью.<br>
Алгоритм напоминает поиск в глубину. Главное отличие состоит в том, что пройденными помечаются не вершины, а ребра графа. Начиная со стартовой вершины <tex>v</tex> строим путь, добавляя на каждом шаге не пройденное еще ребро, смежное с текущей вершиной. Вершины пути накапливаются в стеке <tex>S</tex>. Когда наступает такой момент, что для текущей вершины <tex>w</tex> все инцидентные ей ребра уже пройдены, записываем вершины из <tex>S</tex> в ответ, пока не встретим вершину, которой инцидентны не пройденные еще ребра. Далее продолжаем обход по не посещенным ребрам.
+
Алгоритм напоминает поиск в ширину. Главное отличие состоит в том, что пройденными помечаются не вершины, а ребра графа. Начиная со стартовой вершины <tex>v</tex> строим путь, добавляя на каждом шаге не пройденное еще ребро, смежное с текущей вершиной. Вершины пути накапливаются в [[Стек | стеке]] <tex>S</tex>. Когда наступает такой момент, что для текущей вершины <tex>w</tex> все инцидентные ей ребра уже пройдены, записываем вершины из <tex>S</tex> в ответ, пока не встретим вершину, которой инцидентны не пройденные еще ребра. Далее продолжаем обход по не посещенным ребрам.
  
== Псевдокод ==
+
=== Псевдокод ===
 
<font size=2>
 
<font size=2>
 
'''Код проверки графа на эйлеровость:'''
 
'''Код проверки графа на эйлеровость:'''
 
  '''boolean''' checkForEulerPath():
 
  '''boolean''' checkForEulerPath():
     '''int''' numberOfOdd = 0
+
     '''int''' OddVertex <tex>= 0</tex>
     '''for''' v '''in''' <tex>V</tex>
+
     '''for''' <tex>v : v</tex> <tex>\in</tex> <tex>V</tex>
         '''if''' vertexDegree(v) '''mod''' 2 == 1
+
         '''if''' <tex>\operatorname{deg}</tex>(<tex>v</tex>) '''mod''' <tex>2 == 1</tex>
             numberOfOdd = numberOfOdd + 1
+
             OddVertex++
     '''if''' numberOfOdd > 2   <font color=darkgreen>// если количество вершин с нечетной степенью больше двух, то граф не является эйлеровым</font>
+
     '''if''' OddVertex <tex> > 2 </tex><font color=darkgreen>// если количество вершин с нечетной степенью больше двух, то граф не является эйлеровым</font>
 
         '''return''' ''false''
 
         '''return''' ''false''
     '''boolean''' vis[sizeOf <tex>V</tex><font color=darkgreen>// инициализировать массив значениями ''false''</font>
+
     '''boolean''' visited(<tex>|V|</tex>, ''false'') <font color=darkgreen>// массив инициализируется значениями ''false''</font>
     '''for''' v '''in''' <tex>V</tex>
+
     '''for''' <tex>v : v</tex> <tex>\in</tex> <tex>V</tex>
         '''if''' vertexDegree(v) > 0
+
         '''if''' <tex>\operatorname{deg}</tex>(<tex>v</tex>) <tex> > 0</tex>
             dfs(v, vis)
+
             dfs(<tex>v</tex>, visited)
             break
+
             '''break'''
     '''for''' v '''in''' <tex>V</tex>
+
     '''for''' <tex>v : v</tex> <tex>\in</tex> <tex>V</tex>
         '''if''' vertexDegree(v) > 0 '''and''' '''not''' vis[v]  <font color=darkgreen>// если количество компонент связности, содержащие ребра, больше одной,</font>
+
         '''if''' <tex>\operatorname{deg}</tex>(<tex>v</tex>) <tex> > 0</tex> '''and''' '''not''' visited[<tex>v</tex>]  <font color=darkgreen>// если количество компонент связности, содержащие ребра, больше одной,</font>
             '''return''' ''false''                       <font color=darkgreen> // то граф не является эйлеровым</font>
+
             '''return''' ''false''             <font color=darkgreen> // то граф не является эйлеровым</font>
     '''return''' ''true'' // граф является эйлеровым
+
     '''return''' ''true''   <font color=darkgreen>// граф является эйлеровым</font>
 
<font color=darkgreen>// Вспомогательные функции:</font>
 
'''function''' dfs(v : Vertex, vis[] : '''boolean'''):
 
    vis[v] = ''true''
 
    '''for''' (v, u) '''in''' <tex>E</tex>
 
        '''if not''' vis[u]
 
            dfs(u, vis)
 
 
'''int''' vertexDegree(v : Vertex):
 
    '''int''' count = 0
 
    '''for''' (v, u) '''in''' <tex>E</tex>
 
        count = count + 1
 
    '''return''' count
 
  
'''Код построения эйлерова цикла:'''
+
'''Код построения эйлерова пути:'''
  '''function''' findEulerPath(v : Vertex):
+
  '''function''' findEulerPath(<tex>v</tex>): <font color=darkgreen> // если граф является полуэйлеровым, то алгоритм следует запускать из вершины нечетной степени </font>
     Stack stack
+
     '''for''' <tex>u : u \in V</tex>
     stack.push(v)
+
        '''if''' <tex>\operatorname{deg}</tex>(<tex>u</tex>) '''mod''' <tex>2 == 1</tex>
     '''while not''' stack.isEmpty()
+
            <tex>v = u</tex>
         w = stack.top()
+
            '''break'''
         '''if''' exists (w, u) '''in''' <tex>E</tex>
+
     <tex>S</tex>.push(<tex>v</tex>) <font color=darkgreen>// <tex>S</tex> {{---}} стек</font>
            stack.push(u)
+
     '''while not''' <tex>S</tex>.empty()
            remove(w, u)
+
         <tex>w = </tex> <tex>S</tex>.top()
         '''else'''  
+
         '''for''' <tex>u : u \in V</tex>
             stack.pop()
+
            '''if''' (<tex>w, u</tex>) <tex>\in E</tex> <font color=darkgreen> // нашли ребро, по которому ещё не прошли</font>
             print(w)
+
                <tex>S</tex>.push(<tex>u</tex>) <font color=darkgreen> // добавили новую вершину в стек</font>
 +
                <tex>E</tex>.remove(<tex>w, u</tex>)
 +
                '''break'''
 +
         '''if''' <tex> w == S</tex>.top()
 +
             <tex>S</tex>.pop() <font color=darkgreen> // не нашлось инцидентных вершине <tex>w</tex> рёбер, по которым ещё не прошли</font>
 +
             print(<tex>w</tex>)
 
</font>
 
</font>
  
== Доказательство ==
+
=== Доказательство корректности ===
Пусть <tex>P</tex> {{---}} напечатанный путь. Заметим, что первой в <tex>S</tex> помещается вершина <tex>v</tex>, и она будет последней перемещена из <tex>S</tex> в <tex>P</tex>. Следовательно, она будет последней вершиной в <tex>P</tex>. Далее, первый раз, когда обнаружится, что все инцидентные активной вершине ребра пройдены, активной будет стартовая вершина <tex>v</tex> (Так как степени всех вершин четны).  Значит, эта вершина будет первой перемещена из <tex>S</tex> в <tex>P</tex>. Итак, по окончании работы алгоритма в начале и в конце последовательности вершин, содержащейся в <tex>P</tex>, находится вершина <tex>v</tex>. Иначе говоря, если эта последовательность представляет маршрут, то этот маршрут замкнут.<br>
+
{{Лемма
Покажем, что <tex>P</tex> это маршрут содержащий все ребра.<br>
+
|statement=Данный алгоритм проходит по каждому ребру, причем ровно один раз.
Допустим, что в момент окончания работы алгоритма имеются еще не пройденные ребра. Поскольку граф связен, должно существовать хотя бы одно непройденное ребро, инцидентное посещенной вершине. Но тогда эта вершина не могла быть удалена из <tex>S</tex>, и <tex>S</tex> не мог стать пустым.<br>
+
|proof=Допустим, что в момент окончания работы алгоритма имеются еще не пройденные ребра. Поскольку граф связен, должно существовать хотя бы одно не пройденное ребро, инцидентное посещенной вершине. Но тогда эта вершина не могла быть удалена из стека <tex>S</tex>, и он не мог стать пустым. Значит алгоритм пройдёт по всем рёбрам хотя бы один раз.
Будем говорить, что ребро <tex>(w,u)</tex> представлено в <tex>S</tex> или <tex>P</tex>, если в какой-то момент работы алгоритма вершины <tex>w</tex> и <tex>u</tex> находятся рядом. Каждое ребро графа представлено в <tex>S</tex>. Допустим в какой-то момент из <tex>S</tex> в <tex>P</tex> перемещена вершина <tex>w</tex>, а следующей в <tex>S</tex> лежит <tex>u</tex>. Возможны 2 варианта:
+
Но так как после прохода по ребру оно удаляется, то пройти по нему дважды алгоритм не может.<br>
#На следующем шаге <tex>u</tex> перемещена в <tex>S</tex>. Тогда <tex>(w,u)</tex> представлено в <tex>P</tex>.
+
}}
#Сначала будет пройдена некоторая последовательность ребер, начинающаяся в вершине <tex>u</tex>. Ввиду четности степеней эта последовательность может закончиться только в вершине <tex>u</tex>, а значит она следующей попадет в <tex>P</tex> и <tex>(w,u)</tex> будет представлено в <tex>P</tex>.
+
Вершина <tex>v</tex>, с которой начат обход графа, будет последней помещена в путь <tex>P</tex>. Так как изначально стек пуст, и вершина <tex>v</tex> входит в стек первой, то после прохода по инцидентным ребрам, алгоритм возвращается к данной вершине, выводит ее и опустошает стек, затем выполнение программы завершается.<br>
 
+
{{Лемма
Отсюда понятно, что последовательность вершин в <tex>P</tex> является маршрутом и что каждое ребро графа в конечном итоге будет содержаться в этом маршруте, причем один раз.<br>
+
|statement=Напечатанный путь <tex>P</tex> {{---}} корректный маршрут в графе, в котором каждые две соседние вершины <tex>u_i</tex> и <tex>u_{i+1}</tex> будут образовывать ребро <tex>(u_i, u_{i+1}) \in E</tex>.
 
+
|proof=Будем говорить, что ребро <tex>(w,u)</tex> представлено в <tex>S</tex> или <tex>P</tex>, если в какой-то момент работы алгоритма вершины <tex>w</tex> и <tex>u</tex> находятся рядом. Каждое ребро графа представлено в <tex>S</tex>. Рассмотрим случай, когда из <tex>S</tex> в <tex>P</tex> перемещена вершина <tex>u</tex>, а следующей в <tex>S</tex> лежит <tex>w</tex>. Возможны 2 варианта:
<tex> \Box </tex>
+
*На следующем шаге для вершины <tex>w</tex> не найдётся инцидентного ребра, тогда <tex>w</tex> переместят в <tex>P</tex>, и ребро <tex>(w,u)</tex> будет представлено в <tex>P</tex>.
 
+
*Иначе будет пройдена некоторая последовательность ребер <tex>{u_1, u_2, ..., u_k}</tex>, начинающаяся в вершине <tex>w</tex> и проходящая по ребру <tex>(w, u_1)</tex>. Докажем, что данный проход <tex>{u_1, u_2, ..., u_k}</tex> закончится в вершине <tex>w</tex>:
== Рекурсивная реализация ==  
+
#Ребро <tex>(u_{k-1}, u_k)</tex> не может быть инцидентно вершинам <tex>u_1, \dots , u_{k-2}</tex>, иначе степень вершины <tex>u_k</tex> окажется нечетной.
 +
#Предположим, что <tex>(u_{k-1}, u_k)</tex> инцидентно вершине, пройденной при обходе графа из вершины <tex>u</tex>. Но это неверно, так как тогда бы данные вершины пройдены ранее.
 +
Из этого следует, что мы закончим обход в вершине <tex>w</tex>. Следовательно, данная вершина первой поместится в <tex>P</tex> вслед за <tex>u</tex>, и ребро <tex>(w, u)</tex> будет представлено в <tex>P</tex>.
 +
}}
 +
{{Теорема
 +
|id=proof1
 +
|statement=Данный алгоритм находит корректный эйлеров путь.
 +
|proof=Из предыдущих лемм следует, что <tex>P</tex> {{---}} искомый эйлеров путь и алгоритм работает корректно.
 +
}}
 +
=== Рекурсивная реализация ===
 
<font size=2>
 
<font size=2>
  '''function''' findEulerPath(v):
+
  '''function''' findEulerPath(<tex>v</tex> : Vertex):
     '''for''' (v, u) '''in''' <tex>E</tex>
+
     '''for''' <tex>(v,u)</tex> <tex>\in</tex> <tex>E</tex>
         remove(v, u)
+
         remove <tex>(v, u)</tex>
         findEulerPath(u)
+
         findEulerPath(<tex>u</tex>)
     print(v)
+
     print(<tex>v</tex>)
 
</font>
 
</font>
 
+
=== Время работы ===
== Время работы ==
 
 
Если реализовать поиск ребер инцидентных вершине и удаление ребер за <tex>O(1)</tex>, то алгоритм будет работать за <tex>O(E)</tex>.<br>
 
Если реализовать поиск ребер инцидентных вершине и удаление ребер за <tex>O(1)</tex>, то алгоритм будет работать за <tex>O(E)</tex>.<br>
Чтобы реализовать поиск за <tex>O(1)</tex>, для хранения графа следует использовать списки смежных вершин. Тогда удаление ребер будет выглядеть следующим образом:
+
Чтобы реализовать поиск за <tex>O(1)</tex>, для хранения графа следует использовать списки смежных вершин; для удаления достаточно добавить всем ребрам свойство <tex>\mathtt{deleted}</tex> бинарного типа.
<font size=2>
 
'''function''' remove(v : Vertex, index : '''int'''):
 
    graph[v][index] = graph[v].back()
 
    graph[v].pop()
 
</font>
 
  
 
== См. также ==
 
== См. также ==
* [[Эйлеров цикл, Эйлеров путь, Эйлеровы графы, Эйлеровость орграфов]]
+
* [[Гамильтоновы графы]]
* [[Покрытие ребер графа путями]]
+
* [[Покрытие рёбер графа путями]]
 +
* [[Произвольно вычерчиваемые из заданной вершины графы]]
  
 
== Источники информации ==
 
== Источники информации ==
Строка 91: Строка 88:
 
[[Категория: Алгоритмы и структуры данных]]
 
[[Категория: Алгоритмы и структуры данных]]
 
[[Категория: Обходы графов]]
 
[[Категория: Обходы графов]]
 +
[[Категория: Эйлеровы графы]]

Версия 00:00, 31 января 2017

Алгоритм

Описание алгоритма

Алгоритм находит Эйлеров цикл как в ориентированном, так и в неориентированном графе. Перед запуском алгоритма необходимо проверить граф на эйлеровость. Чтобы построить Эйлеров путь, нужно запустить алгоритм из вершины с нечетной степенью.
Алгоритм напоминает поиск в ширину. Главное отличие состоит в том, что пройденными помечаются не вершины, а ребра графа. Начиная со стартовой вершины [math]v[/math] строим путь, добавляя на каждом шаге не пройденное еще ребро, смежное с текущей вершиной. Вершины пути накапливаются в стеке [math]S[/math]. Когда наступает такой момент, что для текущей вершины [math]w[/math] все инцидентные ей ребра уже пройдены, записываем вершины из [math]S[/math] в ответ, пока не встретим вершину, которой инцидентны не пройденные еще ребра. Далее продолжаем обход по не посещенным ребрам.

Псевдокод

Код проверки графа на эйлеровость:

boolean checkForEulerPath():
   int OddVertex [math]= 0[/math]
   for [math]v : v[/math] [math]\in[/math] [math]V[/math]
       if [math]\operatorname{deg}[/math]([math]v[/math]) mod [math]2 == 1[/math]
           OddVertex++
   if OddVertex [math] \gt  2 [/math]// если количество вершин с нечетной степенью больше двух, то граф не является эйлеровым
       return false
   boolean visited([math]|V|[/math], false) // массив инициализируется значениями false
   for [math]v : v[/math] [math]\in[/math] [math]V[/math]
       if [math]\operatorname{deg}[/math]([math]v[/math]) [math] \gt  0[/math]
           dfs([math]v[/math], visited)
           break
   for [math]v : v[/math] [math]\in[/math] [math]V[/math]
       if [math]\operatorname{deg}[/math]([math]v[/math]) [math] \gt  0[/math] and not visited[[math]v[/math]]   // если количество компонент связности, содержащие ребра, больше одной,
           return false              // то граф не является эйлеровым
   return true   // граф является эйлеровым

Код построения эйлерова пути:

function findEulerPath([math]v[/math]):   // если граф является полуэйлеровым, то алгоритм следует запускать из вершины нечетной степени 
   for [math]u : u \in V[/math]
       if [math]\operatorname{deg}[/math]([math]u[/math]) mod [math]2 == 1[/math]
           [math]v = u[/math]
           break
   [math]S[/math].push([math]v[/math])  // [math]S[/math] — стек
   while not [math]S[/math].empty()
       [math]w = [/math] [math]S[/math].top()
       for [math]u : u \in V[/math]
           if ([math]w, u[/math]) [math]\in E[/math]  // нашли ребро, по которому ещё не прошли
               [math]S[/math].push([math]u[/math])  // добавили новую вершину в стек
               [math]E[/math].remove([math]w, u[/math])
               break
       if [math] w == S[/math].top()
           [math]S[/math].pop()  // не нашлось инцидентных вершине [math]w[/math] рёбер, по которым ещё не прошли
           print([math]w[/math])

Доказательство корректности

Лемма:
Данный алгоритм проходит по каждому ребру, причем ровно один раз.
Доказательство:
[math]\triangleright[/math]

Допустим, что в момент окончания работы алгоритма имеются еще не пройденные ребра. Поскольку граф связен, должно существовать хотя бы одно не пройденное ребро, инцидентное посещенной вершине. Но тогда эта вершина не могла быть удалена из стека [math]S[/math], и он не мог стать пустым. Значит алгоритм пройдёт по всем рёбрам хотя бы один раз.

Но так как после прохода по ребру оно удаляется, то пройти по нему дважды алгоритм не может.
[math]\triangleleft[/math]

Вершина [math]v[/math], с которой начат обход графа, будет последней помещена в путь [math]P[/math]. Так как изначально стек пуст, и вершина [math]v[/math] входит в стек первой, то после прохода по инцидентным ребрам, алгоритм возвращается к данной вершине, выводит ее и опустошает стек, затем выполнение программы завершается.

Лемма:
Напечатанный путь [math]P[/math] — корректный маршрут в графе, в котором каждые две соседние вершины [math]u_i[/math] и [math]u_{i+1}[/math] будут образовывать ребро [math](u_i, u_{i+1}) \in E[/math].
Доказательство:
[math]\triangleright[/math]

Будем говорить, что ребро [math](w,u)[/math] представлено в [math]S[/math] или [math]P[/math], если в какой-то момент работы алгоритма вершины [math]w[/math] и [math]u[/math] находятся рядом. Каждое ребро графа представлено в [math]S[/math]. Рассмотрим случай, когда из [math]S[/math] в [math]P[/math] перемещена вершина [math]u[/math], а следующей в [math]S[/math] лежит [math]w[/math]. Возможны 2 варианта:

  • На следующем шаге для вершины [math]w[/math] не найдётся инцидентного ребра, тогда [math]w[/math] переместят в [math]P[/math], и ребро [math](w,u)[/math] будет представлено в [math]P[/math].
  • Иначе будет пройдена некоторая последовательность ребер [math]{u_1, u_2, ..., u_k}[/math], начинающаяся в вершине [math]w[/math] и проходящая по ребру [math](w, u_1)[/math]. Докажем, что данный проход [math]{u_1, u_2, ..., u_k}[/math] закончится в вершине [math]w[/math]:
  1. Ребро [math](u_{k-1}, u_k)[/math] не может быть инцидентно вершинам [math]u_1, \dots , u_{k-2}[/math], иначе степень вершины [math]u_k[/math] окажется нечетной.
  2. Предположим, что [math](u_{k-1}, u_k)[/math] инцидентно вершине, пройденной при обходе графа из вершины [math]u[/math]. Но это неверно, так как тогда бы данные вершины пройдены ранее.
Из этого следует, что мы закончим обход в вершине [math]w[/math]. Следовательно, данная вершина первой поместится в [math]P[/math] вслед за [math]u[/math], и ребро [math](w, u)[/math] будет представлено в [math]P[/math].
[math]\triangleleft[/math]
Теорема:
Данный алгоритм находит корректный эйлеров путь.
Доказательство:
[math]\triangleright[/math]
Из предыдущих лемм следует, что [math]P[/math] — искомый эйлеров путь и алгоритм работает корректно.
[math]\triangleleft[/math]

Рекурсивная реализация

function findEulerPath([math]v[/math] : Vertex):
   for [math](v,u)[/math] [math]\in[/math] [math]E[/math]
       remove [math](v, u)[/math]
       findEulerPath([math]u[/math])
   print([math]v[/math])

Время работы

Если реализовать поиск ребер инцидентных вершине и удаление ребер за [math]O(1)[/math], то алгоритм будет работать за [math]O(E)[/math].
Чтобы реализовать поиск за [math]O(1)[/math], для хранения графа следует использовать списки смежных вершин; для удаления достаточно добавить всем ребрам свойство [math]\mathtt{deleted}[/math] бинарного типа.

См. также

Источники информации