最短路问题(用C/C++实现)

源点:起点。汇点:终点。

m~n2:稠密图(用邻接矩阵来存)。m~n:稀疏图(用邻接表来存)。

该问题核心是建图,如何定义边与点,如何将复杂问题抽象成最短路问题。

一、单源最短路

一般是求一个点到其他所有点的最短距离。

1. 所有边都是正权值的图

时间按复杂度m如果与n2的数量级相近,则选用朴素的Dijkstra算法处理稠密图(边较多);反之当m数量级较小则选用堆优化版处理稠密图。

方法一:朴素Dijkstra算法O(n2)

s集合:当前已确定最短距离的点。

Step1:初始化点的距离,起点距离为0,其他所有点都是正无穷。

dist[1] = 0; dist[i] = INF;

Step2:for i: 0~n

找到不在s中的距离最近的点t

将t加入集合s

用t更新其他所有点的距离,即看t节点的出边连接的节点到起点的距离能否使用t来更新,dist[x] > dist[t] + w.

该步结束后可以得到每个点到起点的距离。

#include<iostream>
#include<cstring>
#include<algorithm>
/*给定一个n个点m条边的有向图,图中可能存在重边与自环,所有边均为正值
请求出1~n的最短距离,如果不通则输出-1*/
using namespace std;

const int N = 510;

int n, m;
int g[N][N];
int dist[N];
bool st[N];

int dijkstra()
{
	//初始化点的距离 
	memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;
	
	for(int i = 0; i < n; i ++)
	{
		int t = -1;
		for(int j = 1; j <= n; j ++)
			//集合中没有这个点,且当前的t不是最短的就把t更新成j(t=-1直接赋值) 
			if(!st[j] && (t == -1 || dist[t] > dist[j]))
				t = j;
		st[t] = true;
		
		for(int j = 1; j <= n; j ++)
			dist[j] = min(dist[j], dist[t] + g[t][j]); 
	}
	//不连通
	if(dist[n] == 0x3f3f3f3f) return -1;
	return dist[n]; 
}

int main()
{
	scanf("%d%d", &n, &m);
	
	memset(g, 0x3f, sizeof g);
	while(m --)
	{
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		g[a][b] = min(g[a][b], c);//保存长度最短的那条边即可 
	}
	
	int t = dijkstra();
	
	printf("%d\n", t);
	return 0;
} 

方法二:堆优化版的Dijkstra算法O(mlogn)

在方法一中,用t更新其他点的距离需要m次,但如果用堆来存储所有点的距离,那么时间复杂度就会变成mlogn.

堆有两种实现方法,一种是手写堆,堆中只需要n个数;另一种是用优先队列,但是它不能修改元素值,只能通过冗余的方法,即往堆中插入新的元素,即有m个元素,时间复杂度为mlogm。

#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
/*给定一个n个点m条边的有向图,图中可能存在重边与自环,所有边均为正值
请求出1~n的最短距离,如果不通则输出-1*/
using namespace std;

typedef pair<int, int> PII;

const int N = 100010;

int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N];

void add(int a, int b, int c)
{
	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}

int dijkstra()
{
	//初始化点的距离 
	memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;

	priority_queue<PII, vector<PII>, greater<PII>> heap;
	heap.push({0, 1});
	
	while(heap.size())
	{
		//当前距离最小的点 
		auto t = heap.top();
		heap.pop();
		
		int ver = t.second, distance = t.first;
		if(st[ver]) continue;//已经遍历过
		
		for(int i = h[ver]; i != -1; i = ne[i])
		{
			int j = e[i];
			if(dist[j] > distance + w[i])
			{
				dist[j] = distance + w[i];
				heap.push({dist[j], j});
			}
		}
	}
	//不连通
	if(dist[n] == 0x3f3f3f3f) return -1;
	return dist[n]; 
}

int main()
{
	scanf("%d%d", &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 = dijkstra();
	
	printf("%d\n", t);
	return 0;
} 

2. 存在边是负权值的图

SPFA算法是Bellman算法的优化,但有些情况是SPFA算法解决不了的,比如解决边数不超过k条的图的问题,就只能使用Bellman算法。

方法一:Bellman-Ford O(nm)

for n次

    对dist数组进行备份。

    for 所有边 a, b, w

           dist[b] = min(dist[b], dist[a] + w);//松弛操作

看从起点经过a再到b的距离与从起点直接到b的距离哪个更近。

结束之后满足dist[b] <= dist[a] + w,称该式为三角不等式。

此处备份的原因:在更新这些点的距离时可能会发生串联,即随着一些点的变化,另一些点会随之改变距离,所以要进行备份保证每个点的距离都是上一次的结果。

注:如果图中存在负权回路,即有一个回路所有边总和为负值,那么可以走无数遍该回路得到距离为负无穷,故这种情况下不存在最短路。

#include<iostream>
#include<cstring>
#include<algorithm>
/*给一个n个点m条边的问题,图中可能存在重边和自环,边权可能为负数
请你求出从1号到n号点的最多经过k条边的最短距离,不同则输出impossible*/
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);
	return 0;
}
 

