数据结构与基础算法——图(一篇讲透)

基本概念

图(Graph)是一种非线性数据结构,由顶点(Vertex)边(Edge)组成。顶点表示实体,边表示实体之间的关系。

图的分类

按方向性分类
  • 无向图(Undirected Graph):边没有方向,表示双向关系。例如社交网络中的好友关系。
  • 有向图(Directed Graph/Digraph):边有方向,表示单向关系。例如网页链接的指向关系。
按权重分类
  • 无权图(Unweighted Graph):边没有权重,仅表示连接关系。
  • 加权图(Weighted Graph):边带有权重,表示关系的强度或成本。例如交通网络中的距离或时间。

图的实现

1.邻接矩阵(稠密图)

邻接矩阵适合稠密图(边数接近顶点数的平方)或需要频繁查询边是否存在的情况。

是一个 n×n 的方阵(n 为顶点数),记为 A,其中:

  • 行和列分别对应图中的顶点;
  • 元素 A[i][j] 表示「顶点 i 到顶点 j」的连接关系
    • 无向图 / 有向图(无权):A[i][j] = 1 表示有边,A[i][j] = 0 表示无边;
    • 带权图:A[i][j] = 权重值(如距离、成本),无边时记为 0 或 (无穷大,表示不可达);
int graph[MAXN][MAXN];

//以无向图为例
int main() {
    int n, m;
    cin >> n >> m;
    
    //初始化邻接矩阵
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            //自己到自己为0,其它初始化为无穷大
            if (i == j) grpah[i][j] = 0;
            else graph[i][j] = INT_MAX;
        }
    }

    for (int i = 0; i < m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        
        //无向图
        graph[u][v] = min(graph[u][v], w);
        graph[v][u] = min(graph[v][u], w);
    }
}

2.邻接表(稀疏图)

邻接表适合稀疏图(边数远小于顶点数的平方)或需要高效遍历邻接节点的场景。

是一个数组链表(数组+链表/动态数组),其中:

  • 用一个「数组」存储所有顶点(数组下标对应顶点编号,数组元素是链表头);
  • 每个顶点对应的「链表」,存储该顶点的「相邻顶点」(无向图是直接相邻,有向图是出边指向的顶点,带权图还需存储权重)。
//graph[u]储存所有结点 u 可以到达的结点
vector<vector<int>> graph(MAXN);

//以无向图为例
int main() {
    int n, m;
    cin >> n >> m;

    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;

        graph[u].push_back(v);
        graph[v].push_back(u);
    }
}

//也可以储存有权图,但不太建议,空间复杂度太高
struct node {
    int v, w;
};

vector<vector<node>> graph(MAXN);

3.链式前向星(对有权图非常好用)

模拟链表,采用头插法,以边为单位,记录每一条边的目标点,其中:

  • edge数组存放所有边的数据
  • head数组中,索引表示结点,索引上的数据表示以索引结点为起点的边(在edge数组中的下标)
struct Edge {
	int to; //第i条边的终点
	int w; //第i条边的权值
	int next; //与第i条边同起点的下一条边的位置
} edge[MAXM]; //边数组

//头数组(结点数组), 最近一次输入的以i为起点的边在edge数组中的下标
vector<int> head(MAXN, -1);

void addEdge(int u, int v, int w) {
	edge[cnt].to = v; //cnt为edge索引
	edge[cnt].w = w;
	
	//头插法
	edge[cnt].next = head[u];
	head[u] = cnt++; //更新头节点
}

void print(vector<int>& head, int n) {
	//n个结点
	for (int i = 1; i <= n; i++) {
		cout << i << endl;
		for (int j = head[i]; j != -1; j = edge[j].next) {
			cout << i <<  " " << edge[j].to << " " << edge[j].w << endl;
		}
		cout << endl;
	}
}

图的算法

图的遍历

1.广度优先遍历(BFS)

广度优先遍历是一种由近及远的遍历方式,从某个节点出发,始终优先访问距离最近的顶点,并一层层向外扩张。

基本模版

//用二维数组储存图结构
vector<vector<int>> graph(MAXN);

