这是图论中的常见问题,有三种常用算法,以及许多拓展内容。
对于每条有向边 ( u , v , w ) (u,v,w) (u,v,w),令 d i s ( v ) = m i n ( d i s ( v ) , d i s ( u ) + w ) dis(v) = min(dis(v), dis(u) + w) dis(v)=min(dis(v),dis(u)+w)。这是最短路算法的核心操作。
适用于非负权图求解单源最短路径。
1、初始化 d i s ( dis( dis(源点 ) = 0 ) = 0 )=0,其余结点 d i s dis dis为 I N F INF INF。
2、找到一个未被标记的, d i s ( u ) dis(u) dis(u)最小的结点 u u u,然后标记 u u u。
3、扫描 u u u的出边 ( u , v , w ) (u,v,w) (u,v,w),进行松弛。
4、重复上述步骤,直到所有结点都被标记。
设当前被选中的结点为 u u u,当所有边权非负时, d i s ( u ) dis(u) dis(u)不可能被其他结点更新,故每次选出的结点 u u u一定满足 d i s ( u ) dis(u) dis(u)为从源点到 u u u的最短路径。
void Dijkstra(int s) {
memset(dis, 0x3f, sizeof(dis));
memset(vis, false, sizeof(vis));
dis[s] = 0;
for (int j = 1, u; j < n; ++j) {
u = 0;
for (int i = 1; i <= n; ++i)
if (!vis[i] && dis[i] < dis[u]) u = i;
vis[u] = true;
for (int i = G[u], v, w; i != 0; i = e[i].nxt) {
v = e[i].v, w = e[i].w;
dis[v] = min(dis[v], dis[u] + w);
}
}
}
上面这段代码的时间复杂度为 O ( n 2 ) O(n^2) O(n2),瓶颈在于查询全局最小值。可以使用线段树,堆,平衡树等数据结构优化复杂度。这里给出堆优化的代码。
void Dijkstra(int s) {
// first 为 dis 值,second 为点的编号
static priority_queue<pair<int, int>,
vector<pair<int, int>, greater<pair<int, int> > > Q;
memset(dis, 0x3f, sizeof(dis));
memset(vis, false, sizeof(vis));
dis[s] = 0;
Q.push(make_pair(0, s));
while (!Q.empty()) {
int u = Q.front().second; Q.pop();
if (vis[u]) continue;
vis[u] = true;
for (int i = G[u]; i != 0; i = e[i].nxt) {
v = e[i].v, w = e[i].w;
dis[v] = min(dis[v], dis[u] + w);
Q.push(make_pair(dis[v], v));
}
}
}
堆优化Dijkstra的时间复杂度为 O ( m log n ) O(m\log n) O(mlogn)。
适用于求解单源最短路径。
对于一张图,若每条边 ( u , v , w ) (u,v,w) (u,v,w)都满足 d i s ( v ) < = d i s ( u ) + w dis(v)<=dis(u)+w dis(v)<=dis(u)+w,则此时的 d i s dis dis就是所求最短路。
1、扫描每条边,进行松弛。
2、重复上步骤,知道没有松弛操作为止。
以上便是Bellman Ford算法
时间复杂度 O ( n m ) O(nm) O(nm)。
考虑到上面的过程中,每个点的 d i s dis dis即使没有被更新,也会扫描其出边。于是可以用队列保存要更新的结点,避免无效的扫描。
void SPFA() {
static queue<int> Q;
memset(dis, 0x3f, sizeof(dis));
memset(inQ, false, sizeof(inQ));
dis[s] = 0;
Q.push(s), inQ[s] = true;
while (!Q.empty()) {
int u = Q.front();
Q.pop(), inQ[u] = false;
for (int i = G[u], v, w; i != 0; i = e[i].nxt) {
v = e[i].v, w = e[i].w;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
if (!inQ[v]) Q.push(v), inQ[v] = true;
}
}
}
}
时间复杂度还是 O ( n m ) O(nm) O(nm)。。。
但是在随机数据的稀疏图下效率极高,接近线性。
不过可以被特殊数据卡掉,所以非负权图还是尽量用Dijkstra。
用于求解多源最短路径。
Floyd 基于 DP,用 d i s ( k , i , j ) dis(k,i,j) dis(k,i,j)表示经过若干个编号不超过 k k k的结点时从 i i i到 j j j的最短路径。于是可以写出转移
d i s ( k , i , j ) = m i n ( d i s ( k − 1 , i , j ) , d i s ( k − 1 , i , k ) + d i s ( k − 1 , k , j ) ) dis(k,i,j)=min(dis(k-1,i,j),dis(k-1,i,k)+dis(k-1,k,j)) dis(k,i,j)=min(dis(k−1,i,j),dis(k−1,i,k)+dis(k−1,k,j))
与背包问题类似, k k k这一维度实际上进行了一个总 d i s ( k − 1 ) dis(k-1) dis(k−1)到 d i s ( k ) dis(k) dis(k)的拷贝,所以可以用滚动数组优化空间。
for (int k = 1; k <= n; ++k)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
时间复杂度 O ( n 3 ) O(n^3) O(n3),在稠密图上效率较高。
稀疏图的最短路可以转化为 n n n个结点的单源最短路径求解。
当在原图中加入一条边或一个点后,最短路可能会更新。如果每加一条边或一个点都重新跑一次 Floyd,时间复杂度可能难以接受。
考虑到 Floyd 基于 DP 实现,所以在更新时没有必要全局更新,只需更新所加的边或点可能影响的 d i s ( i , j ) dis(i,j) dis(i,j)。
加边
// (u,v,w)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
dis[i][j] = min(dis[i][j], dis[i][u] + w + dis[v][j]);
加点
// k
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
定理: 当一张图中存在负环时,最短路无解。
判定负环可以用SPFA。
用 l e n ( u ) len(u) len(u)表示一个点的最短路上经过了几个点。显然当 l e n ( u ) > = n len(u)>=n len(u)>=n时,原图存在负环。
bool SPFA() {
// 存在负环时返回 false
static queue<int> Q;
memset(dis, 0x3f, sizeof(dis));
memset(inQ, false, sizeof(inQ));
memset(len, 0, sizeof(len));
dis[s] = 0, len[s] = 1;
Q.push(s), inQ[s] = true;
while (!Q.empty()) {
int u = Q.front();
Q.pop(), inQ[u] = false;
for (int i = G[u], v, w; i != 0; i = e[i].nxt) {
v = e[i].v, w = e[i].w;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
if (!inQ[v]) {
len[v] = len[u] + 1;
if (len[v] >= n) return false;
Q.push(v), inQ[v] = true;
}
}
}
}
return true;
}
先跑一遍Floyd,求出最短路。
然后求最小的 d i s ( i , j ) + d i s ( j , k ) + d i s ( k , i ) dis(i,j)+dis(j,k)+dis(k,i) dis(i,j)+dis(j,k)+dis(k,i)。
枚举每个 s s s为源点,将所有 ( u , v ) (u,v) (u,v)中的 v v v以 d i s ( v ) = w ( u , v ) dis(v)=w(u,v) dis(v)=w(u,v)加入堆中。然后跑一遍Dijkstra,得出的 d i s ( s ) dis(s) dis(s)就是通过 s s s的最小环长度。
图论中一种重要的模型。
对于一张图,设 G G G为其邻接矩阵,则 G k G^k Gk中, G k ( u , v ) G^k(u,v) Gk(u,v)表示从 u u u到 v v v路径长度为 k k k的路径条数。