Алгоритмы обхода графов на Python и C#
Если вы во время учебы в институте пренебрегали теорией графов, то зря. Ибо граф – это замечательная абстракция, к которой можно свести много различных задач и решить их через алгоритмы на графах. К графам сводятся не только задачи транспортной логистики. В виде графа можно представить, например, компьютерную сеть, сеть социальных контактов, да и вообще различные взаимоотношения между людьми, а так же многие другие задачи, которые можно решить программированием на языках C# и Python. В частности, задачу паросочетаний можно представить в виде двудольного графа:

А теперь рассмотрим практический пример. Допустим, у нас есть вот такой вот граф:

Вершины графа могут быть закодированы как буквами, так и цифрами. Цифрами – удобнее, так как они могут быть индексами, по которыми мы можем обраться к определённой вершине. Давайте это проверим. В нашем примере мы назначим буквам соответствующую цифру и построим такой граф (пример на Python):
a, b, c, d, e, f, g, h = range(8)
N = [
{b, c, d, e, f}, # a
{c, e}, # b
{d}, # c
{e}, # d
{f}, # e
{c, g, h}, # f
{f, h}, # g
{f, g} # h
]
print(b in N[a])
print(len(N[f]))
В данном примере мы просто присвоили букве, как переменной, соответствующее числовое значение, а потом задали граф как массив вершин. Каждая вершина – это множество исходящих из него ребер. Каждое ребро идет в другую вершину, элементы множества – это вершины, в которые ведут эти ребра.
Как обратиться к конкретной вершине? По номеру, вот так, например, проверить, ведет ли ребро из одной вершины (a) к другой вершине (b)
print(b in N[a])
Степень вершины мы можем узнать, просто посмотрев размер множества исходящих из нее рёбер, вот так:
print(len(N[f]))
Теперь попробуем реализовать алгоритм обхода графа в ширину. Вот шаги этого алгоритма:
- Поместить узел, с которого начинается поиск, в изначально пустую очередь.
- Извлечь из начала очереди узел u {\displaystyle u} u и пометить его как развёрнутый.
- Если узел u {\displaystyle u} u является целевым узлом, то завершить поиск с результатом «успех».
- В противном случае, в конец очереди добавляются все преемники узла u {\displaystyle u}u, которые ещё не развёрнуты и не находятся в очереди.
- Если очередь пуста, то все узлы связного графа были просмотрены, следовательно, целевой узел недостижим из начального; завершить поиск с результатом «неудача».
- Вернуться к п. 2.
Вот пример реализации данного алгоритма на Python
a, b, c, d, e, f, g, h = range(8)
N = [
{b, c, d, e, f}, # a
{c, e}, # b
{d}, # c
{e}, # d
{f}, # e
{c, g, h}, # f
{f, h}, # g
{f, g} # h
]
#импортируем класс очереди
from queue import Queue
#соответсвие цифры букве
letters="abcdefgh"
visited_nodes={}
#Начальные переменные
queue=Queue()
beg_node=a
target_node=h
#шаг 1. Поместить узел, с которого начинается поиск, в изначально пустую очередь.
queue.put(beg_node)
visited_nodes={beg_node}
# это у нас цикл с постусловием
while True:
#шаг 2. Извлечь из начала очереди узел "u" и пометить его как развёрнутый.
u=queue.get()
print("Обходим узел",letters[u])
#Если узел "u" является целевым узлом, то завершить поиск с результатом «успех».
if u==target_node:
print("Поиск успешно завершен")
break
else:
#В противном случае, в конец очереди добавляются все преемники узла "u", которые ещё не развёрнуты и не находятся в очереди.
for node in N[u]:
print(" -- Смотрим ребро", letters[node])
if not(node in visited_nodes):
print(" ---- Узел", letters[node],"добавили в очередь")
visited_nodes.add(node)
queue.put(node)
#шаг 3. Если очередь пуста, то все узлы связного графа были просмотрены, следовательно, целевой узел
# недостижим из начального; завершить поиск с результатом «неудача».
if queue.empty():
print("Вершина ",letters[u]," не найдена")
#шаг 4. Вернуться к п. 2.
Алгоритм обхода графа в ширину (не обязательно это поиск) мы можем использовать, когда надо раскрыть все вершины графа последовательно, уровень за уровнем. То есть, сначала мы раскрываем вершины первого уровня, затем второго и так далее. Вот как это выгляди на практике:

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

