20250812 单源最短路总结

引入

最短路问题是一类经典的图论问题,主要是求解有权图上两点间的最短距离。

我们今天主要的内容是dij和bf两种单源最短路算法。

树有无环、联通的特点,所以任意两点间简单路径唯一,以起点作为根节点DFS/BFS求距离即可。

无(等)权图

无权图上每条边的边权相同,所以bfs即可找到从起点出发到任意一点的最短距离。

非负权图

对于非负权图,我们一般使用dijkstra算法求解单源最短路。

过程

dij是一种点核心的单源最短路算法,核心思想是:

1、找到没有确定最短路的点里面,距离起点最近的点u。

2、用起点到点u的距离dis[u],尝试更新u的邻居们的最短距离。

重复以上操作直到所有点的最短距离都被确定。

dis[i]dis[i]dis[i] 表示当前情况下从起点出发到iii的最短距离,假设有一条从uuuvvv,边权为www的边,那么我们就可以尝试用dis[u]+wdis[u] + wdis[u]+w来更新dis[v]dis[v]dis[v],这个操作就叫松弛

证明

记从起点到点u的最短路为D(u)D(u)D(u),如果我们每次确定的点当时的dis[u]=D(u)dis[u] = D(u)dis[u]=D(u),那么最终所有的dis[i]=D(i)dis[i] = D(i)dis[i]=D(i)。证明这一点,就可以证明dij算法的正确性。

使用反证法证明:

假设存在若干个点被确定时disdisdis大于DDD,记其中第一个被确定的点为xxx

xxx显然不为起点,因为dis[st]=D(st)=0dis[st] = D(st) = 0dis[st]=D(st)=0

假设到xxx最短路径st→a→b→xst \rightarrow a \rightarrow b\rightarrow xstabx,一定存在这条路径,否则dis[x]=D(x)=INFdis[x] = D(x) = INFdis[x]=D(x)=INF

其中bbbsssxxx的最短路径上第一个未被确定的点

如果路径上所有点都被确定,就有dis[x]=D(x)dis[x] = D(x)dis[x]=D(x),所以路径上一定有尚未被确定的点。

aaastststbbb的最短路径上的前驱节点

