图论算法详解

图论是计算机科学中的一个重要领域,研究图(Graph)的结构、性质以及相关算法。图由**顶点(Vertex)边(Edge)**组成,广泛应用于社交网络、路径规划、网络流等问题。

1. 图的基本概念

图的定义

  • 图(Graph):由一组顶点 VV 和一组边 EE 组成,记作 G=(V,E)G=(V,E)。

  • 有向图(Directed Graph):边有方向。

  • 无向图(Undirected Graph):边无方向。

  • 权重图(Weighted Graph):边带有权重。

图的表示方法

  1. 邻接矩阵(Adjacency Matrix)

    • 使用二维数组表示图。

    • 适合稠密图。

    • 空间复杂度:O(V2)O(V2)。

  2. 邻接表(Adjacency List)

    • 使用链表或数组的数组表示图。

    • 适合稀疏图。

    • 空间复杂度:O(V+E)O(V+E)。


2. 常见图论算法

2.1 深度优先搜索(DFS)

核心思想
  • 从起点出发,沿着一条路径尽可能深入地访问顶点,直到无法继续为止,然后回溯。

详细步骤
  1. 从起点开始,标记当前顶点为已访问。

  2. 遍历当前顶点的所有邻居:

    • 如果邻居未被访问,则递归调用 DFS。

  3. 回溯到上一个顶点,继续遍历其他邻居。

  4. 重复上述步骤,直到所有顶点都被访问。

应用场景
  • 图的连通性检测。

  • 拓扑排序。

  • 寻找强连通分量。

代码实现
void dfs(int node, vector<vector<int>>& graph, vector<bool>& visited) {
    visited[node] = true;
    cout << node << " "; // 输出访问的节点
    for (int neighbor : graph[node]) {
        if (!visited[neighbor]) {
            dfs(neighbor, graph, visited);
        }
    }
}

// 测试代码
int main() {
    vector<vector<int>> graph = {
        {1, 2}, // 节点 0 的邻居
        {0, 3}, // 节点 1 的邻居
        {0, 3}, // 节点 2 的邻居
        {1, 2}  // 节点 3 的邻居
    };
    vector<bool> visited(graph.size(), false);
    dfs(0, graph, visited); // 从节点 0 开始 DFS
    return 0;
}

2.2 广度优先搜索(BFS)

核心思想
  • 从起点出发,逐层访问顶点,先访问距离起点最近的顶点。

详细步骤
  1. 从起点开始,将其加入队列并标记为已访问。

  2. 从队列中取出一个顶点,访问其所有邻居:

    如果邻居未被访问,则将其加入队列并标记为已访问。
  3. 重复上述步骤,直到队列为空。

应用场景
  • 最短路径(无权图)。

  • 图的连通性检测。

代码实现
void bfs(int start, vector<vector<int>>& graph) {
    queue<int> q;
    vector<bool> visited(graph.size(), false);

    q.push(start);
    visited[start] = true;

    while (!q.empty()) {
        int node = q.front();
        q.pop();
        cout << node << " "; // 输出访问的节点
        for (int neighbor : graph[node]) {
            if (!visited[neighbor]) {
                visited[neighbor] = true;
                q.push(neighbor);
            }
        }
    }
}

// 测试代码
int main() {
    vector<vector<int>> graph = {
        {1, 2}, // 节点 0 的邻居
        {0, 3}, // 节点 1 的邻居
        {0, 3}, // 节点 2 的邻居
        {1, 2}  // 节点 3 的邻居
    };
    bfs(0, graph); // 从节点 0 开始 BFS
    return 0;
}

2.3 最短路径算法

Dijkstra 算法
  • 核心思想:贪心算法,每次选择当前距离起点最近的顶点,更新其邻居的距离。

  • 详细步骤
  • 初始化距离数组,起点的距离为 0,其他顶点的距离为无穷大。

  • 将起点加入优先队列。

  • 从优先队列中取出距离最小的顶点,遍历其邻居:

    • 如果通过当前顶点到达邻居的距离更短,则更新邻居的距离,并将其加入优先队列。

  • 重复上述步骤,直到优先队列为空。

  • 适用场景:带权图(权重非负)。

  • 时间复杂度:O(V2)O(V2)(朴素实现),O(E+Vlog⁡V)O(E+VlogV)(优先队列优化)。