//以 u 为起始结点的广度优先遍历
void BFS(int u) {
	queue<int> q;
	//标记结点是否被访问
	vector<int> visited(MAXN, false);
	
	q.emplace(u);
	visited[u] = true;
	cout << u << " ";
	
	while (!q.empty()) {
		int cur = q.front();
		q.pop();
		
		//添加 cur 相连的结点
		for (int v : graph[cur]) {
			if (!visited[v]) {
				q.emplace(v);
				visited[v] = true;
				cout << v << " ";
			}
		}
	}
}

int main() {
	int n, m;
	cin >> n >> m;
	
	for (int i = 0; i < m; i++) {
		int u, v;
		cin >> u >> v;
		graph[u].push_back(v);
		//graph[v].push_back(u); 双向图
	}
	
	BFS(1);
}

例题P11046 [蓝桥杯 2024 省 Java B] 星际旅行 - 洛谷https://www.luogu.com.cn/problem/P11046该题需要注意数据的范围,结点数 < 查询的次数,而且结点个数的数量级在1e3,那么就可以使用离线方式处理,算出所有结点,建立查询表(counter),最后只需要查询就可以得出答案。

#include <bits/stdc++.h>
#define MAXN 1005
#define MAXM 5 * MAXN
using namespace std;

//邻接表储存图结构
vector<vector<int>> graph(MAXN);
//记录每个结点 i 传送 j 次可以到达的星球个数 counter[i][j]
vector<vector<int>> counter(MAXN, vector<int>(MAXM, 0));

//以 u 为起始点的广度优先搜索
void bfs(int u) {
    int cnt = 0;
    
    //记录 u 到结点 i 需要的次数 dist[i]
    vector<int> dist(MAXN, INT_MAX);
    dist[u] = 0;

    queue<int> q;
    q.emplace(u);

    counter[u][0] = 1;

    while (!q.empty()) {
        int cur = q.front();
        q.pop();

        for (int v : graph[cur]) {
            //如果 dist[v] 次数没有改变(即 INT_MAX),代表没有被访问
            if (dist[v] == INT_MAX) {
                //cur 到 v 进行一次传送
                dist[v] = dist[cur] + 1;

                //以 u 为起始点传送 dist[v] 次才能到达的星球个数
                counter[u][dist[v]]++;
                q.emplace(v);
            }
        }
    }
}

int main() {
    int n, m, Q;
    cin >> n >> m >> Q;
    
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;

        graph[u].push_back(v);
        graph[v].push_back(u);
    }

    for (int i = 1; i <= n; i++) {
        //对每个结点进行广度优先搜索
        bfs(i);

        for (int j = 1; j <= n; j++) {
            //传送 j 次包括比 j 更小的传送次数
            //原来的 counter[i][j] 表达的是恰好传送 j 次才能到达的星球个数
            //更新后的 counter[i][j] 表达的是传送 j 次共能到达的星球个数
            counter[i][j] += counter[i][j-1];
        }
    }

    double ans = 0.0;

    for (int i = 0; i < Q; i++) {
        int x, y;
        cin >> x >> y;

        //直接查询 counter
        ans += counter[x][y];
    }

    printf("%.2lf", ans / Q);
}
2.深度优先遍历(DFS)

深度优先遍历是一种优先走到底、无路可走再回头的遍历方式。

//用二维数组储存图结构
vector<vector<int>> graph(MAXN);
vector<bool> visited(MAXN, false);

//以 u 结点为起点的深度优先遍历
void DFS(int u) {
	if (visited[u]) return;
	
	visited[u] = true;
	cout << u << " ";
	
	for (int v : graph[u]) {
		DFS(v);
	}
}

int main() {
	int n, m;
	cin >> n >> m;
	
	for (int i = 0; i < m; i++) {
		int u, v;
		cin >> u >> v;
		graph[u].push_back(v);
		//graph[v].push_back(u); 双向图
	}
	
	DFS(1);
}

例题:

200. 岛屿数量https://leetcode.cn/problems/number-of-islands/description/

