08 图 | 数据结构与算法

1. 图的基本概念

1. 图的定义

  1. 图: 是由顶点的有穷非空集合和顶点之间边的集合组成的一种数据结构,通常表示为: \(G=( V, E )\)\(V\)是顶点的集合,\(E\)是顶点之间边的集合。通常用顶点表示数据元素,表示数据元素之间的逻辑关系image

  2. 图的分类

    1. 无向图
      1. 无向边:如果顶点\(v_i,v_j\)之间的边之间没有方向,则称这条边为 无向边,表示为\((v_i,v_j)\)
      2. 无向图:如果图的任意两个顶点之间的边都是无向边,那么该图被称为 无向图
    2. 有向图
      1. 有向边:如果顶点\(v_i,v_j\)之间的边之间有方向,则称这条边为 有向边(弧),表示为\(<v_i,v_j>\),其中\(v_i\)被称为 弧尾\(v_j\)被称为 弧头
      2. 有向图:如果图的任意两个顶点之间的边都是有向边,那么该图被称为 有向图
  3. 完全图

    1. 无向图:在具有\(n\)个顶点的无向图中,最大弧数为\(n(n-1)/2\)
    2. 有向图:在具有\(n\)个顶点的有向图中,最大弧数为\(n(n-1)\)
  4. 顶点的度

    1. 无向图的度:一个顶点\(v\)的度是与它相关联的 边的条数
    2. 有向图的度
      1. 出度:以顶点\(v\)弧尾(起点) 的弧的数目
      2. 入度:以顶点\(v\)弧头(终点) 的弧的数目
  5. 路径

    1. 无向图的路径:在无向图\(G=(V,E)\)中,如果存在顶点序列\(v_p,v_{i1},v_{i2},\dots,v_{im},v_q\),使得\((v_p,v_{i1}),(v_{i1},v_{i2}),\dots,(v_{im},v_q)\in E\),则称该序列为从\(v_p\)\(v_q\)的一条 路径
    2. 有向图的路径:在有向图\(G=(V,E)\)中,如果存在顶点序列\(v_p,v_{i1},v_{i2},\dots,v_{im},v_q\),使得\(<v_p,v_{i1}>,<v_{i1},v_{i2}>,\dots,<v_{im},v_q>\in E\),则称该序列为从\(v_p\)\(v_q\)的一条 有向路径
    3. 路径的 长度:路径上边的数量
    4. 带权图的路径长度:路径上各边的权值之和
    5. 简单路径:如果路径上的各顶点\(v_1,v_2,\dots,v_m\)互不相同,则称这样的路径为 简单路径
  6. 子图:如果图\(G=(V,E),G^{\prime} = (V^{\prime}, E^{\prime})\),其中\(V^{i}\in V,E^{\prime}\in E\),那么\(G^{\prime}\)\(G\)子图

  7. 连通图和连通分量(无向图)

    1. 连通性:顶点的连通性:在无向图中,若从顶点\(v_i\)到顶点\(v_j (i\ne j)\)有路径,则称顶点\(v_i\)\(v_j\)连通的
    2. 连通图:如果一个无向图中任意一对顶点都是连通的, 则称此图是 连通图
    3. 连通分量:非连通图的极大连通子图叫做 连通分量
  8. 强连通图与强连通分量

    1. 顶点的强连通性:在有向图中, 若对于每一对顶点\(v_i\)\(v_j (i\ne j)\),都存在一条从\(v_i\)\(v_j\)和从\(v_i\)\(v_j\)的有向路径,则称顶点\(v_i\)\(v_j\)强连通的
    2. 强连通图:如果一个有向图中任意一对顶点都是强连通的, 则称此有向图是 强连通图
    3. 强连通分量:非强连通图的极大强连通子图叫做 强连通分量
  9. 生成树:假设一个连通图有 \(n\)个顶点和 \(e\) 条边,其中 \(n-1\) 条边和 \(n\) 个顶点构成一个极小连通子图,称该极小连通子图为此连通图的 生成树

