Какие алгоритмы на графах знаете?cplus-54

Графы — фундаментальная структура данных в информатике. Рассмотрим ключевые алгоритмы для работы с графами и их реализации на C/C++.

1. Представление графов

Список смежности

#include <vector>
using Graph = std::vector<std::vector<int>>;

// Нумерованные вершины 0..n-1
Graph g(n);
g[0].push_back(1); // Ребро 0 → 1

Матрица смежности

#include <vector>
using AdjMatrix = std::vector<std::vector<bool>>;

AdjMatrix m(n, std::vector<bool>(n, false));
m[0][1] = true; // Ребро 0 → 1

2. Обход графов

Поиск в глубину

void dfs(const Graph& g, int v, std::vector<bool>& visited) {
    visited[v] = true;
    for(int u : g[v]) {
        if(!visited[u]) {
            dfs(g, u, visited);
        }
    }
}

Применение: компоненты связности, топологическая сортировка, поиск циклов

Поиск в ширину

#include <queue>
void bfs(const Graph& g, int start) {
    std::vector<bool> visited(g.size(), false);
    std::queue<int> q;
    q.push(start);
    visited[start] = true;
    
    while(!q.empty()) {
        int v = q.front(); q.pop();
        for(int u : g[v]) {
            if(!visited[u]) {
                visited[u] = true;
                q.push(u);
            }
        }
    }
}

Применение: кратчайшие пути в невзвешенных графах, проверка двудольности

3. Алгоритмы на взвешенных графах

Алгоритм Дейкстры

#include <queue>
using Pair = std::pair<int, int>; // вес, вершина

void dijkstra(const Graph& g, int start) {
    std::vector<int> dist(g.size(), INT_MAX);
    std::priority_queue<Pair, std::vector<Pair>, std::greater<Pair>> pq;
    
    dist[start] = 0;
    pq.push({0, start});
    
    while(!pq.empty()) {
        auto [d, v] = pq.top(); pq.pop();
        if(d > dist[v]) continue;
        
        for(auto& [u, w] : g[v]) {
            if(dist[u] > dist[v] + w) {
                dist[u] = dist[v] + w;
                pq.push({dist[u], u});
            }
        }
    }
}

Ограничения: только для графов без отрицательных весов

Алгоритм Флойда-Уоршелла

void floydWarshall(std::vector<std::vector<int>>& dist) {
    int n = dist.size();
    for(int k = 0; k < n; ++k)
        for(int i = 0; i < n; ++i)
            for(int j = 0; j < n; ++j)
                if(dist[i][k] != INT_MAX && dist[k][j] != INT_MAX)
                    dist[i][j] = std::min(dist[i][j], dist[i][k] + dist[k][j]);
}

Особенности: O(n³), работает с отрицательными весами (но не с циклами отрицательного веса)

4. Остовные деревья

Алгоритм Крускала

#include <algorithm>
#include <numeric>

struct Edge { int u, v, w; };

int kruskal(std::vector<Edge>& edges, int n) {
    std::sort(edges.begin(), edges.end(), [](Edge a, Edge b) { return a.w < b.w; });
    
    std::vector<int> parent(n);
    std::iota(parent.begin(), parent.end(), 0);
    
    auto find = [&](int u) {
        while(parent[u] != u) u = parent[u] = parent[parent[u]];
        return u;
    };
    
    int total = 0;
    for(Edge e : edges) {
        int u_root = find(e.u);
        int v_root = find(e.v);
        if(u_root != v_root) {
            parent[v_root] = u_root;
            total += e.w;
        }
    }
    return total;
}

Использует: систему непересекающихся множеств (DSU)

5. Сильно связные компоненты

Алгоритм Косарайю

void kosaraju(const Graph& g) {
    int n = g.size();
    std::vector<int> order;
    std::vector<bool> visited(n, false);
    
    // Первый проход (топологическая сортировка)
    std::function<void(int)> dfs1 = [&](int v) {
        visited[v] = true;
        for(int u : g[v])
            if(!visited[u]) dfs1(u);
        order.push_back(v);
    };
    
    // Транспонирование графа
    Graph gT(n);
    for(int v = 0; v < n; ++v)
        for(int u : g[v])
            gT[u].push_back(v);
    
    // Второй проход
    std::vector<int> component(n);
    int current_component = 0;
    visited.assign(n, false);
    
    for(int v : order)
        if(!visited[v]) {
            std::stack<int> stack;
            stack.push(v);
            visited[v] = true;
            component[v] = current_component;
            
            while(!stack.empty()) {
                int u = stack.top(); stack.pop();
                for(int w : gT[u])
                    if(!visited[w]) {
                        visited[w] = true;
                        component[w] = current_component;
                        stack.push(w);
                    }
            }
            current_component++;
        }
}

6. Потоки в сетях

Алгоритм Форда-Фалкерсона

bool bfs_fordFulkerson(const Graph& residual, int s, int t, std::vector<int>& parent) {
    std::vector<bool> visited(residual.size(), false);
    std::queue<int> q;
    q.push(s);
    visited[s] = true;
    parent[s] = -1;
    
    while(!q.empty()) {
        int u = q.front(); q.pop();
        for(int v = 0; v < residual.size(); ++v) {
            if(!visited[v] && residual[u][v] > 0) {
                if(v == t) {
                    parent[v] = u;
                    return true;
                }
                q.push(v);
                parent[v] = u;
                visited[v] = true;
            }
        }
    }
    return false;
}

int fordFulkerson(Graph& graph, int s, int t) {
    Graph residual = graph;
    std::vector<int> parent(graph.size());
    int max_flow = 0;
    
    while(bfs_fordFulkerson(residual, s, t, parent)) {
        int path_flow = INT_MAX;
        for(int v = t; v != s; v = parent[v]) {
            int u = parent[v];
            path_flow = std::min(path_flow, residual[u][v]);
        }
        
        for(int v = t; v != s; v = parent[v]) {
            int u = parent[v];
            residual[u][v] -= path_flow;
            residual[v][u] += path_flow;
        }
        
        max_flow += path_flow;
    }
    return max_flow;
}

Сравнение алгоритмов

АлгоритмСложностьПрименение
DFS/BFSO(V + E)Обход, компоненты связности
ДейкстраO(E + V log V)Кратчайшие пути (нет отр. весов)
Флойд-УоршеллO(V³)Все пары вершин
КрускалO(E log E)Минимальное остовное дерево
КосарайюO(V + E)Сильно связные компоненты
Форд-ФалкерсонO(E * max_flow)Максимальный поток

Резюмируем: выбор алгоритма зависит от типа графа (взвешенный/невзвешенный, ориентированный/неориентированный) и решаемой задачи. Для большинства задач на графах в C++ удобно использовать представление в виде списка смежности и стандартные контейнеры (vector, queue, priority_queue).