class Solution {
public:
    //岛屿链接的方向
    vector<vector<int>> directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};

    int numIslands(vector<vector<char>>& grid) {
        int m = grid.size(), n = grid[0].size();
        int cnt = 0;

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == '1') {
                    cnt++;
                    dfs(grid, i, j);
                }
            }
        }
        return cnt;
    }

    //以 i, j 为起点用 dfs 遍历岛屿
    void dfs(vector<vector<char>>& grid, int i, int j) {
        int m = grid.size(), n = grid[0].size();

        //如果是岛屿,赋值为 '0' (或其他的值也行),表示已访问过。
        if (grid[i][j] == '1') {
            grid[i][j] = '0';
        }
        else {
            return ;
        }

        for (vector<int> d : directions) {
            int x = i + d[0], y = j + d[1];
            
            if (x < 0 || y < 0 || x >= m || y >= n) {
                continue;
            }

            dfs(grid, x, y);
        }
    }
};

拓补排序

拓补排序是一个有向无环图的所有顶点得线性序列。

且序列必须满足下面两个条件:
1.每个顶点出现且只出现一次。
2.若存在一条从顶点A到顶点B的路径,那么在序列中顶点A出现在顶点B的前面。

简单来说就是每次选取入度最小的顶点加入线性序列中,再从图中删除该顶点及该顶点的边。

例题及模版

B3644 【模板】拓扑排序 / 家谱树https://www.luogu.com.cn/problem/B3644

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;

    //记录 i 的入度
    vector<int> head(n+1);

    //记录 i 的出边
    vector<vector<int>> graph(n+1);

    for (int i = 1; i <= n; i++) {
        int v;
        cin >> v;
        while (v != 0) {
            //i -> v
            graph[i].push_back(v);
            //v 的入度+1
            head[v]++;
            cin >> v;
        }
    }

    //每次找到入度最小的结点,再更新入度数组
    for (int i = 1; i <= n; i++) {
        int idx = min_element(head.begin()+1, head.end()) - head.begin();
        cout << idx << " ";

        for (int v : graph[idx]) {
            head[v]--;
        }
        
        //删除已选择的结点
        head[idx] = INT_MAX;
    }
}

最小生成树

最小生成树详细解释可以看图-最小生成树-Prim(普里姆)算法和Kruskal(克鲁斯卡尔)算法_哔哩哔哩_bilibili

最小生成树模版题
洛谷-P3366 【模板】最小生成树https://www.luogu.com.cn/problem/P3366

1.prim最小生成树(加点法)

不断选择距离已选择路径最小的节点,并将其加入到该路径中,最终得到的路径就是最小生成树。

(下面的代码为使用prim算法的解答,可以先自己写一遍)

#include <bits/stdc++.h>
#define MAXM 200005
using namespace std;

//基于链式前向星实现
struct Edge {
    int to, w, next;
} edge[MAXM<<1]; //无向图, 开两倍数组

void addEdge(int cnt, int u, int v, int w, vector<int>& head) {
    edge[cnt].to = v;
    edge[cnt].w = w;
    edge[cnt].next = head[u];
    head[u] = cnt;
}

//最小生成树
int prim(vector<int>& head, int n) {
    int ans = 0;
    int s = 1;
    vector<int> dist(n+1, INT_MAX); //点到生成树的距离
    vector<bool> visitedFlag(n+1, false);

    dist[s] = 0;
    visitedFlag[s] = true;
    for (int i = head[s]; i != -1; i = edge[i].next) {
        int v = edge[i].to;
        int w = edge[i].w;
        //更新剩余点到生成树的距离
        //同时可以处理重复边的输入, 选择最小的权值
        dist[v] = min(dist[v], w);
    }

    for (int i = 1; i < n; i++) {
        int w = INT_MAX, vet = 0;
        for (int j = 1; j < n+1; j++) {
	        //到最小生成树权值最小且未被访问
            if (dist[j] < w && !visitedFlag[j]) {
                w = dist[j];
                vet = j;
            }
        }
        
        //没有找到可以连接的节点, 表示未连通
        if (vet == 0) return INT_MAX;

        ans += w;
        dist[vet] = 0;
        visitedFlag[vet] = true;

        for (int j = head[vet]; j != -1; j = edge[j].next) {
            int v = edge[j].to;
            int w = edge[j].w;
            dist[v] = min(dist[v], w);
        }
        
    }
    return ans;
}

int main() {
    int n, m;
    cin >> n >> m;

    vector<int> head(n+1, -1);
    for (int i = 0; i < 2*m; i+=2) {
        int u, v, w;
        cin >> u >> v >> w;
        addEdge(i, u, v, w, head);
        addEdge(i+1, v, u, w, head);
    }

    int ans = prim(head, n);

    if (ans == INT_MAX) {
        cout << "orz" << endl;
    }
    else {
        cout << ans << endl;
    }
}
2.kruskal最小生成树(加边法)