void dijkstra(int start, vector<vector<pair<int, int>>>& graph, vector<int>& dist) {
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
    dist[start] = 0;
    pq.push({0, start});

    while (!pq.empty()) {
        int u = pq.top().second;
        pq.pop();
        for (auto& edge : graph[u]) {
            int v = edge.first, w = edge.second;
            if (dist[v] > dist[u] + w) {
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }
}
Bellman-Ford 算法
  • 核心思想:动态规划,通过松弛操作逐步更新最短路径。

  • 详细步骤
  • 初始化距离数组,起点的距离为 0,其他顶点的距离为无穷大。

  • 进行 V−1V−1 次松弛操作:

    • 遍历所有边,如果通过当前边到达终点的距离更短,则更新终点的距离。

  • 检测负权环:

    • 再次遍历所有边,如果仍能更新距离,则说明存在负权环。

  • 适用场景:带权图(允许负权重)。

  • 时间复杂度:O(V⋅E)O(V⋅E)。

bool bellmanFord(int start, vector<vector<pair<int, int>>>& graph, vector<int>& dist) {
    dist[start] = 0;
    for (int i = 0; i < graph.size() - 1; i++) {
        for (int u = 0; u < graph.size(); u++) {
            for (auto& edge : graph[u]) {
                int v = edge.first, w = edge.second;
                if (dist[u] != INT_MAX && dist[v] > dist[u] + w) {
                    dist[v] = dist[u] + w;
                }
            }
        }
    }
    // 检测负权环
    for (int u = 0; u < graph.size(); u++) {
        for (auto& edge : graph[u]) {
            int v = edge.first, w = edge.second;
            if (dist[u] != INT_MAX && dist[v] > dist[u] + w) {
                return false; // 存在负权环
            }
        }
    }
    return true;
}
Floyd-Warshall 算法
  • 核心思想:动态规划,计算所有顶点对之间的最短路径。

  • 详细步骤
  • 初始化距离矩阵,对角线元素为 0,其他元素为无穷大。

  • 对于每个中间顶点 kk,更新所有顶点对 (i,j)(i,j) 的距离:

    • 如果通过 kk 到达 jj 的距离更短,则更新 dist[i][j]dist[i][j]。

  • 重复上述步骤,直到所有中间顶点都被考虑。

  • 适用场景:带权图(允许负权重)。

  • 时间复杂度:O(V3)O(V3)。

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

2.4 最小生成树算法

Kruskal 算法
  • 核心思想:贪心算法,按权重从小到大选择边,确保不形成环。

  • 详细步骤
  • 将所有边按权重从小到大排序。

  • 初始化并查集,每个顶点自成一个集合。

  • 遍历所有边:

    • 如果边的两个顶点不在同一个集合中,则将边加入最小生成树,并合并两个集合。

  • 重复上述步骤,直到最小生成树包含 V−1V−1 条边。

  • 适用场景:无向图。

  • 时间复杂度:O(Elog⁡E)O(ElogE)。

struct Edge {
    int u, v, w;
    bool operator<(const Edge& other) const {
        return w < other.w;
    }
};

int findParent(int u, vector<int>& parent) {
    if (parent[u] != u) {
        parent[u] = findParent(parent[u], parent);
    }
    return parent[u];
}

void kruskal(vector<Edge>& edges, int V) {
    sort(edges.begin(), edges.end());
    vector<int> parent(V);
    for (int i = 0; i < V; i++) parent[i] = i;

    vector<Edge> result;
    for (Edge& edge : edges) {
        int uRoot = findParent(edge.u, parent);
        int vRoot = findParent(edge.v, parent);
        if (uRoot != vRoot) {
            result.push_back(edge);
            parent[uRoot] = vRoot;
        }
    }
}
Prim 算法
  • 核心思想:贪心算法,从起点开始逐步扩展最小生成树。

  • 详细步骤
  • 初始化优先队列,起点的权重为 0,其他顶点的权重为无穷大。

  • 将起点加入优先队列。

  • 从优先队列中取出权重最小的顶点,遍历其邻居:

    • 如果邻居未被访问且通过当前顶点到达邻居的权重更小,则更新邻居的权重,并将其加入优先队列。

  • 重复上述步骤,直到优先队列为空。

  • 适用场景:无向图。

  • 时间复杂度:O(Elog⁡V)O(ElogV)。

void prim(int start, vector<vector<pair<int, int>>>& graph) {
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
    vector<bool> visited(graph.size(), false);
    pq.push({0, start});

    while (!pq.empty()) {
        int u = pq.top().second;
        int w = pq.top().first;
        pq.pop();
        if (visited[u]) continue;
        visited[u] = true;
        cout << u << " "; // 输出最小生成树的节点
        for (auto& edge : graph[u]) {
            int v = edge.first, weight = edge.second;
            if (!visited[v]) {
                pq.push({weight, v});
            }
        }
    }
}

3. 总结

算法应用场景时间复杂度
DFS连通性检测、拓扑排序O(V+E)O(V+E)
BFS最短路径(无权图)O(V+E)O(V+E)
Dijkstra最短路径(非负权重)O(E+Vlog⁡V)O(E+VlogV)
Bellman-Ford最短路径(允许负权重)O(V⋅E)O(V⋅E)
Floyd-Warshall所有顶点对最短路径O(V3)O(V3)
Kruskal最小生成树O(Elog⁡E)O(ElogE)
Prim最小生成树O(Elog⁡V)O(ElogV)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值