【数据结构与算法】图算法

一、基本图算法:

1.图的表示:

(1)邻接矩阵:

图的邻接矩阵是一种表示图(无向图或有向图)的方法。邻接矩阵是一个二维数组,其中行和列分别对应图中的顶点。如果两个顶点之间存在边,则在相应的位置标记为1(当为加权图时为边的权重),否则标记为0(或者为无穷大)。对于无向图而言它是对称的,有向图的邻接矩阵不一定是对称的。

下面给出简单无向图在C++的实现:

#include <iostream>
#include <vector>
using namespace std;

class Graph {
private:
    int numVertices;//用来记录当前图的节点数目
    vector<vector<int>> adjMatrix;//利用向量(变长数组)实现一个变长二维数组,这个操作定义了一个名为 adjMatrix 的变量,它是一个包含整数的二维向量。

public:
    Graph(int vertices) {//构造函数 Graph(int vertices) 初始化图对象。
        numVertices = vertices;//我们将节点数目传入到二维矩阵的“边长”之中
        adjMatrix.resize(vertices, vector<int>(vertices, 0));//adjMatrix 被调整为一个大小为 vertices x vertices 的矩阵,并初始化所有元素为0
    }

    void addEdge(int i, int j) {//addEdge(int i, int j) 方法用于在图中添加一条边
        if (i >= 0 && i < numVertices && j >= 0 && j < numVertices) {//首先检查顶点 i 和 j 是否在有效范围内
            adjMatrix[i][j] = 1;//如果有效,则在邻接矩阵中设置 adjMatrix[i][j] 和 adjMatrix[j][i] 为1,表示顶点 i 和 j 之间存在一条无向边。
            adjMatrix[j][i] = 1; 
        }
    }

    void printMatrix() {//printMatrix() 方法用于打印邻接矩阵。
        for (int i = 0; i < numVertices; i++) {
            for (int j = 0; j < numVertices; j++) {
                cout << adjMatrix[i][j] << " ";
            }
            cout << endl;
        }
    }
};

int main() {
    Graph g(4);
    g.addEdge(0, 1);
    g.addEdge(0, 3);
    g.addEdge(1, 2);
    g.printMatrix();
    return 0;
}

下面给出简单无向图在Python的实现:

class Graph:#class Graph: - 定义一个名为 Graph 的类
    def __init__(self, vertices):#定义类的构造函数(初始化方法),接受一个参数 vertices,表示图中顶点的数量。
        self.num_vertices = vertices#将传入的顶点数量赋值给实例变量 num_vertices。
        self.adj_matrix = [[0] * vertices for _ in range(vertices)]#创建一个大小为 vertices x vertices 的二维列表(矩阵),并初始化所有元素为 0。这个矩阵用于存储图的邻接矩阵。

    def add_edge(self, i, j):
        if 0 <= i < self.num_vertices and 0 <= j < self.num_vertices:
            self.adj_matrix[i][j] = 1
            self.adj_matrix[j][i] = 1  # For undirected graph

    def print_matrix(self):
        for row in self.adj_matrix:
            print(" ".join(map(str, row)))

if __name__ == "__main__":
    g = Graph(4)
    g.add_edge(0, 1)
    g.add_edge(0, 3)
    g.add_edge(1, 2)
    g.print_matrix()

(2)邻接矩阵: 

邻接链表是一种表示图(Graph)的方法,特别适用于存储和操作稀疏图。在邻接链表中,每个顶点都有一个链表,用于存储与该顶点相连的所有边。

下面给出C++的实现,我们仍然使用了vector。

#include <iostream>
#include <vector>

// 定义图的顶点数
const int N = 5;

// 创建邻接链表
std::vector<int> ADj[N];

// 添加边到邻接链表
void addEdge(int u, int v) {
    ADj[u].push_back(v);
    ADj[v].push_back(u); // 如果是无向图,需要添加双向边
}

// 打印邻接链表
void printGraph() {
    for (int i = 0; i < N; ++i) {
        std::cout << "Vertex " << i << ": ";
        for (int j : ADj[i]) {
            std::cout << j << " ";
        }
        std::cout << std::endl;
    }
}