А теперь для разнообразия попробуем реализовать этот алгоритм на языке C#. Для начала объявим класс узла (вершины):
/// <summary>
/// Вершина графа
/// </summary>
public class Node
{
/// <summary>
/// Номер вершины
/// </summary>
public int vertex_number;
/// <summary>
/// Список вершин, в которые ведут ребра из этой вершины
/// </summary>
public List<Node> vertex_edges;
/// <summary>
/// Дополнительная информация
/// </summary>
public object tag;
/// <summary>
/// Конструктор
/// </summary>
/// <param name="a_vertex_number">Номер вершины</param>
public Node(int a_vertex_number)
{
vertex_edges = new List<Node>();
vertex_number = a_vertex_number;
}
}
Для удобства программирования создадим еще и класс Graph, который инкапсулирует в себя описание графа.
/// <summary>
/// Граф
/// </summary>
public class Graph
{
/// <summary>
/// Узлы графа
/// </summary>
public List<Node> nodes;
/// <summary>
/// Конструктор
/// </summary>
public Graph()
{
nodes = new List<Node>();
}
/// <summary>
/// Инициализация вершин
/// </summary>
/// <param name="count">Кол-во вершин</param>
public void init(int count)
{
for (int i = 1; i <= count; i++) nodes.Add(new Node(i));
}
/// <summary>
/// Добавить ребро
/// </summary>
/// <param name="a_vertex_number_form">Из какой вершины исходит</param>
/// <param name="a_vertex_number_to">В какую вершину ведет</param>
public void add(int a_vertex_number_from, int a_vertex_number_to)
{
nodes[a_vertex_number_from - 1].vertex_edges.Add(nodes[a_vertex_number_to - 1]);
}
/// <summary>
/// Сбросить метки узлов
/// </summary>
public void reset_tags()
{
foreach(Node node in nodes) node.tag = null;
}
}
В качестве примера вот такой граф:

Закодируем его:
graph = new Graph();
graph.init(8);
graph.add(1, 2);
graph.add(2, 3);
graph.add(2, 4);
graph.add(2, 5);
graph.add(3, 1);
graph.add(4, 1);
graph.add(4, 6);
graph.add(4, 8);
graph.add(5, 6);
graph.add(6, 7);
graph.add(7, 5);
graph.add(8, 6);
Теперь реализуем алгоритм обхода в графа в глубину, который формирует дерево обхода, по сути, удаляет из графа все замкнутые циклы:
/// Построить дерево обхода в глубину
/// </summary>
/// <param name="a_vertex_number">Начальный номер вершины</param>
/// <param name="start">Это стартовая точка</param>
/// <returns>Результатирующее дерево</returns>
public List<Node> dfs_tree(int a_vertex_number, bool start)
{
List<Node> res = new List<Node>();
Node node = nodes[a_vertex_number - 1];
if (node.tag != null) return new List<Node>();
node.tag = true;
List<Node> additig_list = null;
if (start)
{
Node root_item = new Node(a_vertex_number);
res.Add(root_item);
additig_list = root_item.vertex_edges;
}
else
{
additig_list = res;
}
foreach (Node item in node.vertex_edges)
{
if(item.tag==null)
{
Node adding_item = new Node(item.vertex_number);
adding_item.vertex_edges = dfs_tree(item.vertex_number,false);
additig_list.Add(adding_item);
}
}
return res;
}
Выведем полученное дерево в TreeVeiw:
private void btnDfs_Click(object sender, EventArgs e)
{
graph.reset_tags();
List<Node> tree = graph.dfs_tree(1,true);
tvDfs.Nodes.Clear();
draw_tree(tree, tvDfs.Nodes);
}
/// <summary>
/// Вывести дерево
/// </summary>
/// <param name="tree">Дерево</param>
/// <param name="tree_nodes">Узлы визуального дерева</param>
private void draw_tree(List<Node> tree, TreeNodeCollection tree_nodes)
{
foreach(Node node in tree)
{
TreeNode tree_node = new TreeNode(node.vertex_number.ToString());
tree_nodes.Add(tree_node);
if (node.vertex_edges.Count > 0) draw_tree(node.vertex_edges, tree_node.Nodes);
}
}
И вот что в итоге у нас получается:

Теперь поговорим о том, в каком случае какой алгоритм использовать. Если мы предполагаем, что искомая вершина может находиться достаточно далеко от той, с которой мы начинаем поиск (предполагается, что будет много уровней), то лучше использовать алгоритм поиска в глубину. Если мы знаем, что вершина будет где-то близко, на верхних уровнях дерева раскрытия вершин, то лучше использовать алгоритм поиска в ширину. Но, ни в коем случае не используйте алгоритм поиска в ширину, если у вас потенциально может быть очень много уровней – алгоритм может «повеситься».

Comments
So empty here ... leave a comment!