Пошук усіх циклів у спрямованому графіку


198

Як я можу знайти (повторити) ВСІ цикли у спрямованому графіку від / до заданого вузла?

Наприклад, я хочу щось подібне:

A->B->A
A->B->C->A

але не: B-> C-> B


1
Домашнє завдання я припускаю? me.utexas.edu/~bard/IP/Handouts/cycles.pdf не те, що це неправдиве питання :)
ShuggyCoUk

5
Зауважте, що це принаймні NP Hard. Можливо, PSPACE, я повинен був би подумати про це, але рано вранці для теорії складності B-)
Брайан Постув

2
Якщо у вашому вхідному графіку є v вершини та e краї, то є 2 ^ (e - v +1) -1 різних циклів (хоча не всі можуть бути простими циклами). Це досить багато - можливо, ви не хочете прямо писати їх усі. Крім того, оскільки розмір виходу є експоненціальним, складність алгоритму не може бути поліноміальною. Я думаю, що досі немає відповіді на це питання.
CygnusX1

1
Найкращим варіантом для мене було таке: personal.kent.edu/~rmuhamma/Algorithms/MyAlgorithms/GraphAlgor/…
Мелсі

Відповіді:


105

Я знайшов цю сторінку в моєму пошуку, і оскільки цикли не такі, як сильно пов'язані компоненти, я продовжував шукати і, нарешті, знайшов ефективний алгоритм, який перераховує всі (елементарні) цикли спрямованого графа. Це від Дональда Б. Джонсона, а документ можна знайти за наступним посиланням:

http://www.cs.tufts.edu/comp/150GA/homeworks/hw1/Johnson%2075.PDF

Реалізацію Java можна знайти в:

http://normalisiert.de/code/java/elementarCycles.zip

Mathematica демонстрація алгоритму Джонсона можна знайти тут , реалізація може бути завантажена з правого боку ( «Завантажити автор коду» ).

Примітка. Насправді існує багато алгоритмів цієї проблеми. Деякі з них перераховані в цій статті:

http://dx.doi.org/10.1137/0205007

Відповідно до статті, алгоритм Джонсона є найшвидшим.


1
Я вважаю таким клопотом реалізувати з паперу, і в кінцевому підсумку цей аглоритм все ж вимагає реалізації Тарджана. І Java-код теж огидний. :(
Глено

7
@Gleno Добре, якщо ви маєте на увазі, що ви можете використовувати Tarjan для пошуку всіх циклів у графіку, а не для реалізації решти, ви помиляєтесь. Тут ви можете побачити різницю між сильно пов'язаними компонентами та всіма циклами (цикли cd і gh не повернуться водоростями Таряна) (@ batbrat Відповідь вашої плутанини також прихована тут: Усі можливі цикли не повертаються Tarjan's alg, тому її складність може бути меншою, ніж експоненціальна). Java-код міг би бути кращим, але це врятувало мене зусиллями щодо впровадження з паперу.
eminsenay

4
Ця відповідь набагато краща за обрану відповідь. Я довгий час боровся, намагаючись зрозуміти, як отримати всі прості цикли з сильно з'єднаних компонентів. Виявляється, це нетривіально. Документ Джонсона містить чудовий алгоритм, але його важко проникнути. Я подивився на реалізацію Java і прокатав свою власну в Matlab. Код доступний на веб- сайті gist.github.com/1260153 .
codehippo

5
@moteutsch: Можливо, мені чогось не вистачає, але, згідно з паперами Джонсона (та іншими джерелами), цикл є елементарним, якщо жодна вершина (крім старту / кінця) не з’являється більше одного разу. За цим визначенням, A->B->C->Aтеж не елементарно?
psmears

9
Примітка для всіх, хто для цього використовує python: алгоритм Джонсона реалізований як simple_cycleу networkx.
Джоель

35

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

DFS легко реалізувати, якщо у вас є список суміжності для представлення графіка. Наприклад, adj [A] = {B, C} вказує, що B і C - діти А.

Наприклад, псевдо-код нижче. "start" - це вузол, з якого ви починаєте.

dfs(adj,node,visited):  
  if (visited[node]):  
    if (node == start):  
      "found a path"  
    return;  
  visited[node]=YES;  
  for child in adj[node]:  
    dfs(adj,child,visited)
  visited[node]=NO;