先把边按照权值进行排序,用贪心的思想优先选取权值较小的边,并依次连接,若出现环则跳过此边(用并查集来判断是否存在环)继续搜,直到已经使用的边的数量比总点数少一即可。

并查集有关的知识可以去看我写的博客,也可以自己去搜,知道基本模版就行。

并查集——树-优快云博客

(下面的代码为使用kruskal算法的解答,建议自己先写一遍,比prim算法要简单一点)

#include <bits/stdc++.h>
#define MAXM 200005
#define MAXN 5000
using namespace std;

//基于并查集
struct Edge {
    int u, v, w;
} edge[MAXM];

int parent[MAXN];

//sort比较函数
bool cmp(Edge a, Edge b) {
    return a.w <  b.w;
}

//并查集查询
int find(int x) {
    if (parent[x] < 0) 
        return x;

    return parent[x] = find(parent[x]); //路径优化
}

//并查集合并
void merge(int x, int y) {
    int root1 = find(x);
    int root2 = find(y);

    if (root1 == root2)
        return ;
    
    //按秩优化(按结点数)
    if (abs(parent[root1]) < abs(parent[root2])) {
        parent[root2] += parent[root1];
        parent[root1] = root2;
    }
    else {
        parent[root1] += parent[root2];
        parent[root2] = root1;
    }
}

//kruskal最小生成树
int kruskal(int n, int m) {
    int ans = 0;
    int cnt = 1;

    for (int i = 0; i < m; i++) {
        Edge e = edge[i];
        int u = e.u, v = e.v, w = e.w;

        //判断是否为同一集合
        if (find(u) == find(v)) {
            continue;
        }
    
        //添加新边
        ans += w;
        cnt ++;
        merge(u, v);

        if (cnt == n) break;
    }

    //存在独立结点,无法形成通路
    if (cnt != n) {
        ans = INT_MAX;
    }

    return ans;
}

int main() {
    int n, m;
    cin >> n >> m;

    //初始化并查集
    for (int i = 1; i <= n; i++) {
        parent[i] = -1;
    }

    for (int i = 0; i < m; i++) {
        int u, v, w;
        cin >> edge[i].u >> edge[i].v >> edge[i].w;
    }

    //对edge数组按权值从小到大排序
    sort(edge, edge+m, cmp);

    int res = kruskal(n, m);

    if (res == INT_MAX) {
        cout << "orz" << endl;
    }
    else {
        cout << res << endl;
    }
}

最短路径

1.无权图最短路径(迷宫类型)

无权图路径题目适合用图的遍历,DFS或BFS,两者有所侧重,

DFS适合用于寻找一条路径,但不一定是最短路径。实现相对简单,但在复杂迷宫中可能会走很多冤枉路,时间复杂度较高。

DFS例题:

P2196 [NOIP 1996 提高组] 挖地雷https://www.luogu.com.cn/problem/P2196需要找到所有可能的路径,并实时更新最大的地雷数,更适合使用dfs去找每一个路径。

#include <bits/stdc++.h>
using namespace std;

//储存图结构
vector<vector<int>> graph(25);
//地雷数
vector<int> weight(25);
//判断是否访问
vector<bool> visited(25, false);
//最终的路径方案
vector<int> solution;
//最大地雷数
int ans = 0;

//基本上就是dfs模版
void dfs(int i, int cnt, vector<int> cur) {
    cnt += weight[i];
    visited[i] = true;
    cur.push_back(i);

    //更新地雷数以及路径
    if (cnt > ans) {
        ans = cnt;
        solution = cur;
    }

    for (int to : graph[i]) {
        if (visited[to]) continue;

        dfs(to, cnt, cur);
    }
}

int main() {
    int n;
    cin >> n;

    for (int i = 1; i <= n; i++) {
        cin >> weight[i];
    }


    for (int i = 1; i < n; i++) {
        for (int j = 1; j <= n-i; j++) {
            int tmp;
            cin >> tmp;

            //存在路径就添加到数组中
            if (tmp == 1) {
                graph[i].push_back(j+i);
            }
        }
    }

    for (int i = 1; i <= n; i++) {
        dfs(i, 0, {});
        //还原visited,重新设置为未访问
        visited.assign(visited.size(), false);
    }

    //输出答案
    for (int d : solution) {
        cout << d << " ";
    }
    cout << endl;
    cout << ans << endl;
}

