学习笔记 (最短路)

dijkstra:求单源最短路

算法步骤:
  1. 找离起点 x x x最近的未讨论过的点 k k k
  2. 判断经过 k k k点,起点x到其他点的距离是否缩短,如缩短
    则更新。将 k k k点标记为已讨论。
  3. 重复进行第 1 1 1步,直到所有点都被讨论过为止。

于是我们可以得出:

d i s [ i ] = m i n ( d i s [ k ] + M a p [ k ] [ i ] ) ( 1 ≤ i ≤ n ) dis[i]=min(dis[k]+Map[k][i]) (1 \leq i \leq n) dis[i]=min(dis[k]+Map[k][i])(1in) k k k是未讨论过的,离起点最近的点

d i s [ i ] dis[i] dis[i]表示从起点到 i i i点最短距离

P S PS PS:为了方便,此篇文章部分采用邻接矩阵存图,建议各位换成更高效的存图方法。

c o d e code code:

void dijkstra(int x){
	for(int i=1;i<=n;i++){        初始化
		mark[i]=false;         mark记录第i号节点是否被讨论过
		dis[i]=Map[x][i];     初始化dis数组,dis[i]用于记录起点x到i的最短距离
	}
	mark[x]=true;           将起点标记为已被讨论,防止走回头路
	do{                            算法核心部分:
		minn=inf;
		k=0;            k记录离x最近的点的编号
		for(int i=1;i<=n;i++)     寻找当前离x最近且未被讨论过的节点
			if((mark[i]==false) && (dis[i]<minn)){
				minn=dis[i];
				k=i;
			}
               讨论经过k点,有没有其他点到x的距离缩短
		if(k>0){
			mark[k]=true;将k号点设置为已被讨论过
			for(int i=1;i<=n;i++)若经过k号点,起点x到i的距离缩短,则更新dis[i]
				if(dis[i]>dis[k]+Map[k][i])
					dis[i]=dis[k]+Map[k][i];
		}
	}while(k>0);
}

时间复杂度 O ( n 2 ) O(n^2) O(n2) 这肯定不行,得想办法优化!

可以发现,每次我们都是要找出离x最近的点的编号,于是我们便可以用强大的stl里的优先队列来完成这个操作,将其优化至 O ( n l o g n ) O(nlogn) O(nlogn)

c o d e code code d i j + 堆 优 化 + 链 式 前 向 星 dij+堆优化+链式前向星 dij++

struct node{
	int num,dis;       num记录节点编号,dis记录起点到该店的最短距离
	bool operator < (const int &a) const{
		return dis>a.dis;	重载< 以dis从小到大排序 
	}
};
void dijkstra(int s){
	for(int i=1;i<=n;i++){初始化
		dis[i]=inf;
		mark[i]=false;
	}
	dis[s]=0;
	priority_queue<node> q;以距离为关键字的小根堆,方便取出目前离起点最近的点
	q.push((node){s,0});
	while(q.size()){
		int x=q.top().num;
		q.pop();
		if(mark[x])  若离起点最近的点x已被讨论过,就跳过
			continue;
		mark[x]=true;
		for(int i=Last[x];i;i=Next[i]){  链式前向星
			int y=End[i];
			if(!mark[y] && dis[y]>dis[x]+len[i]){更新dis[]
				dis[y]=dis[x]+len[i];
				q.push((node){y,dis[y]});
			}
		}
	}
}

SPFA (他死了)

SPFA是Bellman-Ford算法的一种队列实现,减少了不必要的运算。

算法流程:

用一个队列来进行维护。初始时将起点加入队列。每次从队列中取出一个元素,并对他所连接的点进行松弛,若某个点松弛成功(则通过那个点到起点距离缩短),则将其入队。直到队列为空

简单的说就是队列优化的 b e l l m a n − f o r d bellman-ford bellmanford,利用了每个点不会更新次数太多的特点

SPFA的时间复杂度是 O ( k E ) O(kE) O(kE) k k k一般取 2 2 2左右( k k k是增长很快的
函数 a c k e r m a n n ackermann ackermann的反函数, 2 65536 2^{65536} 265536次方也就 5 5 5以下 ),可以处
理负边。

S P F A SPFA SPFA的实现甚至比 D i j k s t r a Dijkstra Dijkstra或者 B e l l m a n F o r d Bellman_Ford BellmanFord还要简单

queue<int> q;
void spfa(int s){    s为起点,求s到图中所有点的距离
	for(int i=1;i<=n;i++)
		dis[i]=inf;
	q.push(s);
	mark[s]=true;
	dis[s]=0;
	while(q.size()){
		int x=q.front();
		q.pop();
		mark[x]=false;
		for(int i=1;i<=n;i++)   讨论与x相连的点
			if(dis[x]+Map[x][i]<dis[i]){  若x通过i点到起点的距离缩短则更新
				dis[i]=dis[x]+Map[x][i];
				if(mark[i]==false){    若不在队列中,入队
					q.push(i);
					makr[i]=true;
				}
			}
	}
}

那么,上文说了 S P F A SPFA SPFA可以处理负权边,那么遇到了负环(负权回路)该怎么处理呢?

用SPFA判断负权回路

让我们来想一想:
S P F A SPFA SPFA算法中,每个点最多进队多少次?

很显然是 n − 1 n-1 n1次,因为对于一个点 x x x,最坏情况下是其余 n − 1 n-1 n1个点都可以将其松弛。

所以如果有一个点进队次数超过了 n n n次,我们就可以认定,该图中存在负权回路,可以果断结束 S P F A SPFA SPFA了。

c o d e code code:

queue<int> q;
int cnt[N]    用于统计每个节点进队次数 
void spfa(int s){
	for(int i=1;i<=n;i++)
		dis[i]=inf;
	q.push(s);
	mark[s]=true;
	dis[s]=0;
	while(q.size()){
		int x=q.front();
		q.pop();
		mark[x]=false;
		for(int i=1;i<=n;i++)
			if(dis[x]+Map[x][i]<dis[i]){
				dis[i]=dis[x]+Map[x][i];
				if(mark[i]==false){
					q.push(i);
					cnt[i]++
					if(cnt[i]==n){
						cout<<"有负权回路";
						return; 
					}
					makr[i]=true;
				}
			}
	}
}

floyd:计算每一对顶点的最短距离 (其实跟dp差不多)

算法思想:
枚举图中每一个顶点k,判断图中其他节点间的距离在经过k后是否缩短,如果是,则用新的更短距离取代原距离

f l o y d floyd floyd的数学表达式:
f [ i ] [ j ] = m i n f [ i ] [ k ] + f [ k ] [ j ] ( 1 ≤ i , j , k ≤ n ) f[i][j]=min{f[i][k]+f[k][j]} (1 \leq i,j,k \leq n) f[i][j]=minf[i][k]+f[k][j](1i,j,kn)

c o d e code code:

for(k=1;k<=n;k++)
    for(i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            if(Map[i][j]>Map[i][k]+Map[k][j])
            	Map[i][j]=Map[i][k]+Map[k][j];               

f l y o d flyod flyod的时间复杂度为 O ( n 3 ) ! ! ! ! ! O(n^3)!!!!! O(n3)!!!!!

总结:

  1. SPFA很容易被卡,但是在判断负权回路时很好写
  2. 在做最短路问题时,优先考虑dijkstra
  3. floyd纯属娱乐,很少有题用到
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值