Как работают алгоритмы
Chapter 5 Graphs

Глава 5: Графы в алгоритмах

Графы - это фундаментальная структура данных, которая моделирует связи и отношения между объектами. Они имеют широкий спектр применений в информатике и за ее пределами, от моделирования социальных сетей и ссылок на веб-страницах до решения задач в области транспорта, планирования и распределения ресурсов. В этой главе мы исследуем основные свойства и алгоритмы для работы с графами, сосредоточившись на неориентированных графах, поиске в глубину и ширину, минимальных остовных деревьях и кратчайших путях.

Неориентированные графы

Неориентированный граф - это множество вершин (или узлов), соединенных ребрами. Формально, мы определяем неориентированный граф G как пару (V, E), где V - это множество вершин, а E - это множество неупорядоченных пар вершин, называемых ребрами. Ребро (v, w) соединяет вершины v и w. Мы говорим, что v и w являются смежными или соседями. Степень вершины - это количество ребер, инцидентных ей.

Вот простой пример неориентированного графа:

   A --- B
  /     / \
 /     /   \
C --- D --- E

В этом графе множество вершин V = {A, B, C, D, E}, а множество ребер E = {(A, B), (A, C), (B, D), (B, E), (C, D), (D, E)}.

Существует несколько способов представить граф в программе. Два распространенных представления:

  1. Матрица смежности: Булева матрица A размера n x n, где n - число вершин. Элемент A[i][j] равен true, если есть ребро от вершины i к вершине j, и false в противном случае.

  2. Списки смежности: Массив Adj размера n, где n - число вершин. Элемент Adj[v] - это список, содержащий соседей вершины v.

Выбор представления зависит от плотности графа (отношения числа ребер к числу вершин) и операций, которые нужно выполнять. Матрицы смежности просты, но могут быть неэффективными для разреженных графов. Списки смежности более экономичны по памяти для разреженных графов и обеспечивают более быстрый доступ к соседям вершины.

Вот пример того, как мы можем представить вышеприведенный граф с помощью списков смежности на Java:

List<Integer>[] graph = (List<Integer>[]) new List[5];
graph[0] = Arrays.asList(1, 2);        // A -> B, C
graph[1] = Arrays.asList(0, 3, 4);     // B -> A, D, E
graph[2] = Arrays.asList(0, 3);        // C -> A, D
graph[3] = Arrays.asList(1, 2, 4);     // D -> B, C, E
graph[4] = Arrays.asList(1, 3);        // E -> B, D

Поиск в глубину (DFS)

Поиск в глубину (DFS) - это основной алгоритм обхода графа, который исследует как можно дальше по каждой ветви, прежде чем вернуться назад. Его можно использовать для решения многих задач на графах, таких как поиск связных компонент, топологическая сортировка и обнаружение циклов.

Алгоритм DFS работает следующим образом:

  1. Начинаем с исходной вершины s.
  2. Помечаем текущую вершину как посещенную.
  3. Рекурсивно посещаем все непосещенные вершины w, смежные с текущей вершиной.
  4. Если все смежные вершины текущей вершины были посещены, возвращаемся к вершине, из которой была исследована текущая вершина.
  5. Если остались непосещенные вершины, выбираем одну из них и повторяем с шага 1.

Вот простая реализация DFS на Java с использованием списков смежности:

boolean[] visited;
 
void dfs(List<Integer>[] graph, int v) {
    visited[v] = true;
    for (int w : graph[v]) {
        if (!visited[w]) {
            dfs(graph, w);
        }
    }
}

Для полного обхода графа с помощью DFS, мы вызываем dfs(graph, s) для каждой вершины s в графе, где visited изначально установлен в false для всех вершин.

DFS имеет множество применений. Например, мы можем использовать его для поиска связных компонент в неориентированном графе, запуская DFS из каждой непосещенной вершины и присваивая каждую вершину компоненте в соответствии с деревом DFS.

Поиск в ширину (BFS)

Поиск в ширину (BFS) - это еще один основной алгоритм обхода графа, который исследует вершины слоями. Он посещает все вершины на текущем уровне глубины, прежде чем перейти к вершинам на следующем уровне.

Алгоритм BFS работает следующим образом:

  1. Начинаем с исходной вершины s и помечаем ее как посещенную.
  2. Помещаем s в очередь FIFO.
  3. Пока очередь не пуста:Вот перевод на русский язык:

Пока очередь не пуста:

  • Извлечь вершину v из очереди.
  • Для каждой непомеченной вершины w, смежной с v:
    • Пометить w как посещенную.
    • Добавить w в очередь.

Вот реализация BFS на Java с использованием списков смежности:

boolean[] visited;
 
void bfs(List<Integer>[] graph, int s) {
    Queue<Integer> queue = new LinkedList<>();
    visited[s] = true;
    queue.offer(s);
 
    while (!queue.isEmpty()) {
        int v = queue.poll();
        for (int w : graph[v]) {
            if (!visited[w]) {
                visited[w] = true;
                queue.offer(w);
            }
        }
    }
}

