图论基础算法总结
SPFA算法
SPFA(Shortest Path Faster Algorithm)是一种单源最短路径算法,可以用来求解有向图中某一个源点到其他所有点的最短路径。它是Bellman-Ford算法的优化版本,它的时间复杂度是O(k*E),其中k是最短路径的边数上界,E是图中的边数。
SPFA算法采用了贪心的思想,它每次从当前已知最短路径的点出发,尝试更新其邻接点的最短路径。当某个节点的最短路径发生改变时,它的邻接点也可能需要更新最短路径。因此,SPFA算法在每次更新某个节点的最短路径后,将该节点的邻接点入队,以便后续更新。为了防止重复更新,SPFA算法使用了一个标记数组,记录每个节点是否已经入队。
伪代码:
SPFA(G, s):
初始化dis数组,所有节点的距离为无穷大
初始化标记数组,所有节点的标记为false
初始化队列,将起点s入队,并将其标记为true
dis[s] = 0
while 队列不为空:
取出队头元素u
将u的标记设置为false
for u的所有邻接点v:
如果dis[u] + weight(u, v) < dis[v]:
dis[v] = dis[u] + weight(u, v)
如果v没有被标记,则将v入队并将其标记为true
返回dis数组
其中,G表示有向图,s表示起点,dis数组存储从起点到各个节点的最短距离,weight(u, v)表示从节点 u u u到节点 v v v的边权值。
需要注意的是,SPFA算法可能会陷入负环的死循环,因此在实际应用中需要对其进行优化。一种常见的优化方式是限制每个节点进队列的次数,当某个节点的进队列次数超过图中节点数时,认为该图存在负环,退出算法。
举例
给定一个n个点m条边的有向图,图中可能存在重边和自环,边权可能为负数。
请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出impossible
第一行包含整数n和m
接下来m行每行包含三个整数x,y,z,表示点x和点y之间存在一条有向边,边长为z.
输出一个整数,表示1号点到n号点的最短距离
如果路径不存在,则输出"impossible"
1 <= n,m <= 1e5;
图中涉及边长绝对值均不超过10000.
输入
3 3
1 2 5
2 3 -3
1 3 4
输出
2
#include<bits/stdc++.h>
using namespace std;
const int N = 1e3+10;
int n,m;
int e[N],ne[N],h[N],w[N],idx;
int dist[N];
bool vis[N];
void add(int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int spfa() {
memset(dist,0x3f,sizeof(dist));
queue<int> q;
dist[1] = 0;
q.push(1);
vis[1] = true;
while (q.size()) {
int u = q.front();
q.pop();
vis[u] = false;
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[u] + w[i]) {
dist[j] = dist[u] + w[i];
if (!vis[j]) {
q.push(j);
vis[j] = true;
}
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main() {
cin >> n >> m;
memset(h,-1,sizeof(h));
while (m--) {
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
int t = spfa();
if (t == -1) puts("impossible");
else cout << t;
}
Dijkstra算法
Dijkstra算法介绍
Dijkstra算法是一种用于在加权有向图或无向图中寻找最短路径的算法。它是由荷兰计算机科学家Edsger W. Dijkstra在1956年提出的。
该算法的基本思想是从起点开始,逐步扩展路径,直到到达终点。在每一次扩展中,选取当前路径中权重最小的节点,并将其作为下一步要扩展的节点。这个过程会维护一个距离数组来记录每个节点到起点的距离。
具体实现时,我们可以使用一个优先队列来存储节点和其对应的距离值。在每次迭代中,从优先队列中弹出距离起点最近的节点,并更新其周围节点的距离值。如果发现一个新的更短的路径,则将其加入优先队列中,以备后续使用。
最终,当优先队列为空时,Dijkstra算法就完成了,返回的距离数组中存储了每个节点到起点的最短距离。
需要注意的是,Dijkstra算法只能用于处理有权图。此外,如果有负权边,则该算法将不再适用,因为它只适用于没有负权边的图。如果需要在有负权边的图中求最短路径,则需要使用另一种算法,如Bellman-Ford算法。
举例
有了一张自驾旅游路线图,你会知道城市间的高速公路长度、以及该公路要收取的过路费。现在需要你写一个程序,帮助前来咨询的游客找一条出发地和目的地之间的最短路径。如果有若干条路径都是最短的,那么需要输出最便宜的一条路径。
输入格式:
输入说明:输入数据的第1行给出4个正整数N、M、S、D,其中N(2≤N≤500)是城市的个数,顺便假设城市的编号为0~(N−1);M是高速公路的条数;S是出发地的城市编号;D是目的地的城市编号。随后的M行中,每行给出一条高速公路的信息,分别是:城市1、城市2、高速公路长度、收费额,中间用空格分开,数字均为整数且不超过500。输入保证解的存在。
输出格式:
在一行里输出路径的长度和收费总额,数字间以空格分隔,输出结尾不能有多余空格。
输入样例:
4 5 0 3
0 1 1 20
1 3 2 30
0 3 4 10
0 2 2 20
2 3 1 20
输出样例:
3 40
#include<bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
struct Edge {
int v,d,w;
};
int V,E,S,D;
vector<vector<Edge>> G; // G里面存的图
vector<bool> vis;
vector<int> dist,cost;
// 迪杰斯特拉算法
void dijkstra (int s) {
vis.assign(V,false);
dist.assign(V,INF);
cost.assign(V,INF);
dist[s] = 0, cost[s] = 0; // dist为两点之间的距离,cost为两点之间的花费
// int t = 1;
while (true) {
int u = -1;
for (int i = 0; i < V; i++) {
if ((u == -1 || dist[u] > dist[i]) && !vis[i]) {
u = i;
}
}
if (u == -1) break;
vis[u] = true;
// u 表示 第u 点的图的信息,i表示u点的第i个图的信息,不表示任何点
for (int i = 0; i < G[u].size(); i++) {
if (dist[G[u][i].v] > dist[u] + G[u][i].d) { // 贪心算法思想
dist[G[u][i].v] = dist[u] + G[u][i].d;
cost[G[u][i].v] = cost[u] + G[u][i].w;
}
else if (dist[G[u][i].v] == dist[u] + G[u][i].d) {
cost[G[u][i].v] = min(cost[G[u][i].v],cost[u] + G[u][i].w);
}
}
}
}
int main() {
cin >> V >> E >> S >> D;
G.resize(V);
for (int i = 0,u,v,d,w; i < E; i++) {
cin >> u >> v >> d >> w;
G[u].push_back({v,d,w});
G[v].push_back({u,d,w});
}
dijkstra(S);
cout << dist[D] << " " << cost[D];
return 0;
}
Floyd算法
Floyd算法,也称为Floyd-Warshall算法,是一种用于求解所有最短路径的动态规划算法,其时间复杂度为O(n^3)。该算法可以处理有向图或无向图的带权图,其中权值可以是正数、负数或零。
Floyd算法的基本思路是,利用动态规划的思想,逐步地计算出所有点对之间的最短路径,通过逐步优化子问题的解来得到整个问题的解。该算法的核心是一个三重循环,其中每次迭代都更新所有点对之间的距离矩阵,直到所有的最短路径都被求出。
Floyd算法的优点是,它可以处理带有负权边的图,并且可以同时求解所有点对之间的最短路径,而不需要重复计算。缺点是,它的时间复杂度较高,当图的规模很大时,算法的效率会受到影响。此外,该算法需要使用一个二维矩阵来存储任意两个点之间的距离,因此需要更多的内存空间。
总之,Floyd算法是一种经典的动态规划算法,用于求解所有点对之间的最短路径,具有较好的适用性和鲁棒性。
举例
给定一个n个点m条边的有向图,图中可能存在重边和自环,边权可能为负数
再给定K个询问,每个询问包含两个整数x和y,表示查询从点x到点y的最短距离,如果路径不存在,则输出"impossible"
数据保证图中不存在负权回路
输入格式
第一行包含三个整数n,m,k
接下来m行,每行包含三个整数x,y,z,表示点x和点y之间存在一条有向边,边长为z.
接下来k行,每行包含两个整数,x,y,表示询问点x到点y的最短距离。
输出格式
共k行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出"impossible".
数据范围
1 <= n <= 200
1 <= k <= n^2
1 <= m <= 20000
图书涉及边长绝对值均不超过10000
输入样例
1 2 1
2 3 2
1 3 1
2 1
1 3
输出样例
impossible
1
#include<bits/stdc++.h>
using namespace std;
const int N= 1e4+10, INF = 1e9;
int d[N][N];
int n,m,Q;
void floyd() {
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
d[i][j] = min(d[i][j],d[i][k] +d[k][j]);
}
}
}
}
int main() {
cin >> n >> m >> Q;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (i == j) d[i][j] = 0;
else d[i][j] = INF;
}
}
while (m--) {
int a,b,w;
cin >> a >>b >> w;
d[a][b] = min(d[a][b],w);
}
floyd();
while (Q--) {
int a, b;
cin >> a >> b;
if (d[a][b] > INF/2) puts("impossible");
else
printf("%d\n",d[a][b]);
}
return 0;
}
Bellman_ford算法
Bellman-Ford算法是一种用于解决带有负权边的单源最短路径问题的算法。它可以处理有向图和无向图。
该算法通过反复地更新每个节点的最短路径估计值,最终得到源节点到其它所有节点的最短路径。它采用松弛操作来更新节点的最短路径估计值,松弛操作的过程是通过对每条边进行一定次数的松弛操作实现的。
具体来说,Bellman-Ford算法需要进行n-1次松弛操作,其中n为图中节点的个数。每次松弛操作,对于每条边(u,v),如果从源节点到节点u的路径加上边(u,v)的权重,得到的路径长度比从源节点到节点v的当前最短路径长度更小,则更新节点v的最短路径估计值为源节点到节点u再加上边(u,v)的权重。
如果在进行n-1次松弛操作后,仍然存在节点的最短路径估计值可以被更新,则说明图中存在负环路,即从某个节点出发,经过一个或多个环路后,回到该节点,路径长度为负数。此时Bellman-Ford算法将无法计算出最短路径。
总的来说,Bellman-Ford算法的时间复杂度为O(mn),其中m为图中边的个数,n为图中节点的个数。如果没有负环路,则算法的时间复杂度可以优化到O(mlogn),使用Dijkstra算法实现。
举例
给定一个n个点m条边的有向图,图中可能存在重边和自环,边权可能为负数。
请你求出从一号点到n号点的最多经过k条边的最短距离,如果无法从1号点走到n号点,输出impossible.
注意:图中可能存在负权回路
输入格式
第一行包含三个整数n,m,k;
接下来m行,每行包含三个整数x,y,z.表示点x和点t之间存在一条有向边,边长为z.
输出格式
输出一个整数,表示从1号点到n号点的最多经过k条边的最短距离。
如果不存在满足条件的路径,则输出"impossible"
数据范围
1 <= n,k <= 500
1 <= m <= 10000
任意边长的绝对值不超过10000.
输入样例
3 3 1
1 2 1
2 3 1
1 3 3
输出样例
3
#include<bits/stdc++.h>
using namespace std;
const int N = 510, M = 10010;
int n,m,k;
int dist[N],backup[N];
struct Edge {
int a,b,w;
}edges[M];
int bellman_ford() {
memset(dist,0x3f,sizeof(dist));
dist[1] = 0;
for (int i = 0; i < k; i++) {
memcpy(backup,dist,sizeof(dist));
for (int j = 0; j < m; j++) {
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
dist[b] = min(dist[b],backup[a] + w);
}
}
if (dist[n] > 0x3f3f3f3f/2) return -1;
return dist[n];
}
int main() {
scanf("%d%d%d",&n,&m,&k);
for (int i = 0; i < m; i++) {
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
edges[i] = {a,b,w};
}
int t = bellman_ford();
if (t == -1) puts("impossible");
else printf("%d\n",t);
}
拓扑排序
拓扑排序是一种对有向无环图(DAG)进行排序的算法。在DAG中,如果存在从节点A到节点B的有向路径,那么在排序中节点A一定排在节点B的前面。拓扑排序可以用来解决很多问题,比如在一个项目中,需要先完成某些任务才能进行后续的任务,就可以用拓扑排序来确定任务的执行顺序。
拓扑排序的实现方法很多,最常见的方法是 Kahn算法和DFS算法。其中Kahn算法比较简单易懂,步骤如下:
-
统计每个节点的入度(即有多少个节点指向该节点)。
-
将入度为0的节点加入一个队列中。
-
从队列中取出一个节点,将其输出,并将其所有指向的节点的入度减1。如果某个节点的入度为0,则将其加入队列中。
-
重复步骤3,直到队列为空。如果队列为空时还有节点未输出,则说明图中存在环,无法进行拓扑排序。
-
拓扑排序的时间复杂度是O(V+E),其中V是节点数,E是边数。
举例
一个项目由若干个任务组成,任务之间有先后依赖顺序。项目经理需要设置一系列里程碑,在每个里程碑节点处检查任务的完成情况,并启动后续的任务。现给定一个项目中各个任务之间的关系,请你计算出这个项目的最早完工时间。
输入格式:
首先第一行给出两个正整数:项目里程碑的数量 N(≤100)和任务总数 M。这里的里程碑从 0 到 N−1 编号。随后 M 行,每行给出一项任务的描述,格式为“任务起始里程碑 任务结束里程碑 工作时长”,三个数字均为非负整数,以空格分隔。
输出格式:
如果整个项目的安排是合理可行的,在一行中输出最早完工时间;否则输出"Impossible"。
输入样例 1:
9 12
0 1 6
0 2 4
0 3 5
1 4 1
2 4 1
3 5 2
5 4 0
4 6 9
4 7 7
5 7 4
6 8 2
7 8 4
输出样例 1:
18
输入样例 2:
4 5
0 1 1
0 2 2
2 1 3
1 3 4
3 2 5
输出样例 2:
Impossible
#include<bits/stdc++.h>
using namespace std;
const int N = 1e2+10;
int n,m;
struct edge {
int v,w;
};
int d[N];
vector<edge> G[N];
int dist[N];
bool toposort() {
queue<int> q;
for (int i = 0; i < n; i++) {
if (!d[i]) q.push(i);
}
vector<int> nums;
while (q.size()) {
int t = q.front();
q.pop();
nums.push_back(t);
for (auto x : G[t]) {
if (--d[x.v] == 0) q.push(x.v);
dist[x.v] = max(dist[x.v],dist[t]+x.w);
}
}
return nums.size() == n;
}
int main() {
cin >> n >> m;
while (m--) {
int u,v,w;
cin >> u >>v >> w;
G[u].push_back({v,w});
++d[v];
}
if (!toposort() ) puts("Impossible");
else
{
int ret = 0;
for (int i = 0; i < n; i++) {
ret = max(dist[i],ret);
}
cout << ret << '\n';
}
}