int main() {
    // 添加一些边
    addEdge(0, 1);
    addEdge(0, 4);
    addEdge(1, 2);
    addEdge(1, 3);
    addEdge(1, 4);
    addEdge(2, 3);
    addEdge(3, 4);

    // 打印图的邻接链表表示
    printGraph();

    return 0;
}

 在Python之中的实现:

# 定义图的顶点数
N = 5

# 创建邻接链表
ADj = [[] for _ in range(N)]

# 添加边到邻接链表
def add_edge(u, v):
    ADj[u].append(v)
    ADj[v].append(u)  # 如果是无向图,需要添加双向边

# 打印邻接链表
def print_graph():
    for i in range(N):
        print(f"Vertex {i}: {' '.join(map(str, ADj[i]))}")

# 添加一些边
add_edge(0, 1)
add_edge(0, 4)
add_edge(1, 2)
add_edge(1, 3)
add_edge(1, 4)
add_edge(2, 3)
add_edge(3, 4)

# 打印图的邻接链表表示
print_graph()

2.广度优先搜索:

对于图的遍历的定义:对图的所有节点按照一定的顺序进行访问,遍历方法一般有两种:深度优先搜索以及广度优先搜索。下面介绍广度优先搜索(BFS)。

使用BFS遍历一个图的时候,我们需要使用一个队列,通过反复取出队首顶点,将该顶点可到达的未曾加入过队列的顶点全部入对,直到队列为空的时候遍历结束。

 

 下面是对于邻接链表的存储方式下基于C++实现的BFS

#include <iostream>
#include <vector>
#include <queue>
using namespace std;

const int MAXV = 1000; // 假设最大顶点数为1000,可以根据实际需求调整
vector<int> Adj[MAXV]; // 图G,Adj[u]存放从顶点u出发可以到达的所有的顶点。创建了一个数组:大小:MAXV,元素类型:vector<int>
int n; // n为顶点数目,MAXV为最大的顶点数。
bool inq[MAXV] = { false }; // 当节点i被访问过以后,inq[i]==true.

void BFS(int u) {
    queue<int> q; // 设计一个辅助队列p
    q.push(u);
    inq[u] = true; // 将当前节点标记为已访问
    while (!q.empty()) {
        int u = q.front(); // 取出队首元素
        q.pop(); // 将队首元素移除
        for (int i = 0; i < Adj[u].size(); i++) { // 枚举从u出发可以到达的所有的顶点
            int v = Adj[u][i];
            if (inq[v] == false) {//如果v没有加入过2队列
                q.push(v);//将v入队
                inq[v] = true;//标记v为已经加入过队列
            }
        }
    }
}

void BFSTrave() {//遍历图G
    for (int u = 0; u < n; u++) {//枚举所有的顶点
        if (inq[u] == false) {//如果u未曾加入过队列
            BFS(u); // 遍历u所在的连通块
        }
    }
}

 下面是对于邻接矩阵的存储方式下基于C++实现的BFS:

#include <iostream>
#include <vector>
#include <queue>
using namespace std;
const int MAXV = 1000; // 假设最大顶点数为1000,可以根据实际需求调整
int n, G[MAXV][MAXV];
bool inq[MAXV] = { false };
void BFS(int u) {//遍历u所在的连通块
    queue<int> q;//定义队列q
    q.push(u);//将初始点入队
    inq[u] = true;//设置该点被访问
    while (!q.empty()) {
        int u = q.front();//取出队首元素
        q.pop();//将队首元素出列
        for (int v = 0; v < n; v++) {//我们逐个访问每一个元素
            //如果与u相连并且没有被访问过
            if (inq[v] == false && G[u][v] != INF) {
                q.push(v);
                inq[v] = true;
            }

        }

    }
}
void BFSTrave() {
    for (int u; u < n; u++) {
        if (inq[u] == false) {
            BFS(u);
        }
    }
}

3.深度优先搜索:

这里介绍了采取辅助栈的方法:

深度优先搜索(DFS)是一种用于遍历或搜索图的算法。它从一个起始节点开始,沿着一个分支尽可能深入地搜索,直到不能再继续为止,然后回溯并探索其他分支。以下是使用DFS遍历图的方法:

  1. 创建一个栈来存储待访问的节点。
  2. 将起始节点压入栈中。
  3. 当栈不为空时,执行以下步骤: a. 从栈顶弹出一个节点。 b. 如果该节点未被访问过,则标记为已访问,并处理该节点(例如打印节点的值)。 c. 将与该节点相邻且未被访问的所有节点压入栈中。
  4. 重复步骤3,直到栈为空。

 下面是对于邻接链表的存储方式下基于C++实现的DFS

#include <iostream>
#include <vector>
#include <stack>
using namespace std;

const int MAXV = 1000; // 假设最大顶点数为1000,可以根据实际需求调整
vector<int> Adj[MAXV]; // 图G,Adj[u]存放从顶点u出发可以到达的所有的顶点。创建了一个数组:大小:MAXV,元素类型:vector<int>
int n; // n为顶点数目,MAXV为最大的顶点数。
bool inq[MAXV] = { false }; // 当节点i被访问过以后,inq[i]==true.

void DFS(int u) {
    stack<int> s; // 设计一个辅助栈
    s.push(u);
    while (!s.empty()) {
        int u = s.top(); // 取出栈顶元素
        s.pop();
        if (!inq[u]) {
            inq[u] = true; // 将当前节点标记为已访问
            cout << u << " "; // 处理节点,例如打印节点值
            for (int i = Adj[u].size() - 1; i >= 0; --i) { // 枚举从u出发可以到达的所有的顶点
                int v = Adj[u][i];
                if (!inq[v]) {
                    s.push(v); // 将未访问的相邻节点压入栈中
                }
            }
        }
    }
}

void DFSTrave() {
    for (int u = 0; u < n; u++) {
        if (!inq[u]) {
            DFS(u); // 传递顶点u
        }
    }
}

 下面是对于邻接矩阵的存储方式下基于C++实现的DFS

#include <iostream>
#include <vector>
#include <stack>
using namespace std;

const int MAXV = 1000; // 假设最大顶点数为1000,可以根据实际需求调整
int Adj[MAXV][MAXV] = {0}; // 图G,Adj[u][v]表示从顶点u到顶点v是否有边。创建了一个二维数组:大小:MAXV x MAXV,元素类型:int
int n; // n为顶点数目,MAXV为最大的顶点数。
bool inq[MAXV] = { false }; // 当节点i被访问过以后,inq[i]==true.

void DFS(int u) {
    stack<int> s; // 设计一个辅助栈
    s.push(u);
    while (!s.empty()) {
        int u = s.top(); // 取出栈顶元素
        s.pop();
        if (!inq[u]) {
            inq[u] = true; // 将当前节点标记为已访问
            cout << u << " "; // 处理节点,例如打印节点值
            for (int v = 0; v < n; ++v) { // 枚举从u出发可以到达的所有的顶点
                if (Adj[u][v] && !inq[v]) {
                    s.push(v); // 将未访问的相邻节点压入栈中
                }
            }
        }
    }
}

void DFSTrave() {
    for (int u = 0; u < n; u++) {
        if (!inq[u]) {
            DFS(u); // 传递顶点u
        }
    }
}

4.拓扑排序:

首先引入有向无环图的概念:如果一个有向图无法从某个顶点出发经过若干条边回到该点,则这个图是一个有向无环图(DAG图)。

接下来对于拓扑排序我们这样定义:将无环图G的所有顶点排成一个线性序列,使得对图之中的任意两个顶点u、v,如果存在边u->v,那么序列之中u一定在v的前面。该序列又被称之为拓扑序列。同时,如果两个顶点zl没有直接或者间接的连接,那么这两个结点的先后顺序是任意的。

最后介绍出度和入度的概念:在有向图中,出度是指从一个顶点指向其他顶点的边的数量,而入度则是指从其他顶点指向该顶点的边的数量。

算法流程:

①定义一个队列Q,随后将所有入度为0的结点加入到队列中。