BFS适合用于寻找最短路径,保证第一次找到的路径就是最短路径。实现相对复杂,需要更多的内存来储存结点。

BFS例题及模版:

P1135 奇怪的电梯https://www.luogu.com.cn/problem/P1135

#include <bits/stdc++.h>
using namespace std;

//bfs模版 s -> e
void bfs(vector<vector<int>>& graph, int s, int e) {
    int n = graph.size();

    //ans[i] 为 s 到 i 结点按按钮的次数
    vector<int> ans(n, -1);
    queue<int> q;

    ans[s] = 0;
    q.emplace(s);

    while (!q.empty()) {
        int cur = q.front();
        q.pop();

        if (cur == e) {
            break;
        }

        for (int v : graph[cur]) {
            //如果ans[v]为初始值时表示未访问
            if (ans[v] == -1) {
                ans[v] = ans[cur] + 1;
                q.emplace(v);
            }
        }
    }

    cout << ans[e] << endl;
}

int main() {
    int n, a, b;
    cin >> n >> a >> b;

    vector<vector<int>> graph(n+1);
    for (int i = 1; i <= n; i++) {
        int k;
        cin >> k;

        //第 i 层楼可以到达的楼层
        int up = i+k, down = i-k;
        if (up <= n) {
            graph[i].push_back(up);
        }
        if (down >= 1) {
            graph[i].push_back(down);
        }
    }

    bfs(graph, a, b);
}

二维BFS

基本思想是一样的,主要难点在于需要记录的数据较多,包括要激励前驱结点。

P10234 [yLCPC2024] B. 找机厅https://www.luogu.com.cn/problem/P10234

#include <bits/stdc++.h>
using namespace std;

//每次移动的方向
//D R U L
vector<vector<int>> directions = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
string methods = "DRUL";

//输出路径
void printRoute(vector<vector<int>>& route, int i, int j) {
    if (i == 0 && j == 0) return;

    //倒推找上一步所在的位置
    int d = route[i][j];
    if (d == 0) printRoute(route, i-1, j);
    else if (d == 1) printRoute(route, i, j-1);
    else if (d == 2) printRoute(route, i+1, j);
    else printRoute(route, i, j+1);

    //再从最开始依次输出
    cout << methods[d];
}

//广度优先遍历求最短路径
void bfs(vector<string>& graph) {
    int n = graph.size(), m = graph[0].size();

    //记录路径(route[i][j] 表示上一步的方向)
    vector<vector<int>> route(n, vector<int>(m, -1));
    //记录到达 i, j 所花的时间
    vector<vector<int>> ans(n, vector<int>(m, -1));

    //4 表示没有上一步,即为起点
    route[0][0] = 4;
    ans[0][0] = 0;

    queue<pair<int, int>> q;
    q.push({0, 0});

    while (!q.empty()) {
        pair<int, int> cur = q.front();
        int i = cur.first, j = cur.second;
        q.pop();

        if (i == n-1 && j == m-1) {
            break;
        }

        //顺序依次为D R U L
        for (int d = 0; d < 4; d++) {
            int x = i + directions[d][0], y = j + directions[d][1];

            //不能走的格子
            if (x < 0 || y < 0 || x >= n || y >= m || graph[x][y] == graph[i][j]) {
                continue;
            }

            //如果所花的时间为-1(初始值),则表示没有访问过
            if (ans[x][y] == -1) {
                ans[x][y] = ans[i][j] + 1;
                route[x][y] = d;
                q.push({x, y});
            }
        }
    }

    cout << ans[n-1][m-1] << endl;
    if (ans[n-1][m-1] != -1) {
        printRoute(route, n-1, m-1);
        cout << endl;
    }
}

int main() {
    int t;
    cin >> t;

    while (t--) {
        int n, m;
        cin >> n >> m;
        vector<string> graph(n);

        for (int i = 0; i < n; i++) {
            cin >> graph[i];
        }

        bfs(graph);
    }
}
2.有权图最路径
Dijkstra(迪杰斯特拉)

