HTML Diff
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>