②取对队首结点,输出。然后删除所有从他出发的边。并且令这些边到达的顶点的入度减去1,如果某一个顶点的入度被减到0,则将其加入到队列。

③反复进行②操作,直到队列为空。如果队列为空的时候入过队的结点数目为N,说明拓扑排序成功;否则图G为一个有环图。

算法应用:

一个最重要的应用就是判断一个给定的图是不是有向无环图。

5.强连通分量:

(1)连通分量:

在无向图之中,如果任意两个顶点之间可以以互相到达,那么我们称这两个顶点连通,。如果在图G之中的任意两个顶点都连通,则称图G为连通图;否则,称图为非连通图,并且我们称其极大连通子图为连通分量。

(2)强连通分量:

与连通分量的概念类似,只是基于有向图的基础上

无论是对于我们的连通分量和强连通分量而言,我们都可以想象到:如果要遍历一个图的时候,我们需要对所有的连通块进行遍历,这也是我们在DFS和BFS遍历的体现

二、最小生成树:

1.最小生成树的概念:

Minimum Spanning Tree(MST)就是在一个给定的无向图之中求出一棵树T,使得这一棵树T拥有图G的所有顶点,而且所有边都是来自图G的边,并且满足整棵树的边权之和最小。就是实现怎么花最小的成本连通所有的点。

2.prim算法:

算法步骤:

初始化:从任意一个顶点开始,将其加入生成树中,同时将与该顶点相连且权值最小的边加入生成树中。

选择最小边:在当前生成树外的所有顶点中,选择一个与生成树中顶点相连且权值最小的边。将该边加入生成树中,并将该边的另一个顶点也加入生成树中。

重复步骤:重复上述过程,直到所有顶点都被加入到生成树中为止。

算法的伪代码:

//G是图,数组d为顶点与集合s的最短距离
Prim(G,d[]){
   初始化;
   for(循环n次){
      u=使得d[u]最小的还没有被标记为已经访问过的标号;
      记录u已经被访问;
      for(从u出发可以到达的所有顶点v){
          if(v没有被访问&&以u为中介点使得v与集合S的最短距离d[v]更优秀){
             将G[u][v]赋值给v与集合S的最短距离d[v];
          }
      }
   }
}

基于邻接链表实现的C++代码:

#include <iostream>
#include <vector>
#include <queue>
using namespace std;

const int MAXV = 1000; // 假设最大顶点数为1000,可以根据实际需求调整
const int INF = 10000000;//假设INF为一个很大的数字
int n, G[MAXV][MAXV];//n为顶点数目,MAXV是最大顶点数目
int d[MAXV];//顶点与集合S的最短距离;
bool vis[MAXV] = { false };//标记数组,vis[i]==true表示已经访问过。初始值为false
int prim() {
	fill(d, d + MAXV, INF);//fill函数将数组d赋值为INF
	// 将数组 d 的所有元素都设置为 INF。注意,这里的 d + MAXV 表示指向数组末尾之后的位置,因此范围是从 d 到 d + MAXV(不包括 d + MAXV))
	d[0] = 0;
	int ans = 0;
	for (int i = 0; i < n; i++) {
		int u = -1, MIN = INF;//u使得d[u]最小,MIN应该存放最小的d[u]
		for (int j = 0; j < n; j++) {
			if (vis[j] == false && d[j] < MIN) {
				u = j;
				MIN = d[j];
			}
		}
		//找不到小于INF的d[u],则剩下的顶点和集合S不连通
		if (u == -1)return -1;
		vis[u] = true;
		ans += d[u];
		for (int v = 0; v < n; v++) {
			if (vis[v] == false && G[u][v] != INF && G[u][v] < d[v]) {
				d[v] = G[u][v];
			}
		}
	}
	return ans;
}

在初始化之中:

  • MAXV 定义了图中的最大顶点数。
  • INF 是一个非常大的值,用来表示两个顶点之间没有直接连接。
  • n 是图中顶点的数量。
  • G 是邻接矩阵,存储图中每条边的权重。如果两个顶点之间没有边,则对应的值为 INF
  • d 数组存储每个顶点到当前生成树的最短距离,初始值为INF。
  • vis 数组标记顶点是否已经被包含在生成树中。
  • fill(d, d + MAXV, INF); 将 d 数组的所有元素初始化为 INF,表示所有顶点到生成树的距离初始时都是无穷大。
  • d[0] = 0; 将第一个顶点(假设为顶点0)到生成树的距离设为0,因为这是开始点。