BFS особенно полезен для поиска кратчайших путей в неориентированных графах без весов. Расстояние от исходной вершины до любой другой вершины - это минимальное число ребер в пути между ними. BFS гарантирует нахождение кратчайшего пути.

Минимальные остовные деревья

Минимальное остовное дерево (МОД) - это подмножество ребер связного, взвешенного неориентированного графа, которое соединяет все вершины, не содержит циклов и имеет минимальный общий вес ребер.

Два классических алгоритма для нахождения МОД - это алгоритм Краскала и алгоритм Прима.

Алгоритм Краскала работает следующим образом:

  1. Создать лес F, где каждая вершина - отдельное дерево.
  2. Создать множество S, содержащее все ребра графа.
  3. Пока S не пусто и F еще не является остовным деревом:
    • Удалить ребро минимального веса из S.
    • Если удаленное ребро соединяет две разные деревья, добавить его в F, объединяя два дерева в одно.

Алгоритм Прима работает следующим образом:

  1. Инициализировать дерево одной произвольно выбранной вершиной графа.
  2. Расширять дерево на одно ребро: из всех ребер, соединяющих дерево с вершинами, не входящими в дерево, найти ребро минимального веса и добавить его в дерево.
  3. Повторять шаг 2, пока все вершины не будут в дереве.

Вот реализация алгоритма Прима на Java:

int minKey(int[] key, boolean[] mstSet, int V) {
    int min = Integer.MAX_VALUE, min_index = -1;
    for (int v = 0; v < V; v++) {
    if (!mstSet[v] && key[v] < min) {
        min = key[v];
        min_index = v;
    }
}
return min_index;
}
 
void primMST(int[][] graph, int V) {
    int[] parent = new int[V];
    int[] key = new int[V];
    boolean[] mstSet = new boolean[V];
 
    for (int i = 0; i < V; i++) {
        // Инициализируем ключ каждой вершины как бесконечность
        key[i] = Integer.MAX_VALUE;
        // Помечаем все вершины как не включенные в MST
        mstSet[i] = false;
    }
 
    // Начальная вершина имеет ключ 0
    key[0] = 0;
    // Начальная вершина не имеет родителя
    parent[0] = -1;
 
    for (int count = 0; count < V - 1; count++) {
        // Выбираем вершину с минимальным ключом из непосещенных
        int u = minKey(key, mstSet, V);
        // Помечаем выбранную вершину как посещенную
        mstSet[u] = true;
 
        for (int v = 0; v < V; v++) {
            // Если есть ребро от u до v, v не посещена и вес ребра меньше ключа v
            if (graph[u][v] != 0 && !mstSet[v] && graph[u][v] < key[v]) {
                // Обновляем родителя v на u
                parent[v] = u;
                // Обновляем ключ v
                key[v] = graph[u][v];
            }
        }
    }
 
    printMST(parent, graph, V);
}

MST (Минимальное остовное дерево) имеет множество применений, таких как проектирование сетей (связь, электрические, гидравлические, компьютерные) и приближение задачи коммивояжера.

Кратчайшие пути

Задача о кратчайшем пути заключается в поиске пути между двумя вершинами в графе, при котором сумма весов ребер будет минимальной. Эта задача имеет множество вариаций, таких как кратчайшие пути из одного источника, все кратчайшие пути и кратчайшие пути до одного пункта назначения.

Алгоритм Дейкстры - это жадный алгоритм, который решает задачу о кратчайших путях из одного источника для графа с неотрицательными весами ребер. Он работает следующим образом:

  1. Создаем множество sptSet (множество кратчайших путей), которое отслеживает вершины, включенные в дерево кратчайших путей.
  2. Присваиваем расстояние всем вершинам в графе. Инициализируем все расстояния как бесконечность. Присваиваем расстояние 0 для исходной вершины.
  3. Пока sptSet не включает все вершины, выбираем вершину v, которая не входит в sptSet и имеет минимальное расстояние. Включаем v в sptSet.
  4. Обновляем расстояния всех смежных вершин v. Для обновления расстояний перебираем все смежные вершины. Для каждой смежной вершины w, если сумма расстояния до v и веса ребра (v, w) меньше текущего расстояния до w, обновляем расстояние до w.Вот перевод на русский язык с сохранением комментариев к коду:

Если значение v (из источника) и вес ребра v-w меньше, чем значение расстояния w, то обновите значение расстояния w.

Вот реализация алгоритма Дейкстры на Java:

public void dijkstra(int[][] graph, int src) {
    int V = graph.length;
    int[] dist = new int[V];
    boolean[] sptSet = new boolean[V];
 
    for (int i = 0; i < V; i++) {
        dist[i] = Integer.MAX_VALUE;
        sptSet[i] = false;
    }
 
    dist[src] = 0;
 
    for (int count = 0; count < V - 1; count++) {
        int u = minDistance(dist, sptSet);
        sptSet[u] = true;
 
        for (int v = 0; v < V; v++) {
            if (!sptSet[v] && graph[u][v] != 0 && dist[u] != Integer.MAX_VALUE
                    && dist[u] + graph[u][v] < dist[v]) {
                dist[v] = dist[u] + graph[u][v];
            }
        }
    }
 
    printSolution(dist);
}