简单来说,Dijkstra就是每次选择距离起点最短的结点添加到已选择的结点集合中,并利用新增节点的边去更新剩余未访问结点的距离(只能计算边权值大于0的情况)。

与prim最小生成树相似,都是逐点添加,并更新路径,

但不一样的是,prim是选择距离生成树集合最短的结点添加,而Dijkstra是选择距离起点最短的结点添加

Dijkstra模版题

B3602 [图论与代数结构 202] 最短路问题_2https://www.luogu.com.cn/problem/B3602

下面先给出模版代码

看题目的数据量,由于边数较大,而且是有权图,使用链式前向星储存图结构。

#include <bits/stdc++.h>
#define MAXM (int)3e6
#define MAXN (int)3e6
using namespace std;
using ll = long long int;

struct Edge {
    int to, next;
    ll w;
} edge[MAXM];

//结点 i 到 起点的距离
ll dist[MAXN];

//头结点数组
int head[MAXN];

//标记是否访问过
bool visited[MAXN];

//添加边
void addEdge(int i, int u, int v, ll w) {
    edge[i].w = w;
    edge[i].to = v;
    edge[i].next = head[u];
    head[u] = i;
}

void Dijsktra(int begin, int n) {
    //将起点加入到点集中
    visited[begin] = true;
    dist[begin] = 0;

    //更新剩余的结点
    for (int i = head[begin]; i != -1; i = edge[i].next) {
        int v = edge[i].to;
        ll w = edge[i].w;

        dist[v] = min(dist[v], w);
    }

    for (int i = 2; i <= n; i++) {
        ll min_dist = LLONG_MAX;
        int min_u = -1;

        //在未被访问的结点中找距离起点最小的结点
        for (int u = 1; u <= n; u++) {
            if (!visited[u] && dist[u] < min_dist) {
                min_dist = dist[u];
                min_u = u;
            }
        }

        //没有找到,表示图中没有通路了,直接退出
        if (min_u == -1) break;

        //设置为被访问表示加入到点集中
        visited[min_u] = true;

        //更新剩余未被访问的结点
        for (int j = head[min_u]; j != -1; j = edge[j].next) {
            int v = edge[j].to;
            ll w = edge[j].w;
            
            if (!visited[v]) dist[v] = min(dist[v], w+dist[min_u]);
        }
    }
}

int main() {
    int n, m;
    cin >> n >> m;

    //初始化
    for (int i = 1; i <= n; i++) {
        //最开始都是无穷大,表示还没有路径
        dist[i] = LLONG_MAX;
        head[i] = -1;
        visited[i] = false;
    }

    //读取输入数据
    for (int i = 0; i < m; i++) {
        int u, v;
        ll w;
        cin >> u >> v >> w;

        addEdge(i, u, v, w);
    }

    //起点为结点 1
    int begin = 1;
    //Dijsktra(begin, n);
    DijsktraPlus(begin, n);

    for (int i = 1; i <= n; i++) {
        //表示没有找到路径
        if (dist[i] == LLONG_MAX) {
            dist[i] = -1;
        }
        cout << dist[i] << " ";
    }
}

但题目数据量大,这种基本写法只能部分AC

所以我们需要稍微优化一下时间复杂度,可以发现我们寻找最短的结点用了O(n^2),有没有时间复杂度更小的办法,那就是——priority_queue(优先队列),优化时间复杂度到O(nlogn)

AC代码——优化版Dijsktra:

#include <bits/stdc++.h>
#define MAXM (int)3e6
#define MAXN (int)3e6
using namespace std;
using ll = long long int;

struct Edge {
    int to, next;
    ll w;
} edge[MAXM];

//结点 i 到 起点的距离
ll dist[MAXN];

//头结点数组
int head[MAXN];

//标记是否访问过
bool visited[MAXN];

//添加边
void addEdge(int i, int u, int v, ll w) {
    edge[i].w = w;
    edge[i].to = v;
    edge[i].next = head[u];
    head[u] = i;
}

//自定义比较函数
struct compare {
    bool operator()(pair<int, ll> a, pair<int, ll> b) {
        return a.second > b.second; //以距离为依据的小顶堆
    }
};