在主循环之中:

for (int i = 0; i < n; i++) 遍历所有顶点。目的是希望在每次循环选择一个未被访问且距离最小的顶点 。int u = -1, MIN = INF; 初始化 u 和 MIN,用于寻找当前未被访问的顶点中距离最小的那个。for (int j = 0; j < n; j++) 遍历所有顶点,找到未被访问且距离最小的顶点 u。如果找不到这样的顶点(即 u == -1),说明剩余的顶点和集合S不连通,返回 -1。将顶点 u 标记为已访问,并将其距离累加到 ans 中。更新所有与 u 相邻的顶点的距离。如果通过 u 到达某个顶点的距离更短,则更新该顶点的距离。

3.kruskal算法: 

克鲁斯卡尔算法与prim类似都是解决最小生成树的算法,采用了边贪心的策略,其思想极其简洁。

算法步骤:

①隐去图中的所有边,此时所有的顶点自成一个连通分量。

②对所有的边按照从小到大的顺序排序,如果当前测试边的两个顶点不属于同一个连通块之中,则把这条测试边加入到当前最小生成树之中;否则,舍弃该边。

③反复执行②,直到最小生成树的边数等于顶点数减去1,说明该图不连通。

伪代码的实现:

int kruskal(){
   令最小生成树的边权之和为ans、最小生成树的当前边数为NUM_Edge;
   将所有的边按照边权从小到大进行排序;
   for(从小到大枚举所有的边){
      if(当前测试边的两个端点在不同的连通块){
         将该测试边加入到生成树;
         ans+=测试边的边权;
         NUM_Edge ++;
         当边数NUM_Edge==顶点数目-1的时候结束循环;
      }
 }
return ans;
}

代码的实现关键在于:如何判断当前测试边的两个顶点是否在同一个连通块以及我们如何将测试边加入到最小生成树之中。------------------------采取并差集解决:我们将每个连通块视为一个集合,那么问题就变化为判断这两个顶点是否在同一个集合之中以及将这两个顶点所在的结合进行合并即可。

#include <iostream>
#include <vector>
#include <algorithm>//提供排序算法;
using namespace std;

const int N = 100;

struct Edge {//创建该结构体表示图之中的边
    int start;   // 起点
    int end;     // 终点
    int weight;  // 权值
    bool operator < (const Edge& edge) {//重载了<运算符,使得可以根据边的权值进行比较。
        return weight < edge.weight;
    }
};

vector<Edge> Init(int m) {
//Init函数用于初始化边集。首先创建一个大小为m的向量edges,然后从标准输入读取每条边的起点、终点和权值。最后返回这个边集
    vector<Edge> edges(m);//这里创建变长数组来存储数据类型为Edge
    cout << "请依次输入" << m << "条边的起点,终点和权值(用空格隔开):" << endl;//输入边的信息
    for (int i = 0; i < m; i++) {
        cin >> edges[i].start >> edges[i].end >> edges[i].weight;
    }
    return edges;
}

int find(vector<int>& fa, int x) {
    if (fa[x] != x) {
        fa[x] = find(fa, fa[x]); // Path compression
    }
    return fa[x];
}
void unionSets(vector<int>& fa, int x, int y) {
    int rootX = find(fa, x);
    int rootY = find(fa, y);
    if (rootX != rootY) {
        fa[rootY] = rootX;
    }
}
int kruskal(int n, int m) {
    vector<Edge> edges = Init(m);
    sort(edges.begin(), edges.end());
    vector<int> fa(N + 1);
    for (int i = 1; i <= n; i++) fa[i] = i;
    int num = 0, ans = 0;
    for (const auto& edge : edges) {
        if (num == n - 1) break;
        int v1 = find(fa, edge.start);
        int v2 = find(fa, edge.end);
        if (v1 != v2) {
            ans += edge.weight;
            unionSets(fa, v1, v2);
            num++;
        }
    }
    return ans;
}