Алгоритм Беллмана-Форда - это другой алгоритм для поиска кратчайших путей от одного источника ко всем другим вершинам в взвешенном ориентированном графе. В отличие от алгоритма Дейкстры, алгоритм Беллмана-Форда может обрабатывать графы с отрицательными весами ребер, при условии, что в графе нет отрицательных циклов.

Алгоритм работает следующим образом:

  1. Инициализируйте расстояния от источника до всех вершин как бесконечность, а расстояние до самого источника как 0.
  2. Релаксируйте все ребра |V| - 1 раз. Для каждого ребра u-v, если расстояние до v можно сократить, используя ребро u-v, обновите расстояние до v.
  3. Проверьте наличие циклов с отрицательным весом. Выполните еще один шаг релаксации для всех ребер. Если какое-либо расстояние изменилось, значит, есть цикл с отрицательным весом.

Вот реализация алгоритма Беллмана-Форда на Java:

public void bellmanFord(int[][] graph, int src) {
    int V = graph.length;
    int[] dist = new int[V];
 
    for (int i = 0; i < V; i++)
        dist[i] = Integer.MAX_VALUE;
    dist[src] = 0;
 
    for (int i = 1; i < V; i++) {
        for (int u = 0; u < V; u++) {
            for (int v = 0; v < V; v++) {
                if (graph[u][v] != 0 && dist[u] != Integer.MAX_VALUE
                        && dist[u] + graph[u][v] < dist[v]) {
                    dist[v] = dist[u] + graph[u][v];
                }
            }
        }
    }
 
    for (int u = 0; u < V; u++) {
        for (int v = 0; v < V; v++) {
            if (graph[u][v] != 0 && dist[u] != Integer.MAX_VALUE
                    && dist[u] + graph[u][v] < dist[v]) {
                System.out.println("Graph contains negative weight cycle");
                return;
            }
        }
    }
 
    printSolution(dist);
}

Комментарии:

// Если расстояние от начальной вершины до вершины u не равно максимальному значению Integer // и сумма расстояния от начальной вершины до вершины u и веса ребра между u и v меньше, чем расстояние от начальной вершины до вершины v, // то обновляем расстояние от начальной вершины до вершины v.

for (int u = 0; u < V; u++) {
    for (int v = 0; v < V; v++) {
        if (graph[u][v] != 0 && dist[u] != Integer.MAX_VALUE
                && dist[u] + graph[u][v] < dist[v]) {
            System.out.println("График содержит цикл с отрицательным весом");
            return;
        }
    }
}

Комментарии:

// Проверяем все пары вершин u и v // Если вес ребра между u и v не равен 0, расстояние от начальной вершины до вершины u не равно максимальному значению Integer // и сумма расстояния от начальной вершины до вершины u и веса ребра между u и v меньше, чем расстояние от начальной вершины до вершины v, // то выводим сообщение о том, что граф содержит цикл с отрицательным весом и возвращаем.

printSolution(dist);

Комментарии:

// Выводим решение (кратчайшие расстояния от начальной вершины до всех остальных вершин).# Основные проблемы

Проблема 1: Нехватка времени

Многие люди сталкиваются с проблемой нехватки времени в своей повседневной жизни. Это может быть связано с большим количеством обязанностей, работой, семьей и другими факторами. Важно научиться управлять своим временем эффективно, чтобы успевать выполнять все необходимые задачи.

# Этот код демонстрирует простой таймер
import time
 
def timer():
    start_time = time.time()
    # Здесь вы можете добавить свой код
    end_time = time.time()
    print(f"Время выполнения: {end_time - start_time} секунд")
 
timer()

Проблема 2: Стресс и тревожность

Стресс и тревожность - это распространенные проблемы, с которыми сталкиваются многие люди. Они могут негативно влиять на физическое и психическое здоровье. Важно найти способы управления стрессом, такие как практика медитации, физические упражнения и поддержка близких.

# Этот код демонстрирует простое упражнение на дыхание для снижения стресса
import time
 
def deep_breathing():
    print("Начните глубокое дыхание:")
    for i in range(5):
        print("Вдох...")
        time.sleep(2)
        print("Выдох...")
        time.sleep(2)
    print("Отлично! Вы почувствуете себя более расслабленным.")
 
deep_breathing()

Проблема 3: Финансовые трудности

Финансовые трудности - это еще одна распространенная проблема, с которой сталкиваются многие люди. Это может быть связано с низким доходом, высокими расходами или непредвиденными ситуациями. Важно научиться управлять своими финансами, составлять бюджет и искать способы увеличения дохода.

# Этот код демонстрирует простой калькулятор бюджета
def budget_calculator():
    income = float(input("Введите ваш ежемесячный доход: "))
    expenses = float(input("Введите ваши ежемесячные расходы: "))
    savings = income - expenses
    print(f"Ваши ежемесячные сбережения: {savings}")
 
budget_calculator()

Эти три основные проблемы - нехватка времени, стресс и тревожность, а также финансовые трудности - являются распространенными вызовами, с которыми сталкиваются многие люди. Важно найти эффективные способы решения этих проблем, чтобы улучшить качество жизни.