Викличте вищезгадану функцію за допомогою вузла запуску:

visited = {}
dfs(adj,start,visited)

2
Дякую. Я віддаю перевагу такому підходу перед деякими з інших, відзначених тут, оскільки це просто (r) зрозуміти і має розумну часову складність, хоча і, можливо, не оптимальну.
redcalx

1
як це знаходить усі цикли?
штурм мозку

3
if (node == start): - що node and startв першому дзвінку
штурм мозку

2
@ user1988876 Здається, це знайде всі цикли, що містять задану вершину (що було б start). Він починається з цієї вершини і робить DFS, поки не повернеться до цієї вершини знову, тоді він знає, що знайшов цикл. Але це насправді не виводить цикли, а лише їх кількість (але модифікувати його для цього замість не повинно бути занадто складно).
Бернхард Баркер

1
@ user1988876 Ну, він просто друкує "знайдений шлях" у раз, який дорівнює кількості знайдених циклів (це можна легко замінити підрахунком). Так, він буде виявляти цикли лише з start. Вам дійсно не потрібно очищати відвідувані прапори, оскільки кожен відвідуваний прапор буде очищений через visited[node]=NO;. Але майте на увазі, що якщо у вас цикл A->B->C->A, ви виявите це 3 рази, як startі будь-які 3 з них. Одна ідея запобігти цьому - це мати ще один відвідуваний масив, де встановлюється кожен вузол, який був startвузлом у якийсь момент, і тоді ви їх не переглядаєте.
Бернхард Баркер

23

Перш за все - ви не дуже хочете намагатися знайти буквально всі цикли, тому що якщо є 1, то існує нескінченна кількість таких. Наприклад, ABA, ABABA і т. Д. Або можливо, можна об'єднати два цикли в 8-подібний цикл і т. Д. І т.д. ... Змістовний підхід полягає в пошуку всіх так званих простих циклів - тих, які не перетинають себе, крім в початковій / кінцевій точці. Тоді, якщо ви хочете, ви можете створити комбінації простих циклів.

Один з базових алгоритмів пошуку всіх простих циклів у спрямованому графіку полягає в наступному: Проведіть в графі перше глибину всіх простих шляхів (тих, які не перетинаються). Кожного разу, коли поточний вузол має наступника на стеку, виявляється простий цикл. Він складається з елементів на стеці, починаючи з ідентифікованого наступника і закінчуючи вершиною стека. Перше проходження по глибині всіх простих шляхів схоже на пошук по глибині, але ви не відзначаєте / не записуєте відвідувані вузли, окрім тих, що зараз є у стеку, як точки зупинки.

Наведений вище алгоритм грубої сили є надзвичайно неефективним, і на додаток до цього створюється кілька копій циклів. Однак це є відправною точкою для численних практичних алгоритмів, які застосовують різні вдосконалення з метою підвищення продуктивності та уникнення дублювання циклів. Я здивовано дізнався деякий час тому, що ці алгоритми недоступні в підручниках та в Інтернеті. Тому я провів кілька досліджень і реалізував 4 таких алгоритми та 1 алгоритм для циклів у непрямих графіках у бібліотеці Java з відкритим кодом: http://code.google.com/p/niographs/ .

BTW, оскільки я згадав непрямі графіки: Алгоритм для них інший. Побудуйте дерево, що перекидається, і тоді кожен край, який не є частиною дерева, утворює простий цикл разом з деякими краями в дереві. Знайдені таким чином цикли утворюють так звану базу циклу. Всі прості цикли можуть бути потім знайдені шляхом комбінування 2 або більше різних базових циклів. Докладніші відомості див. Наприклад: http://dspace.mit.edu/bitstream/handle/1721.1/68106/FTL_R_1982_07.pdf .


Як приклад, як використовувати те, jgraphtщо використовується у http://code.google.com/p/niographs/вас, ви можете взяти приклад з github.com/jgrapht/jgrapht/wiki/DirectedGraphDemo
Vishrant

19

Найпростішим вибором, який я знайшов для вирішення цієї проблеми, було використання lib python networkx.

Він реалізує алгоритм Джонсона, згаданий у найкращій відповіді на це питання, але він робить його досить простим.

Коротше вам потрібно наступне:

import networkx as nx
import matplotlib.pyplot as plt

# Create Directed Graph
G=nx.DiGraph()