//优化版 Dijsktra
void DijsktraPlus(int begin, int n) {
    dist[begin] = 0;

    priority_queue<pair<int, ll>, vector<pair<int, ll>>, compare> pq;
    //初始化优先队列
    pq.push({begin, dist[begin]});

    while (!pq.empty()) {
        //取出距离起点最短的结点
        pair<int, ll> cur = pq.top();
        pq.pop();

        int min_u = cur.first;
        ll min_dist = cur.second;

        //如果访问过了,则继续选取
        if (visited[min_u]) {
            continue;
        }
        //设置为被访问
        visited[min_u] = true;

        //更新剩余未被访问的结点
        for (int i = head[min_u]; i != -1; i = edge[i].next) {
            int v = edge[i].to;
            ll w = edge[i].w;

            if (!visited[v]) {
                //更新结点距离
                dist[v] = min(dist[v], w + min_dist);
                //加入到队列中
                pq.push({v, dist[v]});
            }
        }
    }
}

int main() {
    int n, m;
    cin >> n >> m;

    //初始化
    for (int i = 1; i <= n; i++) {
        //最开始都是无穷大,表示还没有路径
        dist[i] = LLONG_MAX;
        head[i] = -1;
        visited[i] = false;
    }

    //读取输入数据
    for (int i = 0; i < m; i++) {
        int u, v;
        ll w;
        cin >> u >> v >> w;

        addEdge(i, u, v, w);
    }

    //起点为结点 1
    int begin = 1;
    //Dijsktra(begin, n);
    DijsktraPlus(begin, n);

    for (int i = 1; i <= n; i++) {
        //表示没有找到路径
        if (dist[i] == LLONG_MAX) {
            dist[i] = -1;
        }
        cout << dist[i] << " ";
    }
}
Floyd(弗洛伊德)

简单来说就是遍历所有的结点,每次使用遍历到的结点(via)去更新所有结点(可以用于负权值)。

  1. 每轮第via行和第via列与主对角线是不变的。
  2. 对应行列值相加与现有比较,取最小(graph[i][j] = min(graph[i][j], graph[i][via] + graph[via][j]);)。

Floyd模版题

B3647 【模板】Floydhttps://www.luogu.com.cn/problem/B3647

#include <bits/stdc++.h>
#define MAXN 105
using namespace std;

//用邻接矩阵储存图结构
int graph[MAXN][MAXN];

void Floyd(int n) {
    //遍历每个结点
    for (int via = 1; via <= n; via++) {
        //遍历图
        for (int i = 1; i <= n; i++) {
            //第 i 行不变
            if (via == i) continue;

            for (int j = 1; j <= n; j++) {
                //第 j 列及主对角线不变
                if (via == j || i == j) continue;

                //取 i 通过 via 到 j 与现在 i 到 j 的最小值
                graph[i][j] = min(graph[i][via] + graph[via][j], graph[i][j]);
            }
        }
    }
}

int main() {
    int n, m;
    cin >> n >> m;

    //初始化图
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            if (i == j) {
                graph[i][j] = 0;
            }
            else {
                graph[i][j] = INT_MAX/2;
            }
        }
    }

    //输入图数据
    for (int i = 0; i < m; i++) {
        int u, v, w;
        cin >> u >> v >> w;

        graph[u][v] = min(graph[u][v], w);
        graph[v][u] = min(graph[v][u], w);
    }

    Floyd(n);

    //i 表示起点,graph[i][j] 表示 i 到 j 的最小路径
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            if (graph[i][j] == INT_MAX/2) {
                cout << -1 << " ";
            }
            else {
                cout << graph[i][j] << " ";
            }
        }
        cout << endl;
    }
}

最短路径与最小生成树的区别

最小生成树关注的是连接整个图的所有顶点,使得边的总权值最小,但它不保证任意两点之间的路径是最短的。常用算法有 Prim 和 Kruskal,适用于如网络布线、管道铺设等整体成本最小化的场景。

最短路径则是从一个顶点到另一个顶点,寻找路径权值之和最小的路线。它不要求覆盖所有顶点,常用算法有 Dijkstra(单源最短路径)、Floyd(多源最短路径),典型应用是导航、交通规划等局部最优路径问题。

ps:创作不易,给博主点点赞点点关注,感谢大家。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值