图论基础算法总结

图论基础算法总结

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个正整数NMSD,其中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算法比较简单易懂,步骤如下:

  1. 统计每个节点的入度(即有多少个节点指向该节点)。

  2. 将入度为0的节点加入一个队列中。

  3. 从队列中取出一个节点,将其输出,并将其所有指向的节点的入度减1。如果某个节点的入度为0,则将其加入队列中。

  4. 重复步骤3,直到队列为空。如果队列为空时还有节点未输出,则说明图中存在环,无法进行拓扑排序。

  5. 拓扑排序的时间复杂度是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';
    }   
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-Gaojs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值