# Add a list of nodes:
G.add_nodes_from(["a","b","c","d","e"])

# Add a list of edges:
G.add_edges_from([("a","b"),("b","c"), ("c","a"), ("b","d"), ("d","e"), ("e","a")])

#Return a list of cycles described as a list o nodes
list(nx.simple_cycles(G))

Відповідь: [['a', 'b', 'd', 'e'], ['a', 'b', 'c']]

введіть тут опис зображення


1
Ви також можете конвертувати словник у мережевий графік:nx.DiGraph({'a': ['b'], 'b': ['c','d'], 'c': ['a'], 'd': ['e'], 'e':['a']})
Лука Майлз

Як вказати початкову вершину?
ніссенс

5

Для уточнення:

  1. Міцно підключені компоненти знайдуть усі підграграфи, що мають принаймні один цикл, а не всі можливі цикли в графі. наприклад, якщо ви берете всі сильно з’єднані компоненти та згортаєте / згрупуєте / об'єднуєте кожен з них в один вузол (тобто вузол на компонент), ви отримаєте дерево без циклів (фактично DAG). Кожен компонент (який в основному є підграфом з принаймні одним циклом у ньому) може містити багато більше можливих циклів внутрішньо, тому SCC НЕ знайде всіх можливих циклів, він знайде всі можливі групи, що мають принаймні один цикл, і якщо ви групуєте їх, тоді на графіку не буде циклів.

  2. щоб знайти всі прості цикли у графіку, як згадували інші, алгоритм Джонсона є кандидатом.


3

Мені одного разу мені дали це питання для інтерв'ю, я підозрюю, що з вами це сталося, і ви приїжджаєте сюди за допомогою. Розбийте проблему на три питання, і це стає простіше.

  1. як визначити наступний дійсний маршрут
  2. як визначити, чи була використана точка
  3. як уникнути повторного переходу через ту саму точку

Проблема 1) Використовуйте шаблон ітератора, щоб вказати спосіб ітерації результатів маршруту. Хорошим місцем для логіки отримання наступного маршруту є, мабуть, "moveNext" вашого ітератора. Щоб знайти дійсний маршрут, це залежить від вашої структури даних. Для мене це була таблиця sql, повна дійсних можливостей маршруту, тому мені довелося створити запит, щоб отримати дійсні місця призначення з джерелом.

Проблема 2) Натискайте на кожен вузол, коли ви знаходите їх у колекції, коли ви їх отримуєте, це означає, що ви можете побачити, чи ви «легко подвоюєтесь» над точкою дуже легко, допитуючи колекцію, яку ви будуєте на льоту.

Проблема 3) Якщо в будь-який момент ви побачите, що ви подвоюєтесь назад, ви можете вискакувати речі з колекції та "створити резервну копію". Потім з цього моменту спробуйте "рухатися вперед" знову.

Хак: якщо ви використовуєте Sql Server 2008, є деякі нові "ієрархічні" речі, які ви можете використовувати для швидкого вирішення цього питання, якщо структурувати свої дані в дереві.


3

Варіанти на основі DFS із задніми краями дійсно знайдуть цикли, але у багатьох випадках це НЕ буде мінімальним циклом. Загалом DFS дає вам прапор, що існує цикл, але він недостатньо хороший, щоб реально знаходити цикли. Наприклад, уявіть 5 різних циклів, які ділять два ребра. Не існує простого способу ідентифікації циклів за допомогою лише DFS (включаючи варіанти зворотного відстеження).

Алгоритм Джонсона справді дає всі унікальні прості цикли і має гарний час та простір.

Але якщо ви хочете просто знайти МІНІМАЛЬНІ цикли (це означає, що може бути більше одного циклу, який проходить через будь-яку вершину, і ми зацікавлені в пошуку мінімальних) І ваш графік не дуже великий, ви можете спробувати скористатися простим методом, наведеним нижче. Це ДУЖЕ просто, але досить повільно порівняно з Джонсоном.

