最短路径
源点(source)到终点(destination)所有路径中权值和最少的一条路径
- 非连通图和带负值圈的连通图不存在最短路径
Floyd
基于动态规划,不能处理负值圈
适合多源最短路径,可以处理负边,不能处理负值圈,基于动态规划
可以判断两个点之间是否连通
- 最短路问题具有重叠子问题性质:若s到x的最短路径为s->a->b->x,则可以确定s到b的最短路径一定是s->a->b
动态规划:
- 状态:distance[i][j]表示从i节点到j节点的最短距离
- 状态转移:设有一个中间节点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[]标记节点是否在队列中
- 源点
s入队,标记inQueue[s]=true - 循环出队,将出队的节点标记为未在队列中,尝试更新邻接点,若成功更新邻接点,且邻接点未入队,则将邻接点入队,入队状态改为
true - 队列为空退出循环
#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说明存在负值圈

浙公网安备 33010602011771号