zoukankan      html  css  js  c++  java
  • [算法笔记]基础图论总结一(最短路、最小生成树、拓扑排序)

    零、基础:

    1、图的存储方式:

    1)、邻接表:

    vector<int> h(n, -1);  // 邻接表表头指针
    vector<int> ne(n);  // next指针
    vector<int> e(n);  // value
    vector<int> w(n);  // 边的权值,有时不需要
    int idx = 0;  // 结点的全局标识符
    
    // 1、增加一条边
    void add(int a, int b, int c) {
        ne[idx] = h[a], e[idx] = b, w[idx] = c, h[a] = idx++;
    }
    
    // 2、遍历与t点相连的所有边
    for (int i = h[t]; i != -1; i = ne[i]) {
        int j = e[i];
        // t, j就是相连的两个点
    }
    

    2)、邻接矩阵:

    vector<vector<int>> g;
    
    // 1、初始化
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            if (i == j) g[i][j] = 0;
            else g[i][j] = 1e8;
        }
    }
    
    // 2、增加一条边
    g[i][j] = w;
    
    // 3、遍历t点所有边
    // 邻接矩阵无法记住相邻关系,所有只有全部遍历
    for (int i = 0; i < n; i++) {
        cout << g[t][i];
    }
    

    3)、结构体:

    有些算法(例如Bellman-Ford算法)不一定需要将图完整表示出来,我们只关注边的信息
    
    struct Edge {
        int from, to, w;
        bool operator< (const Edge& e) const {
            return w < e.w;
        }
    }
    

    一、最短路算法:

    1、Dijkstra算法:

    int dijkstra(...) {
    	vector<int> dist(n, 1e8);  // 距离起点的距离
        vector<bool> vis(n, false);  // 是否确定了最短路径
        priority_queue<PII, vector<PII>, greater<>> q;  // 优先队列,确定离起点最近的非树结点
        dist[0] = 0;
        q.push({0, 0});  // {dist[i], i},dist置于pair第一维,用于排序
        while (!q.empty()) {
            int t = q.top().second;
            q.pop();
            if (!vis[t]) { 
                // 遍历结点,根据图的不同存储方式,有两种形式,参考上文,这里使用邻接表形式
                for (int i = h[t]; i != -1; i = ne[i]) {
                    int j = e[i];
                    if (dist[j] > dist[t] + w[i]) {
                        dist[j] = dist[t] + w[i];
                        q.push({dist[j], j});
                    }
                }
                vis[t] = true;
            }
        }
        return dist[n - 1];
    }
    

    2、朴素Bellman-Ford算法:

    • 朴素Bellman-Ford算法效率较低,但是有一个特别的应用场景:有边数限制的最短路算法,假设边数限制为k。
    • Bellman-Ford只关注边,因此选用上述结构体方式存图。
    vector<Edge> edges;  // 图已建好
    
    int bellman_ford(...) {
        vector<int> dist(n, 1e8);
        dist[0] = 0;
    	for (int i = 0; i < k; i++) {
            auto last = dist;  // 必须使用上一次的dist,因为这一轮的dist可能被更新
            for (auto& edge : edges) {
                auto [a, b, c] = edge;
                dist[b] = min(dist[b], last[a] + c);
            }
        }
        if (dist[n - 1] >= 1e8 / 2) return -1;  // 不直接判断相等是因为在权值为负情况下,有可能会被更新
        return dist[n - 1];
    }
    

    3、SPFA算法(队列优化Bellman-Ford算法):

    • 优化思路:假设源点s到点b之间有点a,当dist[a]没有被更新时,dist[b]更新没有意义。
    • 由于使用队列,需要表示点,因此这里选择邻接表。还需要一个数组inQue表示是否在队列中。
    • SPFA与Dijkstra一样使用数组标识一个结点的状态,不同的是:前者表示是否在队列中,因此每次往队列中加入元素j时,inQue[j] = true; 后者表示该点最小路径是否确定,因此必须是t结点被pop出时确定。
    int spfa(...) {
        vector<int> dist(n, 1e8);
        vector<bool> inQue(n, false);
        queue<int> q;
    	dist[0] = 0;
        q.push(0);
        inQue[0] = true;
        while (!q.empty()) {
            int t = q.front();
            q.pop();
            inQue[t] = false;
            for (int i = h[t]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] > dist[t] + w[i]) {
                    dist[j] = dist[t] + w[i];
                    if (!inQue(j)) {
                        q.push(j);
                        inQue[j] = true;
                    }
                }
            }
        }
        if (dist[n - 1] >= 1e8 / 2) return -1;  // 不直接判断相等是因为在权值为负情况下,有可能会被更新
        return dist[n - 1];
    }
    
    • Bellman-Ford算法还有一个应用,一般用SPFA来写,即判断负权环是否存在。只要在上述代码上做些修改即可。
    bool spfa(...) {
        vector<int> dist(n, 1e8);
    	dist[0] = 0;
        vector<int> cnt(n);  // 从源点到点x的边数
        // 为了防止负权环并不在 点0 ~ 点n-1的路径上,把所有点都加入队列中
        for (int i = 0; i < n; i++) {
            q.push(i);
            inQue[i] = true;
        }
        
        while (!q.empty()) {
            int t = q.front();
           // ...
            for (int i = h[t]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] > dist[t] + w[i]) {
                    dist[j] = dist[t] + w[i];
                    cnt[j] = cnt[t] + 1;
                    if (cnt[j] >= n) return true;  // 说明这条路径上存在负权环
                    if (!inQue(j)) {
    					// ...
                    }
                }
            }
        }
        return false
    }
    

    4、Floyd算法:

    • Floyd算法一般用来求多源最短路径,即可以求图中任意两点的最短路径。

    • 使用邻接矩阵,类似于动态规划

    void floyd() {
        for (int k = 0; k < n; k++) {
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    g[i][j] = min(g[i][j], g[i][k] + g[k][j])
                }
            }
        }
    }
    

    二、最小生成树算法:

    1、Prim算法:

    • 类似于Dijkstra算法,实际上Dijkstra也再次发现了Prim算法。
    • Prim算法每次将非最小生成树中的dist最小点加入最小生成树,Dijkstra算法将非最短路径树的最小点加入最短路径树。
    • 基于邻接表实现的时间复杂度在边非常多时容易TLE,这里给出邻接矩阵实现方式。
    int prim() {
    	vector<int> dist(n, 1e8);  // 距离起点的距离
        vector<bool> vis(n, false);  // 是否确定了最短路径
        int res = 0;
        dist[0] = 0;
        for (int i = 0; i < n; i++) {
            int t = -1;
            // 遍历所有节点,找到距离生成树集合最近的节点
            for (int j = 0; j < n; j++) {
                if (!vis[j] && (t == -1 || dist[j] < dist[t])) {
                    t = j;
                }
            }
            // 不存在可以加入最小生成树的点,直接返回false
            if (dist[t] == 1e8) return -1;
            res += dist[t];
            vis[t] = true;
            // 松弛操作
            for (int j = 0; j <= n; j++) {
                dist[j] = min(dist[j], g[t][j]);
            }
        }
        return res;
    }
    

    2、Kruskal算法:

    • 基于并查集实现。
    vector<int> p(n);
    
    int find(int x) {
        return x == p[x] ? x : (p[x] = find(p[x]));
    }
    
    // 对m条边进行排序,每次取最小边加入最小生成树
    Edge edges[m];
    
    int kruskal() {
        //  res:最小生成树代价,cnt:最小生成树中节点个数,如果最后小于n,则返回-1
        int res = 0, cnt = 0;
        sort(edges, edges + m);
        for (int i = 0; i < n; i++) p[i] = i;
        for (int i = 0; i < m; i++) {
            int a = edges[i].from, b = edges[i].to, c = edges[i].w;
            a = find(a), b = find(b);
            // 如果两个点不属于一个连通分量中,则加入最小生成树
            if (a != b) {
                p[a] = b;
                res += c;
                cnt++;
            }
        }
        if (cnt < n - 1) return -1;
        return res;
    }
    

    三、拓扑排序:

    • 基本要点很简单,首先存图,接着将所有点放入优先队列中,取出队头入度为0的节点,所有与该节点相连的节点的入度--。不断重复该操作。
    • 但是需要注意的是,在STL自带优先队列中动态更改值比较麻烦。因此我们需要每次手动遍历寻找ind为0的节点,因此使用普通的queue,甚至数组都是可以的,这里我们使用模拟队列。
    // 邻接表存图
    // 每个点的入度
    int h[N], ne[M], e[M], ind[N], idx;
    
    void add(int a, int b) {
        // b节点入度++
        ne[idx] = h[a], e[idx] = b, ind[b]++, h[a] = idx++;
    } 
    
    bool top_sort() {
        // 模拟队列
        int q[N], hh = 0, tt = -1;
        for (int i = 0; i < n; i++) {
            if (!ind[i]) {
                q[++tt] = i;
            }
        }
        while (hh <= tt) {
            int t = q[hh++];
            for (int i = h[t]; i != -1; i = ne[i]) {
                int j = e[i];
                ind[j]--;
                if (!ind[j]) {
                    q[++tt] = j;
                }
            }
        }
        // 队列中应包含所有节点
        return tt == n - 1;
    }
    
  • 相关阅读:
    第十四周课程总结&实验报告(简单记事本的实现)
    第十三周课程总结
    第十二周课程总结
    第十一周课程总结
    第十周课程总结
    第九周课程总结&实验报告(七)
    第八周课程总结&实验报告(六)
    第七周课程总结&试验报告(五)
    基于C的
    RMQ 区间最值问题
  • 原文地址:https://www.cnblogs.com/enmac/p/14043948.html
Copyright © 2011-2022 走看看