Так, один з абсолютно простий спосіб знайти МІНІМАЛЬНІ циклів полягає в використанні алгоритму Флойда , щоб знайти мінімальні шляху між усіма вершинами , використовуючи матрицю суміжності. Цей алгоритм ніде не є настільки оптимальним, як у Джонсона, але він настільки простий і його внутрішній цикл настільки щільний, що для менших графіків (<= 50-100 вузлів) його абсолютно сенс використовувати. Часова складність становить O (n ^ 3), складність простору O (n ^ 2), якщо ви використовуєте батьківське відстеження, а O (1), якщо цього немає. Насамперед знайдемо відповідь на питання, чи є цикл. Алгоритм мертвий-простий. Нижче фрагмент у Scala.

  val NO_EDGE = Integer.MAX_VALUE / 2

  def shortestPath(weights: Array[Array[Int]]) = {
    for (k <- weights.indices;
         i <- weights.indices;
         j <- weights.indices) {
      val throughK = weights(i)(k) + weights(k)(j)
      if (throughK < weights(i)(j)) {
        weights(i)(j) = throughK
      }
    }
  }

Спочатку цей алгоритм працює на грані зваженого краю для пошуку всіх найкоротших шляхів між усіма парами вузлів (звідси аргумент ваг). Щоб правильно працювати, потрібно вказати 1, якщо між вузлами є спрямований край або NO_EDGE в іншому випадку. Після виконання алгоритму ви можете перевірити головну діагональ, якщо є значення менше NO_EDGE, ніж цей вузол бере участь у циклі довжини, що дорівнює значенню. Кожен інший вузол того ж циклу матиме однакове значення (на головній діагоналі).

Для реконструкції самого циклу нам потрібно використовувати трохи модифіковану версію алгоритму з відстеженням батьків.

  def shortestPath(weights: Array[Array[Int]], parents: Array[Array[Int]]) = {
    for (k <- weights.indices;
         i <- weights.indices;
         j <- weights.indices) {
      val throughK = weights(i)(k) + weights(k)(j)
      if (throughK < weights(i)(j)) {
        parents(i)(j) = k
        weights(i)(j) = throughK
      }
    }
  }

Батьківська матриця спочатку повинна містити індекс вершини у комірці ребра, якщо між вершинами є край, а в іншому - -1. Після повернення функції для кожного краю ви матимете посилання на батьківський вузол у найкоротшому дереві шляху. І тоді легко відновити фактичні цикли.

Загалом у нас є наступна програма для пошуку всіх мінімальних циклів

  val NO_EDGE = Integer.MAX_VALUE / 2;

  def shortestPathWithParentTracking(
         weights: Array[Array[Int]],
         parents: Array[Array[Int]]) = {
    for (k <- weights.indices;
         i <- weights.indices;
         j <- weights.indices) {
      val throughK = weights(i)(k) + weights(k)(j)
      if (throughK < weights(i)(j)) {
        parents(i)(j) = parents(i)(k)
        weights(i)(j) = throughK
      }
    }
  }

  def recoverCycles(
         cycleNodes: Seq[Int], 
         parents: Array[Array[Int]]): Set[Seq[Int]] = {
    val res = new mutable.HashSet[Seq[Int]]()
    for (node <- cycleNodes) {
      var cycle = new mutable.ArrayBuffer[Int]()
      cycle += node
      var other = parents(node)(node)
      do {
        cycle += other
        other = parents(other)(node)
      } while(other != node)
      res += cycle.sorted
    }
    res.toSet
  }

і невеликий основний метод просто перевірити результат

  def main(args: Array[String]): Unit = {
    val n = 3
    val weights = Array(Array(NO_EDGE, 1, NO_EDGE), Array(NO_EDGE, NO_EDGE, 1), Array(1, NO_EDGE, NO_EDGE))
    val parents = Array(Array(-1, 1, -1), Array(-1, -1, 2), Array(0, -1, -1))
    shortestPathWithParentTracking(weights, parents)
    val cycleNodes = parents.indices.filter(i => parents(i)(i) < NO_EDGE)
    val cycles: Set[Seq[Int]] = recoverCycles(cycleNodes, parents)
    println("The following minimal cycle found:")
    cycles.foreach(c => println(c.mkString))
    println(s"Total: ${cycles.size} cycle found")
  }

і вихід є

The following minimal cycle found:
012
Total: 1 cycle found

2

Що стосується ненаправленого графа, нещодавно опублікований документ ( Оптимальний перелік циклів і st-контурів у непрямих графіках ) пропонує асимптотично оптимальне рішення. Ви можете прочитати його тут http://arxiv.org/abs/1205.2766 або тут http://dl.acm.org/citation.cfm?id=2627951 Я знаю, що він не відповідає на ваше запитання, але оскільки назва Ваше запитання не вказує напрямок, воно все ще може бути корисним для пошуку в Google