2. 图与其他数据结构的比较

  1. 比较一
    1. 在线性结构中,数据元素之间仅具有线性关系(1:1)
    2. 在树结构中,结点之间具有层次关系(1:n)
    3. 在图结构中,任意两个点之间都可能有关系(m:n)
  2. 比较二
    1. 在线性结构中,数据元素之间的关系为 前驱后继
    2. 在树结构中,结点之间的关系为 双亲孩子
    3. 在图结构中,顶点之间的关系为 邻接

3. 图的存储

  1. 邻接矩阵表示法

    1. 顶点表:一个记录各个顶点信息的一维数组
    2. 邻接矩阵:一个表示各个顶点之间的关系(边或弧)的二维数组
      1. 定义:设图 \(G = (V, E)\)是一个有\(n\)个顶点的图,则图的邻接矩阵\(A\)定义为

        \[A= \left\{\begin{array}{l} 1,<v_i,v_j>\in E\space or\space (v_i,v_j)\in E \\ 0, 其他 \end{array}\right. \]

      2. 示例image

      3. 特点

        1. 无向图的邻接矩阵是对称的
        2. 有向图的邻接矩阵可能是不对称的
        3. 在无向图中, 第 \(i\) 行 (列) 1 的个数就是顶点\(i\)的度
        4. 在有向图中:第 \(i\) 行 1 的个数就是顶点 \(i\)出度,第 \(j\) 列 1 的个数就是顶点 \(j\)入度
      4. 网(带权图)的邻接矩阵

        1. 定义

          \[A= \left\{\begin{array}{l} W_{i,j},<v_i,v_j>\in E\space or\space (v_i,v_j)\in E \\ \infty, 其他 \end{array}\right. \]

        2. 示例image
  2. 邻接表

    1. 邻接表:是图的一种链式存储结构image

    2. 边(弧)的结点结构

      adjvex *nextArc *info
      该边(弧)所指向的顶点 指向下一条边(弧)指针 该边(弧)相关信息指针
    3. 顶点的结点结构

      data *firstArc
      顶点信息 指向第一条依附该顶点的边(弧)
    4. 有向图的邻接表

      1. 邻接表 (出边表)
      2. 逆邻接表 (入边表)image
    5. 邻接表存储表示

      #define MAXVEX 10000
      // 边表节点
      struct EdgeNode {
          int adjvex; //边指向哪个顶点的索引
          int weight;
          EdgeNode* next;
      };
      // 顶点表结构
      struct VertexNode {
          int value; //顶点的值,为了简化与序号相同,第一个是0
          EdgeNode *firstedge;
      };
      // 图结构
      struct GraphList {
          VertexNode adjList[MAXVEX];
          int numVertex;
          int numEdges;
      };
      
    6. 邻接表建立时间复杂度:设图中有 n 个顶点,e 条边,若顶点信息即为顶点的下标,则时间复杂度为\(O(n+e)\)

  3. 有向图的十字链表

  4. 无向图的邻接多重表

2. 图的遍历

1. 图的遍历

  1. 定义:从图中某一顶点出发访遍图中其余顶点,且使每个顶点仅被访问一次,就叫做图的遍历
  2. 分类:深度优先搜索\(DFS\)和广度优先搜索\(BFS\)

2. 深度优先搜索\(DFS\)

  1. 定义:在访问图中某一起始顶点 \(v\) 后,由 \(v\) 出发,访问它的任一邻接顶点 \(w1\);再从 \(w1\) 出发,访问与 \(w1\)邻接但还没有访问过的顶点 \(w2\);然后再从 \(w2\) 出发,进行类似的访问;如此进行下去,直至到达所有的邻接顶点都被访问过的顶点 \(u\) 为止。接着,退回一步,退到前一次刚访问过的顶点,看是否还有其它没有被访问的邻接顶点。如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问;如果没有,就再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止

  2. 示例:image

  3. 算法:运用栈(递归)结构存储待\(DFS\)的结点

    /*存储结构*/
    /*g[i][j]表示第i个顶点与g[i][j]顶点有边相连接*/
    /*从v0开始*/
    void dfs(vector<vector<int>>& g, int i, vector<int>& visited) {
        cout << i << ' ' ;
        visited[i] = 1;
        for (int& neighbor: g[i]) {
            if (visited[neighbor] == 0) dfs(g, neighbor, visited);
        }
    }
    
    void GraphTraverse(vector<vector<int>>& g) {
        int n = g.size();
        vector<int> visited(n, 0);  // initialize visited array
        for (int i = 0; i < n; i++) {
            if (visited[i] == 0) dfs(g, i, visited);
        }
    }
    
  4. 时间复杂度

    1. 邻接表:扫描边的时间为\(O(e)\);而且对所有顶点递归访问1次,所以遍历图的时间复杂性为\(O(n+e)\)
    2. 邻接矩阵:查找每一个顶点的所有的边,所需时间为\(O(n)\),则遍历图中所有的顶点所需的时间为\(O(n^2)\)

3. 广度优先搜索\(BFS\)

  1. 定义:在访问了起始顶点 \(v\) 之后, 由 \(v\) 出发, 依次访问 \(v\) 的各个未被访问过的邻接顶点 \(w1, w2, \dots, wt\), 然后再顺序访问\(w1, w2, \dots, wt\)的所有还未被访问过的邻接顶点。再从这些访问过的顶点出发,再访问它们的所有还未被访问过的邻接顶点,如此做下去,直到图中所有顶点都被访问到为止

  2. 示例:image

  3. 算法:运用队列结构存储待\(BFS\)的结点

    /* 存储结构 */
    /* g[i][j]表示第i个顶点与g[i][j]顶点有边相连接 */
    /* 从v0开始 */
    void bfs(vector<vector<int>>& g) {
        int n = g.size();
        vector<int> visited(n, 0);  // initialize visited array
        queue<int> q;
        q.push(0);
        while (!q.empty()) {
            int i = q.front();
            cout << i << ' ' ;
            q.pop();
            for (int& neighbor: g[i]) {
                if (visited[neighbor] == 0) {
                    q.push(neighbor);
                }
            }
        }
    }
    
  4. 时间复杂度

    1. 邻接表:遍历图的时间复杂性为\(O(e)\)
    2. 邻接矩阵:遍历图中所有的顶点所需的时间为\(O(n^2)\)

3. 并查集

1. 并查集的定义

  1. 并查集:对于一个集合\(S=\{a_1, a_2,\dots, a_{n-1}, a_n\}\),我们还可以对集合\(S\)进一步划分: \(S_1,S_2,\dots,S_m\)并查集 希望能够快速确定\(S\)中的两两元素是否属于\(S\)的同一子集
  2. 并查集的基本结构:用树表示集合,不同的树是不同的集合,并查集中包含了多棵树,表示并查集中不同的子集,树的集合是森林,所以并查集属于森林

2. 并查集的基本操作

  1. 初始化
    int father[n];      // n:顶点数量
    for (int i = 0; i < n; i++) {
        father[i] = i;
    }
    
  2. find:返回node所属集合树的根节点
    /*递归版*/
    int find(int node) {
        return father[node] == node ? node : find(father[node]);
    }
    /*迭代版*/
    int find(int node) {
        while(father[node] != node) {
            node = father[node];
        }
        return node;
    }
    
  3. join:合并两个点所属的集合
    void join(int node1, int node2) {
        int p1 = find(node1);
        int p2 = find(ndoe2);
        father[node1] = node2;
    }
    

3. 并查集的优化

  1. 优化原因:上面的实现,每一次find时间复杂度为\(O(H)\)\(H\)为树的高度,因为没有对树做特殊处理,所以树的不断合并可能会使树严重不平衡,最坏情况每个节点都只有一个子节点,时间复杂度为\(O(n)\)
  2. 优化方法
    1. 方法一:按秩合并:记录这棵树的高度(记为\(rank\));接下来当合并两棵树时,先对两棵树的高度进行判断,如不同,则让高度小的树的根指向高度大的根
      /*initialize*/
      int father[n];      // n:顶点数量
      int rank[n];
      for(int i=0; i<n; i++) {
          father[i] = i;
          rank[i] = 0;
      }
      /*find*/
      int find(int node) {
          return father[node] == node ? node : find(father[node]);
      }
      /*join:优化*/
      void join(int node1, int node2) {
          int p1 = find(node1);
          int p2 = find(node2);
          if(p1 == p2) return ;
          else if(rank[p1] > rank[p2]) {
              father[p2] = p1;
          }
          else {
              father[p1] = p2;
              if(rank[p1] == rank[p2]) ++rank[p2];
          }
      }
      
    2. 方法二:路径压缩:查询时我们需沿着元素所在的树从下往上查询,最终找到这棵树的根,表明这个元素与其根对应元素属于同一组。因为在此查询过程中我们会经过许多节点,而如果我们能将这个元素直接指向根节点,那么就能节省许多查询的时间。同时,在查询过程中,每次经过的节点,我们都可以同时将他们一起直接指向根节点。这样做的话,我们再查询这些节点时,就能很快找到根
      /*initialize*/
      int father[n];      // n:顶点数量
      for(int i=0; i<n; i++) {
          father[i] = i;
      }
      /*find:优化*/
      int find(int node) {
          return node == father[node] ? node : (father[node] = find(father[node]));
      }
      /*join*/
      void join(int node1, int node2) {
          int p1 = find(node1);
          int p2 = find(node2);
          father[p1] = p2;
      }
      

4. 最小生成树

1. 最小生成树

  1. 生成树的代价:设\(G=(V,E)\)是一个无向连通网,\(E\)中的每一条边上有一个权值,生成树各边的权值之和被称为 生成树的代价
  2. 最小生成树\(MST\):在图\(G\)中的所有生成树中,代价最小的生成树被称为 最小生成树
  3. 最小生成树构造准则
    1. 尽可能用网络中权值最小的边
    2. 必须使用且仅使用 \(n-1\) 条边来联结网络中的 n个顶点
    3. 不能使用产生回路的边

2. \(Prim\)算法

  1. 基本思想:从连通网络 \(N = { V, E }\)中的某一顶点 \(u_0\)出发,选择与它关联的具有最小权值的边\((u_0, v)\),将其顶点加入到生成树的顶点集合\(U\)中。以后每一步从一个顶点在\(U\)中,而另一个顶点不在\(U\)中的各条边中选择权值最小的边\((u, v)\),把该边加入到生成树的边集中,把它的顶点加入到集合\(U\)中;如此重复执行,直到网络中的所有顶点都加入到生成树顶点集合\(U\)中为止

  2. 示例:从v2开始执行\(Prim\)算法image

  3. 注意

    1. 若候选轻权边集中的轻权边不止一条,可任选其中的一条扩充到生成树中
    2. 连通图的最小生成树不一定是唯一的,但它们的权相等
    3. 设连通网络有 \(n\) 个顶点, 则该算法的时间复杂度为 \(O(n^2)\),它适用于边稠密的网络
  4. 算法

    /*
     * 存储结构:g[i][j]表示第i个顶点与第j个顶点的权值
     * 从beginNode开始Prim算法
     * 返回选择的边的数组
     */
    struct edge {
        int begin;     // begin of the edge
        int end;       // end of the edge
        int weight;    // weight of the edge
        edge(int begin, int end, int weight):begin(begin), end(end), weight(weight){}
    };
    vector<edge> Prim(vector<vector<int>>& g, const int beginNode) {
        int n = g.size();
        vector<int> lowcost(n, 0);//lowcost存储生成树顶点集内顶点到树外各顶点各边上当前最小权值
        vector<int> adjvex(n, 0); // adjvex 记录生成树顶点集合外各顶点距离集合内哪个顶点最近
        vector<edge> MST;       // MST 存储最小生成树的边,最后返回
        for(int i = 0; i < n; ++i) {
            lowcost[i] = g[beginNode][i];  // 起始顶点到各点的最小代价
            adjvex[i] = beginNode;
        }
        adjvex[beginNode] = -1;  // -1 表示已经访问
        for(int i = 0; i < n; ++i) { 
            if(i == beginNode) continue;
            int minCost = INT_MAX, pos = -1;
            for(int j = 0; j < n; ++j) {
                if(adjvex[j] != -1 && minCost > lowcost[j]) {  //选择侯选边最小边
                    minCost = lowcost[j];
                    pos = j;
                }
            }
            if(pos != -1) { // pos != -1 表示找到要求顶点
                adjvex[pos] = -1;
                MST.push_back( edge(adjvex[pos], pos, g[adjvex[pos][pos]]) );
                for(int j = 0; j < n; ++j) {        //更新最小代价数组
                    if(adjvex[j] != -1 && lowcost[j] > g[pos][j]) {
                        lowcost[j] = g[pos][j];
                        adjvex[j] = pos;
                    }
                }
            }
        }
        return MST;
    }
    

3. \(Kruskal\)算法

  1. 基本思想:设有一个有 \(n\) 个顶点的连通网络 \(N = \{ V, E \}\),最初先构造一个只有 \(n\)个顶点,没有边的非连通图 \(T = \{ V,\varnothing\}\)图中每个顶点自成一个连通分量。当在\(E\)中选到一条具有最小权值的边时,若该边的两个顶点落在不同的连通分量上,则将此边加入到\(T\)中;否则将此边舍去,重新选择一条权值最小的边。如此重复下去,直到所有顶点在同一个连通分量上为止

  2. 示例image

  3. 算法

    /*
     * 存储结构:边的数组, n为结点数量
     * Kruskal 算法
     * 返回选择的边的数组
     */
    struct edge {
        int begin;     // begin of the edge
        int end;       // end of the edge
        int weight;    // weight of the edge
        edge(int begin, int end, int weight):begin(begin), end(end), weight(weight){}
    };
    /*并查集*/
    vector<int> father;
    int find(int node) {
        return (father[node] == node) ? node : find(father[node]);
    }
    void join(int a, int b) {
        father[find(a)] = find(b);
    }
    /*Kruskal*/
    vector<edge> Kruskal(vector<edge>& edges, const int n) {
        father.resize(n);
        vector<edge> MST;
        sort(edges.begin(), edges.end(), [](const edge& a, const edge& b) {
            return a.weight < b.weight;
        });     // 将边按权值从小到大排序
        for (int i = 0; i < n; ++i) {
            father[i] = i;  //初始化各节点的根为自己
        }
        for (int i = 0; i < edges.size() && MST.size() < n - 1; ++i) {
            int u = edges[i].begin, v = edges[i].end;
            if(find(u) != find(v)) {
                MST.push_back(edges[i]);
                join(u, v);
            }
        }
        return MST;
    }
    

5. 拓扑排序

1. \(AOV\)

  1. \(AOV\)网:用顶点表示活动,用有向边\(<vi, vj>\)表示活动间的优先关系。\(vi\) 必须先于活动\(vj\) 进行,这种有向图叫做顶点表示活动的AOV网络\((Activity On Vertices)\)
  2. 如果AOV网络中存在有向环,此\(AOV\)网络所代表的工程是不可行的
  3. 拓扑序列: 即将各个顶点 (代表各个活动)排列成一个线性有序的序列,使得所有弧尾结点都排在弧头结点的前面
  4. 拓扑排序:构造\(AOV\)网络全部顶点的拓扑有序序列的运算就叫做拓扑排序

2. 拓扑排序

  1. 基本思想:在\(AOV\)网络中选一个没有直接前驱的顶点,从图中删去该顶点, 同时删去所有它发出的有向边,重复以上步骤,全部顶点均已输出,拓扑有序序列形成,拓扑排序完成;图中还有未输出的顶点,但已跳出处理循环。说明图中还剩下一些顶点, 它们都有直接前驱。这时网络中必存在有向环

  2. 示例:image

  3. 算法

    /*
     * 存储结构:g[i][j]表示第i个顶点指向g[i][j]
     * 拓扑排序 算法
     * 返回拓扑序列
     */
    vector<int> top_sort(vector<vector<int>>& g) {
        int n = g.size();
        vector<int> indeg(n, 0);  //存储顶点的入度
        vector<int> ans;
        for (int i = 0; i < n; ++i) {
            for(int& node: g[i]) {
                ++indeg[node];
            }
        }
        queue<int> q;
        for(int i = 0; i < n; ++i) {
            if(indeg[i] == 0) {
                q.push(i);      // 选择没有直接前驱的顶点
            }
        }
        while(!q.empty()) {
            int node = q.front();
            ans.push_back(node);
            q.pop();
            for (int& i: g[node]) {
                --indeg[i];     // 去除边
                if(indeg[i] == 0) {
                    q.push(i);
                }
            }
        }
        if(ans.size() != n) cout << "exist ring, can not sort" << endl;
        return ans.size() == n ? ans : vector<int>();
    }
    

6. \(AOE\)

1. \(AOE\)

  1. \(AOE\)网:如果在无有向环的带权有向图中,用有向边表示一个工程中的各项活动,用边上的权值表示活动的持续时间,用顶点表示事件,则这样的有向图叫做用边表示活动的网络,被称为\(AOE\)\((Activity On Edges)\)网络
  2. 意义
    1. 完成整个工程至少需要多少时间(假设网络中没有环)
    2. 为缩短完成工程所需的时间, 应当加快哪些活动
  3. 源点与汇点:在\(AOE\)网络中, 有些活动顺序进行,有些活动并行进行,入度为零的点叫源点,出度为零的点叫汇点
  4. 关键路径:完成整个工程所需的时间取决于从源点到汇点的最长路径长度,即在这条路径上所有活动的持续时间之和,这条路径长度最长的路径就叫做关键路径
  5. 关键活动:关键路径上的活动为 关键活动

2. 关键路径求解算法

  1. 事件最早可能开始的时间ve[i]

    1. 概念:从源点\(v_0\)到顶点\(v_i\)的最长路径长度
    2. 算法:从ve[0] = 0开始往前递推,ve[i] = max{ve[j] + time<vj, vi>}image
  2. 事件最迟允许发生的时间vl[i]

    1. 概念:在保证汇点\(v_{n-1}\)ve[n-1]时刻完成的前提下,事件\(v_i\)的允许的最迟开始时间
    2. 算法:从vl[n-1]=ve[n-1]开始开始反推,vl[i] = min{vl[j] - time<vi, vj>}
  3. 活动开始的最早时间e[k]

    1. 概念:设活动ak在带权有向边<vi, vj>上,其持续时间为time<vi, vj>,其最早发生时间e[k]=ve[i]
  4. 活动最迟允许开始时间l[k]

    1. 概念:l[k]是在不会引起时间延误的前提下, 该活动允许的最迟开始时间,l[k] = vl[j] - time<i, j>
  5. 时间余量l[k]-e[k]:表示活动 ak 的最早可能开始时间和最迟允许开始时间的时间余量。l[k] == e[k] 表示活动 ak 是没有时间余量的关键活动

  6. 示例image

    vertex 1 2 3 4 5 6 7 8
    ve 0 8 12 22 28 40 46 58
    vl 0 8 12 22 28 40 46 58

    由公式计算出:

    edge(activity) 1 2 3 4 5 6 7 8 9
    e 0 8 12 12 22 22 28 40 46
    l 0 8 12 12 22 32 28 40 46

    因此除了a6不是关键活动,其他都是,因此得出关键路径

  7. 算法

    // time[i][j]表示从i到j的边的权值,如果没有边相连,值为 0
    // 以v0作为源点,vn作为汇点,且无有向环
    struct {
        int begin;  // begin of the edge
        int end;    // end of the edge
        int weight; // weight of the edge
        edge(int begin, int end, int weight):begin(begin), end(end), weight(weight){}
    };
    vector<edge> Critical_Path(vector<vector<int>>& time) {
        int n = time.size();    //n -- vertex number; 
        vector<int> ve(n), vl(n);
        vector<edge> edges;
        vector<edge> critical_path;
        ve[0] = 0;
        for (int i = 1; i < n; ++i) {
            int minVal = 0;
            for (int j = 0; j < n; ++j) {
                if (time[i][j] != 0) {
                    edges.push_back(edge(i, j, time[i][j]));
                    minVal = min(minVal, time[i][j]);
                }
            }
            ve[i] = minVal;
        }
        vl[n - 1] = ve[n - 1];
        for (int i = n - 2; i >= 0; --i) {
            int maxVal = INT_MAX;
            for (int j = 0; j < n; ++j) {
                if(time[i][j] != 0) {
                    maxVal = max(maxVal, time[i][j]);
                }
            }
            vl[i] = maxVal;
        }
        vector<int> e(m), l(m);
        for (int i = 0; i < edges.size(); ++i) {
            e[i] = ve[edges[i].begin];
            l[i] = vl[edges[i].end] - edges[i].weight;
            if(e[i] == l[i]) {
                critical_path.push_back(edges[i]);
            }
        }
        return critical_path;
    }
    

7. 最短路径

1. 最短路径问题

  1. 最短路径:如果图是一个带权图,则路径长度为路径上各边的权值之和,两个顶点之间的路径长度最短的路径为两个点之间的最短路径,其长度是 最短路径长度
  2. 算法
    1. \(Dijkstra\)算法:边上权值非负情形的单源最短路径问题
    2. \(Floyd\)算法:所有顶点之间的最短路径

2. \(Dijkstra\)算法

  1. 基本思想:按路径长度的递增次序, 逐步产生最短路径的算法。首先求出长度最短的一条最短路径,再参照它求出长度次短的一条最短路径,依次类推,直到从顶点v到其它各顶点的最短路径全部求出为止

  2. 示例:计算从v4顶点到各顶点的最短路径(pre表示连接的前驱点,dist表示当前的最短路径长度)image

    v1 v2 v3 v4 v5 v6
    dist \(\infty\) 20 \(\infty\) 0 \(\infty\) 15
    pre v4 v4 v4 v4 v4 v4
    dist 30 20 \(\infty\) ✔️ \(\infty\) 15
    pre v2 v4 v4 ✔️ v4 v4
    dist 30 20 45 ✔️ 50 ✔️
    pre v2 v4 v1 ✔️ v2 ✔️
    dist 30 ✔️ 45 ✔️ 50 ✔️
    pre v2 ✔️ v1 ✔️ v2 ✔️
    dist ✔️ ✔️ 45 ✔️ 50 ✔️
    pre ✔️ ✔️ v1 ✔️ v2 ✔️
    dist ✔️ ✔️ ✔️ ✔️ 50 ✔️
    pre ✔️ ✔️ ✔️ ✔️ v2 ✔️
    dist ✔️ ✔️ ✔️ ✔️ ✔️ ✔️
    pre ✔️ ✔️ ✔️ ✔️ ✔️ ✔️

    因此从顶点v4出发到其余顶点的最短路径及长度

    vertex length path
    v1 30 v4->v2->v1
    v2 20 v4->v2
    v3 45 v4->v2->v1->v3
    v4 0 v4
    v5 50 v4->v2->v5
    v6 15 v4->v6
  3. 算法

    1. 基于dist[]数组的\(Dijkstra\)算法(时间复杂度\(O(n^2)\),如果求每个点到各点的最短路径则\(O(n^3)\)
      /*
       * 从beginNode开始的到各顶点的最短路径
       * g[i][0:2]表示g[0]点到g[1]点的权g[2]
       * n 为顶点数量
       */
      vector<int> Dijkstra(vector<vector<int>>& g, int n, int beginNode) {
          int inf = 1e9;
          vector<vector<int>> adj(n, vector<int>(n, inf));
          for(vector<int>& edge: g) {
              adj[edge[0]][edge[1]] = edge[2];
          }
          vector<bool> visited(n, false);
          vector<int> dist(n, inf);
          dist[beginNode] = 0;
          for(int i = 0; i < n; ++i) {
              int x = -1;
              for(int j = 0; j < n; ++j) {
                  if(!visited[j] && (x == -1 || dist[j] < dist[x])) {
                      x = j;
                  }
              }
              visited[x] = true;
              for(int j = 0; j < n; ++j) {
                  dist[j] = min(dist[j], dist[x] + adj[x][j]);
              }
          }
          return dist;
      }
      
    2. 使用一个小根堆来寻找未确定节点中与起点距离最近的点的\(Dijkstra\)算法
      /*
       * 从beginNode开始的到各顶点的最短路径
       * g[i][0:2]表示g[0]点到g[1]点的权g[2]
       * n 为顶点数量
       */
      vector<int> Dijkstra(vector<vector<int>>& g, int n, int beginNode) {
          int inf = 1e9;
          vector<vector<pair<int, int>>> adj(n);
          for (vector<int>& edge: g) {
              int x = edge[0], y = edge[1];
              adj[x].push_back({y, edge[2]});
          }
      
          vector<int> dist(n, inf);
          dist[beginNode] = 0;
          priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> q;
          q.push({0, beginNode});
          while (!q.empty()) {
              auto p = q.top();
              q.pop();
              int val = p.first, x = p.second;
              if (dist[x] < val) {
                  continue;
              }
              for (auto &e : adj[x]) {
                  int y = e.first, d = dist[x] + e.second;
                  if (d < dist[y]) {
                      dist[y] = d;
                      q.push({d, y});
                  }
              }
          }
          return dist;
      }
      

3. \(Floyd\) 算法

  1. 基本思想:从初始的邻接矩阵\(A_0\)开始,递推地生成矩阵序列\(A_1,A_2,\dots,A_n\)
  2. 递推公式:\(A_0[i][j] = C[i][j]; A_{k+1}[i][j] = min(A_k[i][j], A_k[i][k]+A_k[k][j])\)
  3. 路径记录:显然,\(A\)中记录了所有顶点对之间的最短路径长度。若要求得到最短路径本身,还必须设置一个路径矩阵\(P[n][n]\),在第\(k\)次迭代中求得的path[i][j],是从ij的中间点序号不大于\(k\)的最短路径上顶点i的后继顶点。算法结束时,由path[i][j]的值就可以得到从ij的最短路径上的各个顶点
  4. 算法
    /*
     * g[i][0:2]表示g[0]点到g[1]点的权g[2]
     * n 为顶点数量
     */
    vector<vector<int>> Floyd(vector<vector<int>>& g, int n) {
        int inf = 1e9;
        vector<vector<int>> A(n, vector<int>(n, inf));
        for(vector<int>& edge: g) {
            A[edge[0]][edge[1]] = edge[2];
        }
        for(int i = 0; i < n; ++i) {
            A[i][i] = 0;
        }
        for(int k = 0; k < n; ++k) {
            for(int i = 0; i < n; ++i) {
                for(int j = 0; j < n; ++j) {
                    A[i][j] = min(A[i][j], A[i][k] + A[k][j]);
                }
            }
        }
        return A;
    }
    
posted @ 2023-01-27 22:14  RadiumStar  阅读(29)  评论(0编辑  收藏  举报