方法二:SPFA  一般:O(m) 最坏:O(nm)    

优化了dist[b] = min(dist[b], dist[a] + w)这个式子,当dist[a]更新后变小了,dist[b]才可能会变小。

1. 这里使用宽搜来实现:

将更新后变小的点放入队列,拿出队头更新它的所有出边,更新后的出边再放入队列。(思路是,更新过谁,再拿谁来更新别人)

#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
/*给一个n个点m条边的问题,图中可能存在重边和自环,边权可能为负数
请你求出从1号到n号点的最多经过k条边的最短距离,不同则输出impossible*/
using namespace std;

const int N = 100010;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N];//当前点是否在队列当中 

void add(int a, int b, int c)
{
	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
	
}

int spfa()
{
	memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;
	
	queue<int> q;
	q.push(1);
	st[1] = true;
	
	while(q.size())
	{
		int t = q.front();
		q.pop();
		st[t] = false;
		for(int i = h[t]; i != -1; i = ne[i])
		{
			int j = e[i];
			if(dist[j] > dist[t] + w[i])
			{
				dist[j] = dist[t] + w[i];
				if(!st[j])
				{
					q.push(j);
					st[j] = true;
				}
			}
		}
	}
	if(dist[n] > 0x3f3f3f3f) return -1;
	return dist[n]; 
}

int main()
{
	scanf("%d%d", &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 printf("%d\n", t);
	return 0;
} 

2. 判断负环的方式:

dist[x]存最短距离,cnt[x]存边数

dist[x] = dist[t] + w[i];

cnt[x] = cnt[t] + 1;

在更新最短距离的同时更新边的数量,当cnt[x] >= n时,即一共n个点却出现n+1条边,且距离更新了,故一定存在负环。

#include<iostream>
#include<cstring>
#include<algorithm> 
#include<queue>
/*给定一个n个点m条边的有向图,图中存在重边和自环,边权可能为负
请判断图中是否存在负权回路*/
using namespace std;

const int N = 10010; 
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N], cnt[N];
bool st[N];

void add(int a, int b, int c)
{
	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}

int spfa()
{	
	queue<int> q;
	for(int i = 1; i <= n; i ++)
	{
		st[i] = true;
		q.push(i);
	}
	
	while(q.size())
	{
		int t = q.front();
		q.pop();
		st[t] = false;
		for(int i = h[t]; i != -1; i = ne[i])
		{
			int j = e[i];
			if(dist[j] > dist[t] + w[i])
			{
				dist[j] = dist[t] + w[i];
				cnt[j] = cnt[t] + 1;
				if(cnt[j] >= n) return true;
				if(!st[j])
				{
					q.push(j);
					st[j] = true;
				}
			}
		}
	}

	return false; 
}

int main()
{
	scanf("%d%d", &n, &m);
	
	memset(h, -1, sizeof h);
	while(m --)
	{
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c);
	}
	
	
	if(spfa()) puts("Yes");
	else puts("No");
	return 0;
} 

二、 多源汇最短路

多个起点,有多个询问,求起点到终点的距离,起点终点是不确定。

Floyd算法 O(n3)(基于动态规划)

for( k: 1 ~ n)

   for( i: 1 ~ n)

for( j: 1 ~ n)

       d(i, j) = min(d(i, j), d(i, k) + d(k, j));

循环完毕之后,d(i, j)存的就是i到j的最短路。

原理:d[k, i, j]:从i开始要经过1~k个中间点到达j点。

d[k, i, j] = d[k-1, i, k] + d[k-1, k, j];将最高维优化掉后就是d[i, j] = d[i, k] + d[k, j]

#include<iostream>
#include<algorithm>
#include<cstring>
/*给一个n个点m条边的问题,图中可能存在重边和自环,边权可能为负数
在给定k个询问,每个询问包含两个整数x和y,表示查询从x到y的最短距离
不存在输出imposssible*/
using namespace std;

const int N = 210, INF = 1e9;
int n, m, Q;
int d[N][N];

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()
{
	scanf("%d%d%d", &n, &m, &Q);
	//初始化 
	for(int i = 1; i <= n; i ++)
		for(int j = 1; j <= n; j ++)
		{
			if(i == j) d[i][i] = 0;
			else d[i][j] = INF;
		}
	
	while(m--)
	{
		int a, b, w;
		scanf("%d%d%d", &a, &b, &w);
		//有多条边就读入最小的即可 
		d[a][b] = min(d[a][b], w);
	}
	
	floyd(); 
	
	while(Q --)
	{
		int a, b;
		scanf("%d%d", &a, &b);
		if(d[a][b] > INF / 2) puts("impossible");
		else printf("%d\n", d[a][b]);
	}
		
	return 0;
} 

  • 22
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值