1

Почніть з вузла X і перевірте, чи всі дочірні вузли (батьківські та дочірні вузли еквівалентні, якщо непрямі). Позначте ці дочірні вузли як діти X. Від будь-якого такого дочірнього вузла A позначте, що це діти, що є дітьми A, X ', де X' позначено як 2 кроки.) Якщо пізніше ви натиснете X і позначаєте його як дочірню X '', це означає, що X знаходиться в циклі 3 вузла. Зворотне відстеження до його батьківського просто (так, алгоритм не підтримує цього, тому ви знайдете те, у кого з батьків є X ').

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


1

Якщо ви хочете знайти всі елементарні схеми в графі, ви можете скористатися алгоритмом EC від JAMES C. TIERNAN, що знаходиться на папері з 1970 року.

Дуже оригінальний алгоритм EC , як мені вдалося реалізувати в PHP (сподіваюся , що немає ніяких помилок наведені нижче). Він також може знайти петлі, якщо такі є. Схеми в цій реалізації (які намагаються клонувати оригінал) є ненульовими елементами. Нуль тут означає неіснування (нульове, як ми це знаємо).

Крім того, що нижче випливає інша реалізація, яка надає алгоритму більше незалежності, це означає, що вузли можуть починатися з будь-якого місця, навіть із від'ємних чисел, наприклад, -4, -3, -2, .. і т.д.

В обох випадках потрібно, щоб вузли були послідовними.

Можливо, вам доведеться вивчити оригінальний документ, алгоритм елементарних схем Джеймса К. Тірнана

<?php
echo  "<pre><br><br>";

$G = array(
        1=>array(1,2,3),
        2=>array(1,2,3),
        3=>array(1,2,3)
);


define('N',key(array_slice($G, -1, 1, true)));
$P = array(1=>0,2=>0,3=>0,4=>0,5=>0);
$H = array(1=>$P, 2=>$P, 3=>$P, 4=>$P, 5=>$P );
$k = 1;
$P[$k] = key($G);
$Circ = array();


#[Path Extension]
EC2_Path_Extension:
foreach($G[$P[$k]] as $j => $child ){
    if( $child>$P[1] and in_array($child, $P)===false and in_array($child, $H[$P[$k]])===false ){
    $k++;
    $P[$k] = $child;
    goto EC2_Path_Extension;
}   }

#[EC3 Circuit Confirmation]
if( in_array($P[1], $G[$P[$k]])===true ){//if PATH[1] is not child of PATH[current] then don't have a cycle
    $Circ[] = $P;
}

#[EC4 Vertex Closure]
if($k===1){
    goto EC5_Advance_Initial_Vertex;
}
//afou den ksana theoreitai einai asfales na svisoume
for( $m=1; $m<=N; $m++){//H[P[k], m] <- O, m = 1, 2, . . . , N
    if( $H[$P[$k-1]][$m]===0 ){
        $H[$P[$k-1]][$m]=$P[$k];
        break(1);
    }
}
for( $m=1; $m<=N; $m++ ){//H[P[k], m] <- O, m = 1, 2, . . . , N
    $H[$P[$k]][$m]=0;
}
$P[$k]=0;
$k--;
goto EC2_Path_Extension;

#[EC5 Advance Initial Vertex]
EC5_Advance_Initial_Vertex:
if($P[1] === N){
    goto EC6_Terminate;
}
$P[1]++;
$k=1;
$H=array(
        1=>array(1=>0,2=>0,3=>0,4=>0,5=>0),
        2=>array(1=>0,2=>0,3=>0,4=>0,5=>0),
        3=>array(1=>0,2=>0,3=>0,4=>0,5=>0),
        4=>array(1=>0,2=>0,3=>0,4=>0,5=>0),
        5=>array(1=>0,2=>0,3=>0,4=>0,5=>0)
);
goto EC2_Path_Extension;

#[EC5 Advance Initial Vertex]
EC6_Terminate:
print_r($Circ);
?>

то це інша реалізація, більш незалежна від графіка, без goto та без значень масиву, замість цього вона використовує клавіші масиву, шлях, графік та схеми зберігаються як ключі масиву (використовуйте значення масиву, якщо хочете, просто змініть необхідні лінії). Приклад графіку починається з -4, щоб показати його незалежність.

<?php

$G = array(
        -4=>array(-4=>true,-3=>true,-2=>true),
        -3=>array(-4=>true,-3=>true,-2=>true),
        -2=>array(-4=>true,-3=>true,-2=>true)
);


$C = array();


EC($G,$C);
echo "<pre>";
print_r($C);
function EC($G, &$C){

    $CNST_not_closed =  false;                          // this flag indicates no closure
    $CNST_closed        = true;                         // this flag indicates closure
    // define the state where there is no closures for some node
    $tmp_first_node  =  key($G);                        // first node = first key
    $tmp_last_node  =   $tmp_first_node-1+count($G);    // last node  = last  key
    $CNST_closure_reset = array();
    for($k=$tmp_first_node; $k<=$tmp_last_node; $k++){
        $CNST_closure_reset[$k] = $CNST_not_closed;
    }
    // define the state where there is no closure for all nodes
    for($k=$tmp_first_node; $k<=$tmp_last_node; $k++){
        $H[$k] = $CNST_closure_reset;   // Key in the closure arrays represent nodes
    }
    unset($tmp_first_node);
    unset($tmp_last_node);


    # Start algorithm
    foreach($G as $init_node => $children){#[Jump to initial node set]
        #[Initial Node Set]
        $P = array();                   // declare at starup, remove the old $init_node from path on loop
        $P[$init_node]=true;            // the first key in P is always the new initial node
        $k=$init_node;                  // update the current node
                                        // On loop H[old_init_node] is not cleared cause is never checked again
        do{#Path 1,3,7,4 jump here to extend father 7
            do{#Path from 1,3,8,5 became 2,4,8,5,6 jump here to extend child 6
                $new_expansion = false;
                foreach( $G[$k] as $child => $foo ){#Consider each child of 7 or 6
                    if( $child>$init_node and isset($P[$child])===false and $H[$k][$child]===$CNST_not_closed ){
                        $P[$child]=true;    // add this child to the path
                        $k = $child;        // update the current node
                        $new_expansion=true;// set the flag for expanding the child of k
                        break(1);           // we are done, one child at a time
            }   }   }while(($new_expansion===true));// Do while a new child has been added to the path

            # If the first node is child of the last we have a circuit
            if( isset($G[$k][$init_node])===true ){
                $C[] = $P;  // Leaving this out of closure will catch loops to
            }

            # Closure
            if($k>$init_node){                  //if k>init_node then alwaya count(P)>1, so proceed to closure
                $new_expansion=true;            // $new_expansion is never true, set true to expand father of k
                unset($P[$k]);                  // remove k from path
                end($P); $k_father = key($P);   // get father of k
                $H[$k_father][$k]=$CNST_closed; // mark k as closed
                $H[$k] = $CNST_closure_reset;   // reset k closure
                $k = $k_father;                 // update k
        }   } while($new_expansion===true);//if we don't wnter the if block m has the old k$k_father_old = $k;
        // Advance Initial Vertex Context
    }//foreach initial


}//function

?>

Я проаналізував та задокументував ЕК, але, на жаль, документація є грецькою мовою.


1

Є два етапи (алгоритми), які беруть участь у пошуку всіх циклів у DAG.

Перший крок - використовувати алгоритм Таряна для пошуку набору сильно пов'язаних компонентів.

  1. Почніть з будь-якої довільної вершини.
  2. DFS з цієї вершини. Для кожного вузла x зберігайте два числа, dfs_index [x] і dfs_lowval [x]. dfs_index [x] зберігає, коли цей вузол відвідується, тоді як dfs_lowval [x] = min (dfs_low [k]), де k - всі діти x, які не є безпосередньо батьком x у дереві, що охоплює dfs.
  3. Усі вузли з тим самим dfs_lowval [x] знаходяться в одному сильно з'єднаному компоненті.

Другий крок - пошук циклів (доріжок) у підключених компонентах. Моя пропозиція - використовувати модифіковану версію алгоритму Hierholzer.

Ідея така:

  1. Виберіть будь-яку початкову вершину v і дотримуйтесь сліду ребер від цієї вершини, поки не повернетесь до v. Не можна застрягати в будь-якій вершині, окрім v, тому що рівний ступінь усіх вершин гарантує, що, коли слід переходить у іншу вершину вершина w має бути невикористаний край, залишаючи w. Сформований таким чином тур є закритим туром, але може не охоплювати всіх вершин і країв початкового графа.
  2. Поки існує вершина v, яка належить до поточного туру, але має сусідні краї, які не є частиною туру, почніть інший слід від v, дотримуючись невикористаних країв, поки не повернетесь до v, і приєднайтеся до сформованого таким чином туру до попередній тур.

Ось посилання на реалізацію Java з тестовим випадком:

http://stones333.blogspot.com/2013/12/find-cycles-in-directed-graph-dag.html


16
Як може існувати цикл у DAG (спрямований ациклічний графік)?
sky_coder123

Це не знаходить усіх циклів.
Vishwa Ratna


0

Я наткнувся на наступний алгоритм, який здається більш ефективним, ніж алгоритм Джонсона (принаймні, для великих графіків). Однак я не впевнений у його ефективності порівняно з алгоритмом Тарджана.
Крім того, я перевірив це лише на трикутники поки що. Якщо ви зацікавлені, будь ласка, перегляньте "Алгоритми лістингу та підграфа" Норішіге Чиби та Такао Нішізекі ( http://dx.doi.org/10.1137/0214017 )


0

Рішення Javascript, використовуючи роз'єднані набори пов'язаних списків. Можна модернізувати, щоб розрізнити встановлені ліси для швидшого пробігу.

var input = '5\nYYNNN\nYYYNN\nNYYNN\nNNNYN\nNNNNY'
console.log(input);
//above solution should be 3 because the components are
//{0,1,2}, because {0,1} and {1,2} therefore {0,1,2}
//{3}
//{4}

//MIT license, authored by Ling Qing Meng

//'4\nYYNN\nYYYN\nNYYN\nNNNY'

//Read Input, preformatting
var reformat = input.split(/\n/);
var N = reformat[0];
var adjMatrix = [];
for (var i = 1; i < reformat.length; i++) {
    adjMatrix.push(reformat[i]);
}

//for (each person x from 1 to N) CREATE-SET(x)
var sets = [];
for (var i = 0; i < N; i++) {
    var s = new LinkedList();
    s.add(i);
    sets.push(s);
}

//populate friend potentials using combinatorics, then filters
var people =  [];
var friends = [];
for (var i = 0; i < N; i++) {
    people.push(i);
}
var potentialFriends = k_combinations(people,2);
for (var i = 0; i < potentialFriends.length; i++){
    if (isFriend(adjMatrix,potentialFriends[i]) === 'Y'){
        friends.push(potentialFriends[i]);
    }
}


//for (each pair of friends (x y) ) if (FIND-SET(x) != FIND-SET(y)) MERGE-SETS(x, y)
for (var i = 0; i < friends.length; i++) {
    var x = friends[i][0];
    var y = friends[i][1];
    if (FindSet(x) != FindSet(y)) {
        sets.push(MergeSet(x,y));
    }
}


for (var i = 0; i < sets.length; i++) {
    //sets[i].traverse();
}
console.log('How many distinct connected components?',sets.length);



//Linked List data structures neccesary for above to work
function Node(){
    this.data = null;
    this.next = null;
}

function LinkedList(){
    this.head = null;
    this.tail = null;
    this.size = 0;

    // Add node to the end
    this.add = function(data){
        var node = new Node();
        node.data = data;
        if (this.head == null){
            this.head = node;
            this.tail = node;
        } else {
            this.tail.next = node;
            this.tail = node;
        }
        this.size++;
    };


    this.contains = function(data) {
        if (this.head.data === data) 
            return this;
        var next = this.head.next;
        while (next !== null) {
            if (next.data === data) {
                return this;
            }
            next = next.next;
        }
        return null;
    };

    this.traverse = function() {
        var current = this.head;
        var toPrint = '';
        while (current !== null) {
            //callback.call(this, current); put callback as an argument to top function
            toPrint += current.data.toString() + ' ';
            current = current.next; 
        }
        console.log('list data: ',toPrint);
    }

    this.merge = function(list) {
        var current = this.head;
        var next = current.next;
        while (next !== null) {
            current = next;
            next = next.next;
        }
        current.next = list.head;
        this.size += list.size;
        return this;
    };

    this.reverse = function() {
      if (this.head == null) 
        return;
      if (this.head.next == null) 
        return;

      var currentNode = this.head;
      var nextNode = this.head.next;
      var prevNode = this.head;
      this.head.next = null;
      while (nextNode != null) {
        currentNode = nextNode;
        nextNode = currentNode.next;
        currentNode.next = prevNode;
        prevNode = currentNode;
      }
      this.head = currentNode;
      return this;
    }


}


/**
 * GENERAL HELPER FUNCTIONS
 */

function FindSet(x) {
    for (var i = 0; i < sets.length; i++){
        if (sets[i].contains(x) != null) {
            return sets[i].contains(x);
        }
    }
    return null;
}

function MergeSet(x,y) {
    var listA,listB;
    for (var i = 0; i < sets.length; i++){
        if (sets[i].contains(x) != null) {
            listA = sets[i].contains(x);
            sets.splice(i,1);
        }
    }
    for (var i = 0; i < sets.length; i++) {
        if (sets[i].contains(y) != null) {
            listB = sets[i].contains(y);
            sets.splice(i,1);
        }
    }
    var res = MergeLists(listA,listB);
    return res;

}


function MergeLists(listA, listB) {
    var listC = new LinkedList();
    listA.merge(listB);
    listC = listA;
    return listC;
}

//access matrix by i,j -> returns 'Y' or 'N'
function isFriend(matrix, pair){
    return matrix[pair[0]].charAt(pair[1]);
}

function k_combinations(set, k) {
    var i, j, combs, head, tailcombs;
    if (k > set.length || k <= 0) {
        return [];
    }
    if (k == set.length) {
        return [set];
    }
    if (k == 1) {
        combs = [];
        for (i = 0; i < set.length; i++) {
            combs.push([set[i]]);
        }
        return combs;
    }
    // Assert {1 < k < set.length}
    combs = [];
    for (i = 0; i < set.length - k + 1; i++) {
        head = set.slice(i, i+1);
        tailcombs = k_combinations(set.slice(i + 1), k - 1);
        for (j = 0; j < tailcombs.length; j++) {
            combs.push(head.concat(tailcombs[j]));
        }
    }
    return combs;
}

0

DFS від початкового вузла s, слідкуйте за маршрутом DFS під час проходження та записуйте шлях, якщо ви знайдете край від вузла v на шляху до s. (v, s) є зворотним краєм у дереві DFS і, таким чином, позначає цикл, що містить s.


Добре, але це не те, що шукає ОП: знайти весь цикл, швидше за все, мінімальний.
Шон L

0

Що стосується вашого питання про цикл перестановки , читайте більше тут: https://www.codechef.com/problems/PCYCLE

Ви можете спробувати цей код (введіть розмір і номер цифр):

# include<cstdio>
using namespace std;

int main()
{
    int n;
    scanf("%d",&n);

    int num[1000];
    int visited[1000]={0};
    int vindex[2000];
    for(int i=1;i<=n;i++)
        scanf("%d",&num[i]);

    int t_visited=0;
    int cycles=0;
    int start=0, index;

    while(t_visited < n)
    {
        for(int i=1;i<=n;i++)
        {
            if(visited[i]==0)
            {
                vindex[start]=i;
                visited[i]=1;
                t_visited++;
                index=start;
                break;
            }
        }
        while(true)
        {
            index++;
            vindex[index]=num[vindex[index-1]];

            if(vindex[index]==vindex[start])
                break;
            visited[vindex[index]]=1;
            t_visited++;
        }
        vindex[++index]=0;
        start=index+1;
        cycles++;
    }

    printf("%d\n",cycles,vindex[0]);

    for(int i=0;i<(n+2*cycles);i++)
    {
        if(vindex[i]==0)
            printf("\n");
        else
            printf("%d ",vindex[i]);
    }
}

0

Версія DFS c ++ для псевдо-коду у відповіді другого поверху:

void findCircleUnit(int start, int v, bool* visited, vector<int>& path) {
    if(visited[v]) {
        if(v == start) {
            for(auto c : path)
                cout << c << " ";
            cout << endl;
            return;
        }
        else 
            return;
    }
    visited[v] = true;
    path.push_back(v);
    for(auto i : G[v])
        findCircleUnit(start, i, visited, path);
    visited[v] = false;
    path.pop_back();
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.