最短路径

源点(source)到终点(destination)所有路径中权值和最少的一条路径

  • 非连通图和带负值圈的连通图不存在最短路径

Floyd

基于动态规划,不能处理负值圈

适合多源最短路径,可以处理负边,不能处理负值圈,基于动态规划
可以判断两个点之间是否连通

  • 最短路问题具有重叠子问题性质:若s到x的最短路径为s->a->b->x,则可以确定s到b的最短路径一定是s->a->b

动态规划

  1. 状态:distance[i][j]表示从i节点到j节点的最短距离
  2. 状态转移:设有一个中间节点k,节点i到j可以经过k节点,所以distance[i][j]=min(distance[i][j],distance[i][k]+distance[k][j])
  • distance二维数组表示从i到j的最短路径,按图中的边初始化(自己到自己为0,到邻接点为对应权值,其他为INF),path二维数组表示从i到j的路径中j的前驱
  • 中间节点k有graph->vertexNum种情况,所以需要循环graph->vertexNum次,每次以某个节点为中间节点遍历并更新diatance,每次更新状态后需要更新path[i][j]为j的前缀
int distance[MaxNum][MaxNum];
int path[MaxNum][MaxNum];

void Floyd(MGraph* graph){
	//初始化
	for(int i=0;i<graph->vertexNum;i++){
		for(int j=0;j<graph->vertexNum;j++){
			//初始化distance
			distance[i][j]=graph->edges[i][j];
			//初始化path
			if(distance[i][j]!=INF&&distance[i][j]!=0){
				path[i][j]=i;
			}
			else path[i][j]=-1;
		}
	}
	//更新
	for(int k=0;k<graph->vertexNum;k++){//所有可能的中间节点
		//遍历distance
		for(int i=0;i<graph->vertexNum;i++){
			for(int j=0;j<graph->vertexNum;j++){
				//满足更新条件
				if(distance[i][k]+distance[k][j]<distance[i][j]){
					distance[i][j]=distance[i][k]+distance[k][j];
					path[i][j]=path[k][j];
				}
			}
		}
	}
}
  • 时间复杂度O(\(V^3\))

Dijkstra

基于贪心,不能处理负边

适合有权图的单源最短路径,不能处理负边,不能处理负值圈,基于贪心

需要维护一个distance数组,表示源点通过已收录的顶点到每个节点的最短路径,初始为无穷,一个path数组,表示原点到每个节点最短路径中该节点的上一个节点,初始化为-1,一个collected数组,表示是否被收录(确定了最短路径),初始化为false,每次迭代:

  • 从未收录的顶点中找一个distance最小的顶点,收录该顶点,collected改为true,distance可以用最小堆存
  • 收录该顶点后可能会影响该顶点的邻接点的distance,所以需要检查并更新该顶点的邻接点,若某个邻接点和该点的边的权值加上该点的distance小于这个邻接点原来的distance,说明这个邻接点的最短路径需要更新,修改distance和这个点的path
int distance[MaxNum];
int path[MaxNum];
bool collected[MaxNum];

//初始化collected数组
void initCollected(bool collected[],MGraph* graph){
	memset(collected,0,sizeof graph->vertexNum);
}

void Dijkstra(MGraph* graph,int s){
	//初始化dis和path
	for(int i=0;i<graph->vertexNum;i++){
		dis[i]=INFI;
		path[i]=-1;
	}
	dis[s]=0;
	int min;    //最小值
	int m;    //最小值对应的下标
	for(int i=1;i<=graph->vertexNum;i++){
		//找最小的distance
		min=INFI;
		for(int j=0;j<vertexNum;j++){
			if(collected[j]==false&&distance[j]<min){
				min=distance[j];
				m=j;
			}
		}
		//收录distance最小的顶点
		collected[m]=true;
		//检查并更新其未收录的邻接点
		for(int j=0;j<graph->vertexNum;j++){
			if(collected[j]==false&&distance[m]+graph->nodes[m][j]<distance[j]){
				distance[j]=distance[m]+graph->nodes[m][j];
				path[j]=m;
			}
		}
	}
}
  • 时间复杂度O(\(V^2\))

输出路径

  • 输出路径类似于findRoot,沿着路径中的上一个节点直到走到上一个节点为-1的节点,利用stack把逆序的路径顺序输出
void showPath(const int path[],int dest){
	stack<int> s;    //栈
	int p=dest;
	while(p!=-1){
		s.push(p);
		p=path[p];
	}
	while(!s.empty()){
		p=s.top();
		s.pop();
		cout<<p<<endl;
	}
}

最短路径条数

  • num[]数组存每个节点从源点开始的最短距离路径条数
  • 最开始num[start]初始化为1
  • 更新dist时:
    • dist[k]+graph.edges[k][i]<dis[i],则不仅要更新dist,还要更新num[i]=num[k]
    • dist[k]+graph.edges[k][i]==dis[i]num会增加,即num[i]+=num[k]

优化

  • 每次找未收录的点中最小的dis,直接枚举所有点复杂度为O(V),用小顶堆存未收录的边,时间复杂度为O(logV)
  • 由于未收录的点的dis可能会更新,所以一个点的dis可能会重复入队,出队后要判断该点是否已收录