三、单源最短路径:

1.Dijkstra算法:

采取迪杰斯特拉算法解决单源最短路径的问题:

设置集合S存放已经被访问的顶点,随后执行n次下面的两个步骤(n为顶点数目)。

①每次从集合V-S之中选择一个与起点距离最短的一个顶点(记录为u),访问并且加入到集合S。

②随后,令顶点u为中介点,优化起点与所有从u可以到达的顶点v的最短距离。

当然这个算法我们可以画一个表格轻轻松松解决,对于落实到代码层还是比较抽象困难的。我们对于集合S可以采用一个bool类型的数组vis[ ]来实现,即当vis[i]==ture时表示顶点Vi已经被访问,当vis[i]==false时表示顶点Vi未被访问。

令int类型的数组d[ ]表示起点s到达顶点Vi的最短距离,初始除了起点s的d[s]被赋值为0,其余均为无穷大。表示不可达。

伪代码:

//G为图,一般设计为全局变量;数组d为源点到达各个点的最短路径长度,s为起点。
Dijkstra(G,d[],s){
     初始化;
     for(循环n次){
       u=使得d[u]取得最小值还没被访问过的顶点的标号;
       记录u以及被访问;
       for(从u出发可以到达的所有顶点v){
           if(v未被访问&&以u为中介点使得s到达顶点v的最短距离更加优秀){
               优化更新d[v];
           }
      }
   }
}

邻接矩阵的C++实现:

#include<iostream>
using namespace std;
const int MAXV = 1000;//最大的顶点数目
const int INF = 1000000000;//设一个很大的数字
int n, G[MAXV][MAXV];//n为顶点的数目,MAXV为最大的顶点数目
int d[MAXV];//起点到达各点的最短路径长度
bool vis[MAXV] = {false};//标记数组,vis[i]==true表示已经被访问。
void Dijkstra(int s) {//以s作为起点
	fill(d, d + MAXV, INF);
	d[s] = 0;
	for (int i = 0; i < n; i++) {
		int u = -1, MIN = INF;
		for (int j = 0; j < n; j++) {
			if (vis[j] == false && d[j] < MIN) {
				u = j;
				MIN = d[j];
			}
		}
		if (u == -1)return;
		vis[u] = true;
		for (int v = 0; v < n; v++) {
			if (vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]) {
				d[v] = d[u] + G[u][v];
			}
		}
	}
}

初始化:

  • MAXV 定义了图中最多可以有的顶点数。
  • INF 是一个非常大的数,用来表示两个顶点之间没有直接连接(即不可达)。
  • n 是实际顶点的数量。
  • G 是邻接矩阵,G[u][v] 表示从顶点 u 到顶点 v 的边的权重。如果 G[u][v] == INF,则表示 u 和 v 之间没有边。
  • d 数组用于存储从起点到每个顶点的最短路径长度。
  • vis 数组用于标记某个顶点是否已被访问过。

迪杰斯特拉:

  • fill(d, d + MAXV, INF) 将 d 数组的所有元素初始化为 INF,表示初始时所有顶点的距离都是无穷大。
  • d[s] = 0 设置起点 s 到自身的距离为0。
  • 外层循环执行 n 次,每次找到一个当前未被访问且距离起点最近的顶点 u
  • 内层循环遍历所有顶点,寻找未被访问且距离起点最近的顶点 u
  • 如果找不到这样的顶点(即 u == -1),说明剩下的顶点都不可达,直接返回。
  • 将找到的顶点 u 标记为已访问。
  • 再次遍历所有顶点,更新与顶点 u 相邻的顶点 v 的距离。
  • 如果顶点 v 未被访问,并且存在从 u 到 v 的边(即 G[u][v] != INF),且通过 u 到达 v 的距离小于当前已知的最短距离,则更新 d[v]

 邻接链表的C++实现:

