08 图 | 数据结构与算法

1. 图的基本概念

1. 图的定义

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

  2. 图的分类

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

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

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

    1. 无向图的路径:在无向图G=(V,E)中,如果存在顶点序列vp,vi1,vi2,,vim,vq,使得(vp,vi1),(vi1,vi2),,(vim,vq)E,则称该序列为从vpvq的一条 路径
    2. 有向图的路径:在有向图G=(V,E)中,如果存在顶点序列vp,vi1,vi2,,vim,vq,使得<vp,vi1>,<vi1,vi2>,,<vim,vq>∈E,则称该序列为从vpvq的一条 有向路径
    3. 路径的 长度:路径上边的数量
    4. 带权图的路径长度:路径上各边的权值之和
    5. 简单路径:如果路径上的各顶点v1,v2,,vm互不相同,则称这样的路径为 简单路径
  6. 子图:如果图G=(V,E),G=(V,E),其中ViV,EE,那么GG子图

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

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

    1. 顶点的强连通性:在有向图中, 若对于每一对顶点vivj(ij),都存在一条从vivj和从vivj的有向路径,则称顶点vivj强连通的
    2. 强连通图:如果一个有向图中任意一对顶点都是强连通的, 则称此有向图是 强连通图
    3. 强连通分量:非强连通图的极大强连通子图叫做 强连通分量
  9. 生成树:假设一个连通图有 n个顶点和 e 条边,其中 n1 条边和 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={1,<vi,vj>∈E or (vi,vj)E0,

      2. 示例

      3. 特点

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

        1. 定义

          A={Wi,j,<vi,vj>∈E or (vi,vj)E,

        2. 示例
  2. 邻接表

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

    2. 边(弧)的结点结构

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

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

      1. 邻接表 (出边表)
      2. 逆邻接表 (入边表)
    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. 示例:

  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(n2)

3. 广度优先搜索BFS

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

  2. 示例:

  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(n2)

3. 并查集

1. 并查集的定义

  1. 并查集:对于一个集合S={a1,a2,,an1,an},我们还可以对集合S进一步划分: S1,S2,,Sm并查集 希望能够快速确定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. 必须使用且仅使用 n1 条边来联结网络中的 n个顶点
    3. 不能使用产生回路的边

2. Prim算法

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

  2. 示例:从v2开始执行Prim算法

  3. 注意

    1. 若候选轻权边集中的轻权边不止一条,可任选其中的一条扩充到生成树中
    2. 连通图的最小生成树不一定是唯一的,但它们的权相等
    3. 设连通网络有 n 个顶点, 则该算法的时间复杂度为 O(n2),它适用于边稠密的网络
  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,}图中每个顶点自成一个连通分量。当在E中选到一条具有最小权值的边时,若该边的两个顶点落在不同的连通分量上,则将此边加入到T中;否则将此边舍去,重新选择一条权值最小的边。如此重复下去,直到所有顶点在同一个连通分量上为止

  2. 示例

  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网络(ActivityOnVertices)
  2. 如果AOV网络中存在有向环,此AOV网络所代表的工程是不可行的
  3. 拓扑序列: 即将各个顶点 (代表各个活动)排列成一个线性有序的序列,使得所有弧尾结点都排在弧头结点的前面
  4. 拓扑排序:构造AOV网络全部顶点的拓扑有序序列的运算就叫做拓扑排序

2. 拓扑排序

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

  2. 示例:

  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(ActivityOnEdges)网络
  2. 意义
    1. 完成整个工程至少需要多少时间(假设网络中没有环)
    2. 为缩短完成工程所需的时间, 应当加快哪些活动
  3. 源点与汇点:在AOE网络中, 有些活动顺序进行,有些活动并行进行,入度为零的点叫源点,出度为零的点叫汇点
  4. 关键路径:完成整个工程所需的时间取决于从源点到汇点的最长路径长度,即在这条路径上所有活动的持续时间之和,这条路径长度最长的路径就叫做关键路径
  5. 关键活动:关键路径上的活动为 关键活动

2. 关键路径求解算法

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

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

    1. 概念:在保证汇点vn1ve[n-1]时刻完成的前提下,事件vi的允许的最迟开始时间
    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. 示例

    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表示当前的最短路径长度)

    v1 v2 v3 v4 v5 v6
    dist 20 0 15
    pre v4 v4 v4 v4 v4 v4
    dist 30 20 ✔️ 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(n2),如果求每个点到各点的最短路径则O(n3)
      /* * 从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. 基本思想:从初始的邻接矩阵A0开始,递推地生成矩阵序列A1,A2,,An
  2. 递推公式:A0[i][j]=C[i][j];Ak+1[i][j]=min(Ak[i][j],Ak[i][k]+Ak[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; }

__EOF__

本文作者RadiumGalaxy
本文链接https://www.cnblogs.com/RadiumGalaxy/p/17069446.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   RadiumStar  阅读(65)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示