算法竞赛进阶指南--基本算法之最短路(一)

版权声明:如需转载请联系博主! From 博主Haolin https://blog.csdn.net/ohh_haolin/article/details/85137750

存图

邻接矩阵

定义(x,y)是x到y的有向边,边权为w(x,y):
用矩阵A存,空间消耗是O(n^2)

A[x,y] 含义
0 x=y
w(x,y) x,y联通
x,y不联通
邻接链表

定义(x,y)是x到y的有向边,边权为ver
开特定数组来存图,空间消耗是O(n+m),n代表出发点的值的数量,m为边数

名称 意义
head[x] 当前x的第一条边
nxt[i] 定义i是x的一条边,表示x的下一条边
ver[i] 表示i边到哪里
edge/weight[i] i边的边权
tot 表示当前有几个边
//加入有向边(x,y)权为z
void add(int x, int y, int z) {
	ver[++tot] = y, edge[tot] = z;	//构建边
	nxt[tot] = head[x], head[x] = tot;	//插入边
}
//遍历方法
for (int i = head[x];i;i = nxt[i]) {
	int y = ver[i], z = edge[i];//此边到y,权值为z
}

单源最短路

Single Source Shortest Path, SSSP问题

给定有向图 G = (V,E)V是点集合,E是边集,|V|=n,|E|=m,用(x,y,z)描述一条边,求出1号点到所有点的最短路,并存入dis[i] 之中

Dijkstra算法
  1. 初始化 dis[1] = 0, 其余都为无穷大
  2. 找出一个为被标记过的点,并且dis[x] 最小,标记节点 x
  3. 扫描i的所有出边(x,y,z), 若dis[y] > dis[x] + z, 则用dis[x]+z更新dis[y]
  4. 重复2~3直到所有节点都被标记(在这个联通块内)

Dijkstra 基于贪心思想,所以只适用于边长非负数的图!

code:

#include <algorithm>
#include <cstdio>
#include <queue>
#include <iostream>
#include <cstring>
using namespace std;

const int N = 100010, M = 1000010;
int head[N],ver[M],edge[M],nxt[M],d[N];
bool vis[N];
int n,m,tot;

priority_queue< pair<int, int> > q;
// 大堆跟维护即可(取最大值即负数最小值) 
// 第一个是距离的负数,第二个是节点编号 
 

void add(int x,int y,int z) {
	ver[++tot]=y, edge[tot]=z, nxt[tot]=head[x], head[x]=tot; 
}

void dijkstra() {
	memset(d, 0x3f, sizeof(d));
	memset(vis, 0, sizeof(vis));
	
	d[1] = 0;
	q.push(make_pair(0, 1));
	while(!q.empty()) {
		int x = q.top().second; q.pop();
		
		if(vis[x]) continue;
		
		vis[x] = 1;
		for(int i=head[x];i;i=nxt[i]) {
			int y = ver[i], z = edge[i];
			if(d[y] > d[x]+z){
				d[y] = d[x] + z;
				q.push(make_pair(-d[y],y));
			}
		}
	}
}

int main() {
	cin >> n >> m;
	for(int i=1;i<=m;i++) {
		int x, y, z;
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);
	}
	
	dijkstra();
	
	for(int i=1;i<=n;i++) {
		printf("%d\n",d[i]);
	}
} 
Bellmen-Ford算法和SPFA算法

SPFA 为 Shortest Path Fast Algorithm 的缩写

给定一张有向图,若对图中的任意一遍(x,y,z), 有 dis[y] <= dis[x] + z 成立那么则称该边满足三角形不等式。若所有边都满足三角形不等式,则 dis 数组就是所求最短路。

迭代思想的Bellman-Ford算法:

  1. 扫描所有边(x,y,z),若 dis[y] > dis[x] + z 则用 dis[x] + z 更新 dis[y]
  2. 重复步骤1直到没有更新操作发生

实际上, SPFA在国际上统称之为“队列优化的Bellman-Ford算法”, 仅在中国叫SPFA,流程如下:

  1. 建立一个队列,最初队列只有起点1
  2. 取出对头节点x,扫描他的所有出边(x,y,z),若 dis[y] > dis[x] + z, 则用 dis[x] + z 更新dis[y]。同时,若y不在队列中,则把y入队
  3. 重复上述操作,直到队列为空

在任意时刻,该算法的队列仅存了需要扩展的节点,每一次入队完成一次dis数组的更新操作,使其满足三角不等式,一个节点可能会出对入队多次,最终图会都收敛到满足三角形不等式的状态,这个队列避免了 Bellman-Ford 算法对于不需要的点的冗余扫描,在稀疏图的理想情况下可以实现O(km)级别,并且k是一个较小的常数,但在稠密图上或者特殊的网格图上会退化到O(n,m)。但是SPFA可以跑负权图!

code:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;

const int N = 100010, M = 1000010;
int head[N], ver[M], edge[M], nxt[M], d[N];
int n, m, tot;
bool vis[N];

queue<int> q;