#include<iostream>
#include<vector>
using namespace std;
const int MAXV = 1000;//最大的顶点数目
const int INF = 1000000000;//设一个很大的数字
struct Node {
	int v, dis;
};
vector<Node>Adj[MAXV];//图G,Adj[u]存放从顶点u可以到达的所有顶点。
int n;//n为顶点数目,
int d[MAXV];
bool vis[MAXV] = { false };
void Dijkstra(int s) {
	fill(d, d + MAXV, MAXV);
	d[s] = 0;
	for (int i = 0; i < n; i++) {
		int u = -1, MIN = INF;
		for (int j = 0; j < n; j++) {
			if (vis[j] == false && d[j] < MIN) {
				u = j;
				MIN = d[j];
			}
		}
		if (u == -1)return;
		vis[u] = true;
		//相较于邻接矩阵只在下面这一部分进行更改:
		for (int j = 0; j < Adj[u].size(); j++) {
			int v = Adj[u][v].v;
			if (vis[v] == false && d[u] + Adj[u][v].dis < d[v]) {
				//如果v没有访问&&以u为中介点可以使得d[v]更优;
				d[v] = d[u] + Adj[u][j].dis;//优化d[v]
			}
		}
	}
}

2.Bellman-Ford算法:

迪杰斯特拉可以很好的解决无负权图的最短路径算法,但是无法处理含有负权的图。为了解决单源最短路径的问题我们介绍Bellman-Ford算法。下面我们介绍环的概念:即从某一个顶点出发、经过若干个不同的顶点之后可以再次到达该顶点的情况。我们将边权之和相加后,可以将环分为零环、正环、负环。存在负环时,从源点到某些节点的最短路径长度是没有意义的,因为可以通过负环使得路径长度无限减小。所以,此时返回的最短路径结果是不可靠的。返回 false 是一种约定俗成的方式,用于告知调用者该图中存在负环,当前计算的最短路径结果是无效的。这样,使用 Bellman - Ford 算法的程序可以根据这个返回值进行相应的错误处理,比如提示用户输入的图不符合要求,或者采取其他的策略来处理这个包含负环的图。

在实际应用中,如何避免负环对最短路径计算的影响?

与Dijkstra算法相同,Bellman-Ford算法也设计了一个数组d,用来存放从源点到达各个顶点的最短距离。同时Bellman-Ford算法会1返回一个bool值:如果其中存在从源点可达的负环,那么,函数返回false,否则函数返回true,此时数组d的存放的值就是从源点到达各个顶点的最短距离。

伪代码:

for(i=0;i<n-1;i++){//我们执行n-1次操作。其中n为顶点数目
  for(each edge u->v){//每轮操作都遍历所有的边
      if(d[u]+length[u->v]<d[v]){//以u为中介点可以使得d[v]更小
          d[v]=d[u]+length[u->v];//松弛操作;
      }
    }
}
for(each edge u->v){
   if(d[u]+length[u->v]<d[v]){如果环可以被松弛;
     return false;//说明图中有源点可以到达的负环
   }
}
  return true;//数组d之中的所有值到达最优;

 由于Bellman-Ford算法需要遍历所有的边,显然使用邻接表会更加的合适;使用邻接矩阵的时的时间复杂度达到三次。这里我们在多说一句松弛操作的含义:在Bellman-Ford算法中,松弛操作(Relaxation)是图算法中的一种基本操作,特别是在最短路径算法如Bellman-Ford算法中使用。它的目的是通过检查和更新节点之间的路径长度来逐步逼近最优解。松弛操作用于更新从源点到某个顶点的最短路径估计值。如果通过一个中间节点可以找到一个更短的路径,那么这个路径估计值就会被更新。

#include<iostream>
#include<vector>
using namespace std;
const int MAXV = 1000;//最大的顶点数目
const int INF = 1000000000;//设一个很大的数字
struct Node {
	int v, dis;
};
vector<Node>Adj[MAXV];//图G,Adj[u]存放从顶点u可以到达的所有顶点。
int n;//n为顶点数目,
int d[MAXV];//起点到达各个点的最短路径长度。
bool Bellman(int s) {
	fill(d, d + MAXV, INF);
	d[s] = 0;
	for (int i = 0; i < n - 1; i++) {
		for (int u = 0; u < n; u++) {
			for (int j = 0; j < Adj[u].size(); j++) {
				int v = Adj[u][j].v;
				int dis = Adj[u][j].dis;
				if (d[u] + dis < d[v]) {
					d[v] = d[u] + dis;
				}
			}
		}
	}
	for (int u = 0; u < n; u++) {
		for (int j = 0; j < Adj[u].size(); j++) {
			int v = Adj[u][j].v;
			int dis = Adj[u][v].dis;
			if (d[u] + dis < d[v]) {
				return false;
			}
		}
	}
	return true;
}