#define INFI 1e8
typedef pair<int,int> PII;
priority_queue<PII,vector<PII>,greater<PII>> q;
int dis[MaxNum];
bool selected[MaxNum];

void Dijkstra(int s){
	for(int i=1;i<=n;i++)    //initialize dis
		dis[i]=INFI;
	dis[s]=0;
	q.push({dis[s],s});
	while(!q.empty()){
		auto a=q.front();
		q.pop();
		if(select[a.second])continue;
		selected[a.second]=true;
		for(int i=head[a.second];i!=-1;i=e[i].next){    //forward start
			int u=e[i].to;
			int w=e[i].w;
			if(!selected[u]&&a.first+w<dis[u]){
				dis[u]=dis[a.second]+w;
				q.push({dis[u],u});
			}
		}
	}
}

时间复杂度O(VlogV)

Bellman-Ford算法

Floyd算法和Dijkstra算法核心都是找一个中间节点,尝试缩小源点和目标节点的距离,即松弛(relax),Bellman-Ford算法对所有边暴力松弛
求单源最短路径,可以处理负边,不能处理负值圈

松弛

对一条边进行松弛操作:
设边的权值为w[i],边的起点为u,指向v

if(dis[u]+w[i]<dis[v]){
	dis[v]=dis[u]+w[i];
}

若是无向图,需要再以v为起点,指向u,反过来再进行一次松弛

if(dis[v]+w[i]<dis[u]){
	dis[u]=dis[v]+w[i];
}

暴力循环

  • 每轮循环遍历所有边,对所有边执行relax,当一轮循环没有一次成功的relax时退出循环
  • 由于一轮循环至少能确定一个点的最短路,所以最多循环n-1次(n为节点数),或者说,离起点最远的节点最多相离n-1条边,所以最多循环n-1次可以确定最远节点的最短路

实现

#define INFI 1e8

void Ford(int s){
	for(int i=1;i<=n;i++){
		dis[i]=INFI;
		pre[i]=-1;    //最短路径上的前驱节点
	}
	dis[s]=0;    //起点
	for(int i=1;i<=n-1;i++){    //最多循环n-1次
		bool isRelaxed=false;
		for(int j=1;j<=e;j++){    //遍历所有边
			int u=edges[j].u;
			int v=edges[j].v;
			int w=edges[j].w;
			if(dis[u]+w[j]<dis[v]){
				dis[v]=dis[u]+w[j];
				pre[v]=u;
				isRelaxed=true;
			}
		}
		if(!isRelaxed)break;    //一轮循环没有边成功松弛直接退出
	}
}
  • 时间复杂度O(E*V)

判断负值圈

若图中有负值圈,则无论执行多少次循环,每次循环都会有边被松弛,所以循环第n次若有边被松弛则说明有负值圈,反之则没有负值圈

#define INFI 1e8

bool Ford(int s){
	for(int i=1;i<=n;i++){
		dis[i]=INFI;
		pre[i]=-1;    //最短路径上的前驱节点
	}
	dis[s]=0;    //起点
	for(int i=1;i<=n;i++){    //最多循环n-1次
		bool isRelaxed=false;
		for(int j=1;j<=e;j++){    //遍历所有边
			int u=edges[j].u;
			int v=edges[j].v;
			int w=edges[j].w;
			if(dis[u]+w[j]<dis[v]){
				dis[v]=dis[u]+w[j];
				pre[v]=u;
				isRelaxed=true;
			}
		}
		if(!isRelaxed)break;    //一轮循环没有边成功松弛直接退出
		if(i==n&&isRelaxed){
			cout<<"有负值圈"<<endl;
			return false;
		}
	}
	return true;
}

优化(SPFA算法

下一轮一定可以更新dis的点一定是上一轮更新了dis的点的邻接点,其他边不一定可以松弛,所以每轮循环只需要对上一轮更新了dis的点的出边执行松弛操作

实现

  • bool inQueue[]标记节点是否在队列中
  1. 源点s入队,标记inQueue[s]=true
  2. 循环出队,将出队的节点标记为未在队列中,尝试更新邻接点,若成功更新邻接点,且邻接点未入队,则将邻接点入队,入队状态改为true
  3. 队列为空退出循环
#define INFI 2e9
bool inQueue[MaxNum];
queue<int> q;

void spfa(int s){
	for(int i=1;i<=n;i++){
		dis[i]=INFI;
	}
	dis[s]=0;
	q.push(s);    //源点入队
	inQueue[s]=true;
	while(!q.empty()){
		int a=q.frnot();
		q.pop();
		inQueue[a]=false;
		for(int i=head[a];i!=-1;i=e[i].next){
			int u=e[i].to;
			int w=e[i].w;
			if(dis[a]+w<dis[u]){
				dis[u]=dis[a]+w;
				if(!inQueue[u]){
					q.push(u);
					inQueue[u]=true;
				}
			}
		}
	}
}

时间复杂度不确定

判断负值圈

方案1:sum[i]记录边i成功松弛次数(若有重边,则记录入队次数),当sum[i]==n说明存在负值圈
方案2:cur[i]记录点i到源点s路径的边数,当cur[i]==n说明存在负值圈

posted @ 2026-04-04 09:58  mofei1116  阅读(12)  评论(0)    收藏  举报