0 added
0 removed
Original
2026-01-01
Modified
2026-02-26
1
<p>В этом и других курсах вы могли сталкиваться с различными учебными проектами. Обычно они достаточно реалистичные, но одно различие все таки есть - все учебные проекты небольшие.</p>
1
<p>В этом и других курсах вы могли сталкиваться с различными учебными проектами. Обычно они достаточно реалистичные, но одно различие все таки есть - все учебные проекты небольшие.</p>
2
<p>Реальные проекты намного больше учебных. Чтобы разработчики не путались в коде, обычно большие проекты состоят из отдельных пакетов. При этом появляются<strong>зависимости</strong>.</p>
2
<p>Реальные проекты намного больше учебных. Чтобы разработчики не путались в коде, обычно большие проекты состоят из отдельных пакетов. При этом появляются<strong>зависимости</strong>.</p>
3
<p>Например, если пакет А импортирует или использует пакет Б, мы говорим: "А зависит от Б". На схеме эту зависимость можно обозначить так:</p>
3
<p>Например, если пакет А импортирует или использует пакет Б, мы говорим: "А зависит от Б". На схеме эту зависимость можно обозначить так:</p>
4
<p>Чтобы собрать проект, нам нужно загружать или компилировать пакеты в правильном порядке. Если А зависит от Б, то сначала надо загрузить пакет Б, и только потом перейти к пакету А.</p>
4
<p>Чтобы собрать проект, нам нужно загружать или компилировать пакеты в правильном порядке. Если А зависит от Б, то сначала надо загрузить пакет Б, и только потом перейти к пакету А.</p>
5
<p>Когда пакетов в проекте много, количество зависимостей тоже растет. Вместе они образуют сложный граф зависимостей:</p>
5
<p>Когда пакетов в проекте много, количество зависимостей тоже растет. Вместе они образуют сложный граф зависимостей:</p>
6
<p>Пакеты из этого графа можно загружать или компилировать в правильном порядке. Судя по графу, возможно три варианта:</p>
6
<p>Пакеты из этого графа можно загружать или компилировать в правильном порядке. Судя по графу, возможно три варианта:</p>
7
<ul><li>Д, Б, В, Г, А</li>
7
<ul><li>Д, Б, В, Г, А</li>
8
<li>Д, Г, В, Б, А</li>
8
<li>Д, Г, В, Б, А</li>
9
<li>Д, В, Г, Б, А</li>
9
<li>Д, В, Г, Б, А</li>
10
</ul><p>У нас есть выбор, потому что ветки Г и Б + В не зависят друг от друга. В любом случае первым пакетом следует Д, потому что он ни от кого не зависит. Последним идет пакет А, потому что от него никто не зависит.</p>
10
</ul><p>У нас есть выбор, потому что ветки Г и Б + В не зависят друг от друга. В любом случае первым пакетом следует Д, потому что он ни от кого не зависит. Последним идет пакет А, потому что от него никто не зависит.</p>
11
<p>Подобная структура зависимостей часто встречается в программировании. Например, таким образом мы описываем связи между классами, которые задействованы в коде. Это называется<strong>внедрение зависимостей</strong>(<em>Dependency Injection</em>).</p>
11
<p>Подобная структура зависимостей часто встречается в программировании. Например, таким образом мы описываем связи между классами, которые задействованы в коде. Это называется<strong>внедрение зависимостей</strong>(<em>Dependency Injection</em>).</p>
12
<p>Но есть проблема - часто программисты не описывают граф целиком, а только указывают, от каких узлов зависит очередной узел.</p>
12
<p>Но есть проблема - часто программисты не описывают граф целиком, а только указывают, от каких узлов зависит очередной узел.</p>
13
<p>Это приводит к тому, что в графе может появиться циклическая зависимость:</p>
13
<p>Это приводит к тому, что в графе может появиться циклическая зависимость:</p>
14
<p>В циклическом графе невозможно определить правильный порядок компиляции пакетов или создания объектов. Справиться с этой сложностью помогают<strong>системы сборки</strong>и<strong>библиотеки внедрения зависимостей</strong>. Именно благодаря им программисты находят циклы в описанных графах.</p>
14
<p>В циклическом графе невозможно определить правильный порядок компиляции пакетов или создания объектов. Справиться с этой сложностью помогают<strong>системы сборки</strong>и<strong>библиотеки внедрения зависимостей</strong>. Именно благодаря им программисты находят циклы в описанных графах.</p>
15
<p>В этом уроке мы научимся искать циклы в ориентированных графах. Задача для неориентированных графов на практике встречается гораздо реже, поэтому мы не станем обсуждать ее подробно. Она решается похожим образом, так что алгоритм можно без труда адаптировать.</p>
15
<p>В этом уроке мы научимся искать циклы в ориентированных графах. Задача для неориентированных графов на практике встречается гораздо реже, поэтому мы не станем обсуждать ее подробно. Она решается похожим образом, так что алгоритм можно без труда адаптировать.</p>
16
<h2>Матрица смежности</h2>
16
<h2>Матрица смежности</h2>
17
<p>В прошлых уроках мы познакомились с двумя способами представления графов:</p>
17
<p>В прошлых уроках мы познакомились с двумя способами представления графов:</p>
18
<ul><li>Со списками смежности</li>
18
<ul><li>Со списками смежности</li>
19
<li>С неявным представлением</li>
19
<li>С неявным представлением</li>
20
</ul><p>Сегодня изучим<strong>матрицу смежности</strong>- еще одну структуру данных, которую часто применяют при работе с графами.</p>
20
</ul><p>Сегодня изучим<strong>матрицу смежности</strong>- еще одну структуру данных, которую часто применяют при работе с графами.</p>
21
<p>Все вершины графа мы храним в массиве, так что у каждой вершины появляется уникальный порядковый номер. В большинстве современных языков программирования нумерация элементов начинается с ноля. Поэтому у нас будут вершина 0, вершина 1, вершина 2 и так далее:</p>
21
<p>Все вершины графа мы храним в массиве, так что у каждой вершины появляется уникальный порядковый номер. В большинстве современных языков программирования нумерация элементов начинается с ноля. Поэтому у нас будут вершина 0, вершина 1, вершина 2 и так далее:</p>
22
<p>Важно, что номера вершин идут подряд без пропусков.</p>
22
<p>Важно, что номера вершин идут подряд без пропусков.</p>
23
<p>Вместе с массивом мы храним матрицу - двумерный массив. Ее ширина и высота равны количеству вершин. Есть речь идет об обычном невзвешенном графе, элементами массива будут:</p>
23
<p>Вместе с массивом мы храним матрицу - двумерный массив. Ее ширина и высота равны количеству вершин. Есть речь идет об обычном невзвешенном графе, элементами массива будут:</p>
24
<ul><li>Либо булевы значения - false и true</li>
24
<ul><li>Либо булевы значения - false и true</li>
25
<li>Либо числа 0 и 1</li>
25
<li>Либо числа 0 и 1</li>
26
</ul><p>Вернемся к рисунку выше. На нем мы видим, какие вершины связаны между собой. Эту информацию мы можем записать в матрицу смежности. Помните, что нумерация вершин начинается с 0, поэтому нумерация столбцов и строк сдвигается на единицу.</p>
26
</ul><p>Вернемся к рисунку выше. На нем мы видим, какие вершины связаны между собой. Эту информацию мы можем записать в матрицу смежности. Помните, что нумерация вершин начинается с 0, поэтому нумерация столбцов и строк сдвигается на единицу.</p>
27
<ul><li><p>На графе вершина 6 связана с вершиной 2 - записываем true в строку 6 и столбец 2:</p>
27
<ul><li><p>На графе вершина 6 связана с вершиной 2 - записываем true в строку 6 и столбец 2:</p>
28
</li>
28
</li>
29
<li><p>На графе вершина 2 не связана с вершиной 4 - записываем false в строку 2 и столбец 4:</p>
29
<li><p>На графе вершина 2 не связана с вершиной 4 - записываем false в строку 2 и столбец 4:</p>
30
</li>
30
</li>
31
</ul><p>Перебирая элементы строки, мы можем найти все связанные вершины.</p>
31
</ul><p>Перебирая элементы строки, мы можем найти все связанные вершины.</p>
32
<p>Во взвешенном графе вместо булевых значений мы можем хранить числа:</p>
32
<p>Во взвешенном графе вместо булевых значений мы можем хранить числа:</p>
33
<ul><li>Если вершины не связаны - null</li>
33
<ul><li>Если вершины не связаны - null</li>
34
<li>Если вершины связаны - число, обозначающее вес ребра</li>
34
<li>Если вершины связаны - число, обозначающее вес ребра</li>
35
</ul><p>Матрица смежности подходит для хранения ориентированных графов. Для хранения неориентированных графов программисты используют небольшой трюк, делая матрицу симметричной. Для примера представим, что мы хотим связать вершины n и m. В этом случае мы записываем true два раза:</p>
35
</ul><p>Матрица смежности подходит для хранения ориентированных графов. Для хранения неориентированных графов программисты используют небольшой трюк, делая матрицу симметричной. Для примера представим, что мы хотим связать вершины n и m. В этом случае мы записываем true два раза:</p>
36
<ul><li>В столбец n и строку m</li>
36
<ul><li>В столбец n и строку m</li>
37
<li>В столбец m и строку n</li>
37
<li>В столбец m и строку n</li>
38
</ul><p>Так мы получаем двустороннюю связь.</p>
38
</ul><p>Так мы получаем двустороннюю связь.</p>
39
<h2>Поиск цикла</h2>
39
<h2>Поиск цикла</h2>
40
<p>Обычно нам надо убедиться, что в графе нет циклов. Поэтому мы пытаемся найти хотя бы один любой цикл. Если нам это удается, то мы подсказываем пользователю, из каких вершин он состоит.</p>
40
<p>Обычно нам надо убедиться, что в графе нет циклов. Поэтому мы пытаемся найти хотя бы один любой цикл. Если нам это удается, то мы подсказываем пользователю, из каких вершин он состоит.</p>
41
<p>Оказывается, для поиска циклов не надо изобретать нового алгоритма - можно адаптировать обход графа в глубину. При обходе мы будем помечать посещенные вершины. В реальной программе каждой вершине будет приписан числовой код, но при объяснении алгоритма принято говорить о "перекрашивании" вершин.</p>
41
<p>Оказывается, для поиска циклов не надо изобретать нового алгоритма - можно адаптировать обход графа в глубину. При обходе мы будем помечать посещенные вершины. В реальной программе каждой вершине будет приписан числовой код, но при объяснении алгоритма принято говорить о "перекрашивании" вершин.</p>
42
<p>Попробуем найти цикл в таком небольшом графе:</p>
42
<p>Попробуем найти цикл в таком небольшом графе:</p>
43
<p>В начале все вершины в графе белые. Такому графу соответствует матрица смежности, изображенная ниже:</p>
43
<p>В начале все вершины в графе белые. Такому графу соответствует матрица смежности, изображенная ниже:</p>
44
<p>Начинать можно с любой вершины. Мы начнем с вершины 0, которой соответствует верхняя строка в матрице.</p>
44
<p>Начинать можно с любой вершины. Мы начнем с вершины 0, которой соответствует верхняя строка в матрице.</p>
45
<p>Перекрашиваем вершину в серый цвет. Это означает, что мы начали с ней работать:</p>
45
<p>Перекрашиваем вершину в серый цвет. Это означает, что мы начали с ней работать:</p>
46
<p>Ищем в первой строке связанные вершины. Они помечены значением true.</p>
46
<p>Ищем в первой строке связанные вершины. Они помечены значением true.</p>
47
<p>Видим, что вершина 0 связана со вторым элементом строки - вершиной 1. Начинаем работать с ней и также перекрашиваем ее в серый цвет:</p>
47
<p>Видим, что вершина 0 связана со вторым элементом строки - вершиной 1. Начинаем работать с ней и также перекрашиваем ее в серый цвет:</p>
48
<p>Чтобы обнаружить связанные вершины, рассмотрим вторую строку сверху. Она соответствует вершине 1. Похожим образом продолжаем перекрашивать вершины. Следующие на очереди - 2 и 3:</p>
48
<p>Чтобы обнаружить связанные вершины, рассмотрим вторую строку сверху. Она соответствует вершине 1. Похожим образом продолжаем перекрашивать вершины. Следующие на очереди - 2 и 3:</p>
49
<p>Из вершины 3 двигаться некуда. Мы завершаем ее обработку, перекрашиваем в черный цвет и возвращаемся в вершину 2.</p>
49
<p>Из вершины 3 двигаться некуда. Мы завершаем ее обработку, перекрашиваем в черный цвет и возвращаемся в вершину 2.</p>
50
<p>У второй вершины есть еще одна необработанная вершина - 4. Перекрашиваем ее в серый цвет:</p>
50
<p>У второй вершины есть еще одна необработанная вершина - 4. Перекрашиваем ее в серый цвет:</p>
51
<p>У вершины 4 также нет связанный вершин. Завершаем ее обработку, перекрашиваем в черный цвет, снова возвращаемся в вершину 2.</p>
51
<p>У вершины 4 также нет связанный вершин. Завершаем ее обработку, перекрашиваем в черный цвет, снова возвращаемся в вершину 2.</p>
52
<p>Теперь и у вершины 2 обработаны все соседи - значит, ее обработка также завершена. Перекрашиваем ее в черный цвет и возвращаемся в вершину 1:</p>
52
<p>Теперь и у вершины 2 обработаны все соседи - значит, ее обработка также завершена. Перекрашиваем ее в черный цвет и возвращаемся в вершину 1:</p>
53
<p>У вершины 1 есть следующая необработанная вершина - это 5.</p>
53
<p>У вершины 1 есть следующая необработанная вершина - это 5.</p>
54
<p>В свою очередь у вершины 5 есть необработанная вершина 6:</p>
54
<p>В свою очередь у вершины 5 есть необработанная вершина 6:</p>
55
<p>Вершина 6 связана с вершиной 0, которая окрашена в серый цвет. Это значит, что мы обнаружили цикл.</p>
55
<p>Вершина 6 связана с вершиной 0, которая окрашена в серый цвет. Это значит, что мы обнаружили цикл.</p>
56
<p>Далее нужно разобраться, какие вершины входят в цикл. Для этого мы возвращаемся назад, пока не встретим вершину 0. Так мы выясним, что в нашем примере в цикл попадают вершины 6, 5, 1 и 0.</p>
56
<p>Далее нужно разобраться, какие вершины входят в цикл. Для этого мы возвращаемся назад, пока не встретим вершину 0. Так мы выясним, что в нашем примере в цикл попадают вершины 6, 5, 1 и 0.</p>
57
<p>Если мы встречаем черную вершину, это означает, что цикл в нашем графе есть, но он не опасен. Двигаясь по стрелкам, мы не попадаем в замкнутый круг:</p>
57
<p>Если мы встречаем черную вершину, это означает, что цикл в нашем графе есть, но он не опасен. Двигаясь по стрелкам, мы не попадаем в замкнутый круг:</p>
58
<p>Двигаясь по стрелкам, ромб слева можно обойти без зацикливания, а ромб справа - нет. Другими словами, встречая черную вершину, мы должны ее пропускать. Черная вершина означает, что мы встретили уже обработанную область графа без циклов.</p>
58
<p>Двигаясь по стрелкам, ромб слева можно обойти без зацикливания, а ромб справа - нет. Другими словами, встречая черную вершину, мы должны ее пропускать. Черная вершина означает, что мы встретили уже обработанную область графа без циклов.</p>
59
<h2>Реализация</h2>
59
<h2>Реализация</h2>
60
<p>Теперь посмотрим, как такой алгоритм реализуется в коде:</p>
60
<p>Теперь посмотрим, как такой алгоритм реализуется в коде:</p>
61
<p>Как и в прошлых уроках, для работы с графом сделаем класс Graph. В конструкторе мы будем получать массив значений в вершинах графа. Размер массива сохраним в поле size для последующего использования. Также сразу создадим квадратную матрицу смежности, заполним ее значением false:</p>
61
<p>Как и в прошлых уроках, для работы с графом сделаем класс Graph. В конструкторе мы будем получать массив значений в вершинах графа. Размер массива сохраним в поле size для последующего использования. Также сразу создадим квадратную матрицу смежности, заполним ее значением false:</p>
62
<p>Далее мы создаем связь. По сути, это нахождение элемента в матрице и присвоение ему значения true:</p>
62
<p>Далее мы создаем связь. По сути, это нахождение элемента в матрице и присвоение ему значения true:</p>
63
<p>Для поиска циклов нам потребуются константы, чтобы обозначать белый, серый и черный цвет вершин, а также массив цветов всех вершин. В начале работы алгоритма все вершины белые:</p>
63
<p>Для поиска циклов нам потребуются константы, чтобы обозначать белый, серый и черный цвет вершин, а также массив цветов всех вершин. В начале работы алгоритма все вершины белые:</p>
64
<p>Функция возвращает массив вершин, образующих либо цикл, либо значение null при отсутствии циклов.</p>
64
<p>Функция возвращает массив вершин, образующих либо цикл, либо значение null при отсутствии циклов.</p>
65
<p>Основная работа делается во внутренней рекурсивной функции visit(). В качестве параметра она получает порядковый номер вершины. Так как нумерация начинается с нуля, первой вершине соответствует номер 0.</p>
65
<p>Основная работа делается во внутренней рекурсивной функции visit(). В качестве параметра она получает порядковый номер вершины. Так как нумерация начинается с нуля, первой вершине соответствует номер 0.</p>
66
<p>В массиве colors функция отслеживает состояние каждой вершины:</p>
66
<p>В массиве colors функция отслеживает состояние каждой вершины:</p>
67
<p>Если вершина чёрная, это означает, что она уже полностью обработана - повторно её проверять не нужно.</p>
67
<p>Если вершина чёрная, это означает, что она уже полностью обработана - повторно её проверять не нужно.</p>
68
<p>Если вершина серая, значит, она уже в стеке вызовов - и это признак обнаружения цикла. В этом случае функция возвращает массив, содержащий текущую вершину, чтобы начать восстановление пути цикла.</p>
68
<p>Если вершина серая, значит, она уже в стеке вызовов - и это признак обнаружения цикла. В этом случае функция возвращает массив, содержащий текущую вершину, чтобы начать восстановление пути цикла.</p>
69
<p>В середине мы обрабатываем текущую вершину. Перекрашиваем ее в серый цвет, а после обработки всех связанных вершин - в черный:</p>
69
<p>В середине мы обрабатываем текущую вершину. Перекрашиваем ее в серый цвет, а после обработки всех связанных вершин - в черный:</p>
70
<p>Обработка связанных вершин заключается в поочередном рекурсивном вызове функции visit(). Если во время очередного вызова функция возвращает массив, это значит, что мы нашли цикл. Добавляем в массив текущую вершину и возвращаем новое значение.</p>
70
<p>Обработка связанных вершин заключается в поочередном рекурсивном вызове функции visit(). Если во время очередного вызова функция возвращает массив, это значит, что мы нашли цикл. Добавляем в массив текущую вершину и возвращаем новое значение.</p>
71
<p>Когда рекурсия развернется, в массиве окажутся все вершины из цикла. Именно этот массив и получит программист, вызвавший функцию.</p>
71
<p>Когда рекурсия развернется, в массиве окажутся все вершины из цикла. Именно этот массив и получит программист, вызвавший функцию.</p>
72
<p>Чтобы проверить наш код, создадим граф из примера выше и попытаемся найти в нем цикл:</p>
72
<p>Чтобы проверить наш код, создадим граф из примера выше и попытаемся найти в нем цикл:</p>
73
<h2>Выводы</h2>
73
<h2>Выводы</h2>
74
<p>В этом уроке мы обсудили поиск циклов и пришли к выводу, что этот алгоритм нам уже знаком. На самом деле, это адаптированный обход графа в глубину (<em>Depth-first search</em>).</p>
74
<p>В этом уроке мы обсудили поиск циклов и пришли к выводу, что этот алгоритм нам уже знаком. На самом деле, это адаптированный обход графа в глубину (<em>Depth-first search</em>).</p>
75
<p>Кроме того, в этом уроке вы узнали, что для хранения ориентированных графов можно использовать матрицу смежности. Так же можно хранить неориентированные графы, связывая каждую пару вершин в прямом и обратном направлении. Чтобы хранить взвешенные графы, в матрице смежности вместо булевых значений можно использовать числа.</p>
75
<p>Кроме того, в этом уроке вы узнали, что для хранения ориентированных графов можно использовать матрицу смежности. Так же можно хранить неориентированные графы, связывая каждую пару вершин в прямом и обратном направлении. Чтобы хранить взвешенные графы, в матрице смежности вместо булевых значений можно использовать числа.</p>