void add(int x, int y, int z) {
	ver[++tot] = y, edge[tot] = z, nxt[tot] = head[x], head[x] = tot;
}

void spfa() {
	memset(d, 0x3f, sizeof(d));
	memset(vis, 0, sizeof(vis));
	
	d[1] = 0; vis[1] = 1;
	q.push(1);
	while(!q.empty()){
		int x = q.front();q.pop();
		vis[x] = 0;
		
		for(int i=head[x];i;i = nxt[i]) {
			int y = ver[i], z = ver[z];
			if(d[y] > d[x] + z) {
				d[y] = d[x] +z;
				if(!vis[y]) q.push(y), vis[y]=1;
			}
		}	
	}
	
}

int main(){
	cin >> n >> m;
	for(int i=1;i<=m;i++) {
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		add(x, y, z);
	}
	
	spfa();
	
	for(int i=1;i<=n;i++) {
		printf("%d\n", d[i]);
	}
}

当图中存在负权边时,bellman-ford算法和 spfa 能够正常工作,但是时间复杂度会进一步增加,有神仙用双端队列的方法优化SPFA,其名为SLF,如果当前的 dis[y] 小于队头则压入队头, 否则压入队尾,一般情况能稍稍提高(没啥大作用)。
如果我们不存在负权边时候,可以使用类似dijkstra中的优先队列来进行优化,每次取出当前距离最小的的(堆顶)来进行拓展。。。。。然后,然后,然后,它就变成了dijkstra!。。。
其实在一般情况下是不需要优化滴!所以背会板子就好了

例题1

洛谷P3371:https://www.luogu.org/problemnew/show/P3371

注意:
1、自己改改板子交一下
2、第三个数据点毒瘤,要判重边!存最小的,方法很简答,单独存或者遍历出边即可

例题2

洛谷P1948: https://www.luogu.org/problemnew/show/P1948

目测题目具有单调性,因为当前的支付方案是一定建立在合法的方案上的,所以可以二分,进而转化问题成为:是否存在一种合法的升级方法使得花费不超过mid。转化后的判定问题非常容易,只需要把升级价格大于mid的电缆看作长度为1的边,把升级价格不超过mid的电缆看作为长度为0的边,然后求从1到N的最短路是否超过k即可,流程如下:

洛谷里面包含一组毒瘤数据!

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <queue>
#include <cstring>
using namespace std;

const int N = 1010, M = 20010;
int n,p,k,tot,head[N],ver[M],nxt[M],edge[M],dis[N];
bool vis[1005];
queue<int > q;

void add(int x,int y,int z){
	ver[++tot] = y; edge[tot] = z;
	nxt[tot] = head[x], head[x] = tot;
}

bool check(int x) {
	memset(dis, 0x3f, sizeof(dis));
	memset(vis, 0, sizeof(vis));
	int s,now;
	
	dis[1]=0,vis[1]=1;
	q.push(1);
	while(!q.empty()) {
		now = q.front();q.pop();
		vis[now] = 0;
		
		for(int i=head[now];i;i=nxt[i]) {
			int z = edge[i],y = ver[i];
			
			if(z>x) s = dis[now]+1;
			else s = dis[now];
			
			if(s<dis[y]) {
				dis[y]=s;
				if(!vis[y]) {
				q.push(y),vis[y]=1;
				}
			}
		} 
	}
	if(dis[n]<=k) return 1;
	return 0;
}

int main(){
	scanf("%d%d%d",&n,&p,&k);
	int x, y, z;
	for(int i=1;i<=p;i++) {
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);add(y,x,z);
	}
	
	int l=0,r=1000000,ans;
	while(l<=r) {
		int mid = (l+r)>>1;
		if(check(mid)) {
			ans = mid,r = mid-1;
		}else l = mid+1;
	}
	cout << ans << endl;
	
	return 0;
}

多元最短路

Floyd算法

为了求出图中任意两点间的最短距离,当然可以把每个点作为起点,求解N次单元最短路,不过在任意两点之间的问题中,图一般较为稠密,使用Floyd可以完成O(N^3)的时间

设 D[k, i, j] 表示经过若干个编号不超过k的节点从 i 到 j 的最短路长度,该问题可以划分成为两个子问题:经过编号k-1 的节点从i到j或者从i先到k再到j,于是我们可以得到公式:

D[ k, i ,j] = min(D [k-1,i ,j ], D[k-1,i,k] + D [k-1, k ,j] )

其实可以这样理解:每一轮把编号为k的点加进去,比较距离,因为之后的节点会保存之前节点的对应信息,所以关系会被一直传递下去

for (int k=1;k<=n;l++) 		//模拟加入一个点k
	for(int i=1;i<=n;i++)		//模拟起点i
		for(int j=1;j<=n;j++)		//模拟终点位置j
			d[i][j] = min(d[i][j], d[i][k]+d[k][j]);	//是否需要过这个点k

猜你喜欢

转载自blog.csdn.net/ohh_haolin/article/details/85137750