На самом деле, поиск в глубину (или в ширину) не вполне достаточен. Вам нужен чуть более сложный алгоритм.
Например, предположим, что существует граф с узлами {a, b, c, d} и ребрами {(a, b), (b, c), (b, d), (d, c)}, где ребро (x, y) является ребром от x до y.
(выглядит примерно так, все края направлены вниз.)
(a)
|
|
(b)
/ \
(d) |
| |
\ /
(c)
Затем, выполняя поиск в глубине, можно сначала посетить узел (a), затем (b), затем (c), затем вернуться к (b), затем посетить (d) и, наконец, снова посетить (c) и заключить, что есть цикл - когда нет. Подобная вещь случается с шириной сначала.
То, что вам нужно сделать, это отслеживать, какие узлы вы находитесь в середине посещения. В приведенном выше примере, когда алгоритм достигает (d), он завершил посещение (c), но не (a) или (b). Так что вернуться к законченному узлу - это нормально, но посещение незаконченного узла означает, что у вас есть цикл. Обычный способ сделать это - покрасить каждый узел белым (еще не посещенным), серым (посещая потомков) или черным (завершив посещение).
вот какой-то псевдокод!
define visit(node n):
if n.colour == grey: //if we're still visiting this node or its descendants
throw exception("Cycle found")
n.colour = grey //to indicate this node is being visited
for node child in n.children():
if child.colour == white: //if the child is unexplored
visit(child)
n.colour = black //to show we're done visiting this node
return
тогда при выполнении посещения (root_node) возникнет исключение, если и только если есть цикл (изначально все узлы должны быть белыми).