因为bbb是第一个未被确定的点,所以aaa一定被确定。(aaa可能等于ststst

因为xxx是第一个disdisdis大于DDD的点,所以已经被确定的aaa满足dis[a]=D(a)dis[a] = D(a)dis[a]=D(a)

因为aaa是到bbb的最短路径上的前驱节点,所以D(b)=dis[b]=D(a)+w(a,b)D(b) = dis[b] = D(a) + w(a, b)D(b)=dis[b]=D(a)+w(a,b)

因为xxx是被确定的点,所以此时dis[x]dis[x]dis[x]应当小于等于所有未被确定的点的disdisdis

又有D(x)=D(b)+w(b,x)D(x) = D(b) + w(b, x)D(x)=D(b)+w(b,x),其中w(b,x)>=0w(b, x) >= 0w(b,x)>=0

综上,dis[x]>D(x)=D(b)+w(b,x)>=D(b)=dis[b]dis[x] > D(x) = D(b) + w(b, x) >= D(b) = dis[b]dis[x]>D(x)=D(b)+w(b,x)>=D(b)=dis[b]

推出矛盾,所以不存在点xxx被确定时dis[x]>D[x]dis[x] > D[x]dis[x]>D[x],原结论成立。

实现

朴素dij

vector <int> dis, vis;
vector <pair<int, int> > ve[N];
void dij(int st) {
    dis = vector<int>(n + 1, INF);
    vis = vector<int>(n + 1, 0);
    dis[st] = 0;
    for (int _i = 0; _i < n; _i++) {
        int id = 0;
        for (int i = 1; i <= n; i++) {
            if (vis[i] == 0 && dis[i] < dis[id]) {
                id = i;
            }
        }
        if (id == 0) return;
        vis[id] = 1;
        for (auto p : ve[id]) {
            int nxt = p.first, w = p.second;
            dis[nxt] = min(dis[nxt], dis[id] + w);
        }
    }
}

堆优化dij

vector <int> dis;
vector <pair<int, int> > ve[N];
void dij(int st) {
    dis = vector<int>(n + 1, INF);
    dis[st] = 0;
    priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > q;
    q.push({0, st});
    while (q.size()) {
        int d = q.top().first;
        int now = q.top().second;
        q.pop();
        if (dis[now] < d) continue;
        for (auto p : ve[now]) {
             int w = p.first, nxt = p.second;
             if (dis[nxt] > dis[now] + w) {
                 dis[nxt] = dis[now] + w;
                 q.push({dis[nxt], nxt});
             }
        }
    }
}

他们的时空复杂度分别是多少?

朴素 时间O(n2+m)O(n^2+m)O(n2+m)

堆优化 时间O((n+m)logm)O((n+m)logm)O((n+m)logm)

所以什么情况下用朴素dij,什么情况下用堆优化dij?

稀疏图 堆优化dij

稠密图 朴 素 dij

一般图

如果图上有负权边,dij算法的正确性就不能保证了,这时我们可以使用bellman-ford算法来求解单源最短路,我们常说的SPFA算法就是bellman的队列优化

过程

bellman算法是一种边核心的最短路算法。

和点核心的dij不同,bellman在松弛时不是以点作为单位来松弛,而是在一轮松弛中依次松弛所有边,直到找到所有点的最短路(或发现有负环)为止。

松弛操作与dij中相同,假设有一条从uuuvvv,边权为www的边,那么我们就可以尝试用dis[u]+wdis[u] + wdis[u]+w来更新dis[v]dis[v]dis[v]

在每一轮循环中,我们尝试用每一条边更新这条边的终点的最短路,复杂度O(m)O(m)O(m)

如果起点不能到达负环,存在最短路,那么最短路最多包括n−1n-1n1条边

每轮松弛都会使最短路至少多一条边。

所以最多需要进行n−1n-1n1轮循环,最终复杂度O(nm)O(nm)O(nm)

如果需要判断是否存在负环,比较简单的做法是新加一个超级源点S,从S出发建nnn条边权为000的边连向其他nnn个节点,然后以S作为起点执行bellman。

实现

vector <array<int, 3> > path;
bool bellman(int st) {
    dis[st] = 0;    
    for (int i = 1; i <= n; i++) {
        bool ed = 1;
        for (auto p : path) {
            int u = p[0], v = p[1], w = p[2];
            if (dis[u] == inf) continue;
            if (dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w;
                ed = 0;
            }
        }
        if (ed) return 0;
    }
    return 1;
}

SPFA

SPFA是基于bellman算法的队列优化,在随机图和一些有特殊性质的图上表现优秀,但复杂度上界和bellman相同,也是O(nm)O(nm)O(nm)

SPFA是基于这样一种思想:只有上一次被松弛过的终点,才有作为起点松弛的价值。所以可以用一个队列存被松弛过的终点的集合,每次取出队头松弛该点的出边。

bool spfa(int st) {
    queue<int> q;
	q.push(st);
	for (int i = 1; i <= n; i++) dis[i] = inf, vis[i] = 0;
	dis[st] = 0;
	cnt[st] = 0;
	vis[st] = 1;
	while(q.size()) {
		int now = q.front(); q.pop();
		vis[now] = 0;
		for (auto p : ve[now]) {
			int nxt = p.first, w = p.second;
			if (dis[nxt] > dis[now] + w) {
				dis[nxt] = dis[now] + w;
				cnt[nxt] = cnt[now] + 1;
				if (cnt[nxt] >= n) {
					return 1;
				}
				if (!vis[nxt]) {
					q.push(nxt);
					vis[nxt] = 1;
				}
			}
		}
	}
	return 0;
}

每个点的入队次数是O(度) 的,复杂度上界仍然是O(nm)O(nm)O(nm)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值