四、所有节点对最短路径:

1.Floyd-Warshall算法:

解决全源最短路径算法,即给定的图G(V,E),求任意两个点u,v之间的最短路径长度,时间复杂度为三次方,所以适用于采取邻接矩阵实现。需要两个二维数组来实现算法:①数组二维数组D用来存放任意两个点之间的最短距离(我们可以发现该数组的初值为邻接矩阵)②Path数组用来保存任意两个点之间的最短路径(在对角线上我们选择使用-1来表示最短路径不存在,对于最短路径,我们一般以最短路径终点的前一个点表示。

算法步骤:

①初始化:我们将邻接矩阵的值服装到数组D之中。并且根据图的信息补全Path

②依次将每一个点作为“中间点”去进行更新。我们每次的更新只去更新中间点以外的行和列。其余的路径我们需要以“中间点”作为中间点参与路径的实现,随后我们还需要与原来的数组D对应的值进行比较,若小于就更新(松弛)。同时我们需要更新Path数组:由于在该阶段我们选取了M作为X到达Y的最短路径,所以我们此时有两段路径:X->M和M->Y;对于Path数组我们此时的(X,Y)位置的值,我们依据在上一级的Path数组的(M,Y)的值即可。

 

#include <iostream>
#include <vector>
#include <limits>

using namespace std;

const int INF = numeric_limits<int>::max(); // 定义无穷大

void floydWarshall(vector<vector<int>>& graph, vector<vector<int>>& path) {
    int n = graph.size();
    vector<vector<int>> dist = graph; // 初始化距离矩阵

    // 初始化路径矩阵
    path.resize(n, vector<int>(n, -1));
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            if (graph[i][j] != INF && i != j) {
                path[i][j] = i;
            }
        }
    }

    // Floyd-Warshall算法核心部分
    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] != INF && dist[k][j] != INF && dist[i][k] + dist[k][j] < dist[i][j]) {
                    dist[i][j] = dist[i][k] + dist[k][j];
                    path[i][j] = path[k][j];
                }
            }
        }
    }

    // 更新原图的距离矩阵
    graph = dist;
}

void printPath(const vector<vector<int>>& path, int u, int v) {
    if (path[u][v] == -1) {
        cout << "No path from " << u << " to " << v << endl;
        return;
    }
    vector<int> stack;
    while (v != u) {
        stack.push_back(v);
        v = path[u][v];
    }
    stack.push_back(u);
    reverse(stack.begin(), stack.end());
    for (int i = 0; i < stack.size(); ++i) {
        if (i > 0) cout << " -> ";
        cout << stack[i];
    }
    cout << endl;
}

int main() {
    int n, m;
    cout << "Enter number of vertices and edges: ";
    cin >> n >> m;

    vector<vector<int>> graph(n, vector<int>(n, INF));
    vector<vector<int>> path;

    cout << "Enter the edges (u, v, weight):" << endl;
    for (int i = 0; i < m; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        graph[u][v] = w;
    }

    floydWarshall(graph, path);

    cout << "Shortest distance matrix:" << endl;
    for (const auto& row : graph) {
        for (int val : row) {
            if (val == INF) {
                cout << "INF ";
            } else {
                cout << val << " ";
            }
        }
        cout << endl;
    }

    cout << "Path matrix:" << endl;
    for (const auto& row : path) {
        for (int val : row) {
            cout << val << " ";
        }
        cout << endl;
    }

    int u, v;
    cout << "Enter source and destination to find path: ";
    cin >> u >> v;
    printPath(path, u, v);

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值