先说句关于二分图的问题。今天看书发现二分图判定可以使用dfs,然而在做题时惊奇的发现,一道二分图的题,正是前几天做的并查集的题。因此,得出一个未必正确的结论:二分图判断既可以使用DFS,也可以使用并查集。
接下来系统地整理几种最短路的方法。
1.Floyd-Warshall
解决:全局最短路
不能使用情况:带有“负权环”的图
存储方式:邻接矩阵
时间复杂度:O(v^3)
空间复杂度:O(v^2)
算法思想:Floyd算法是一个经典的动态规划算法。从i到j只有直接到达和经过中间点k两种情况。
PS:为什么Floyd的循环顺序是 k i j?
S:Floyd算法的本质是DP,而k是DP的阶段,因此要写最外面。
f[k][i][j]表示i和j之间可以通过编号为1…k的节点的最短路径。
初值f[0][i][j]为原图的邻接矩阵,f[k][i][j] = min(f[k-1][i][j] , f[k-1][i][k]+f[k-1][k][j])
f最外层一维空间可以省略,因为f[k]只与f[k-1]有关。
void floyd(){
for(int k = 1; k <= n; k++) //遍历所有中间点
for(int i = 1; i <= n; i++) //遍历所有起点
for(int j = 1; j <= n; j++) //遍历所有终点
if(f[i][j] > f[i][k] + f[k][j]) //不断更新i到j的最短路
f[i][j] = f[i][k] + f[k][j];
}
2.Dijkstra算法
解决:单源最短路
不能使用情况:图中含有负权边
最初时间复杂度:O(V*V+E)
源点可达:O(V*lgV+E*lgv) => O(E*lgV)
稀疏图:O(V^2)
用斐波那契堆实现优先队列:O(V*lgV+E)
算法思想:本质是贪心的思想。每次在剩余节点中找到离起点最近的节点放到队列中,并用来更新剩下的节点的距离,再将它标记上表示已经找到到它的最短路径,以后不用更新它了。
1 )Dijkstra+邻接矩阵 O(V^2)+路径输出
int e[maxn][maxn]; //邻接矩阵
int d[maxn]; //从起点到某一点的最短距离
bool v[maxn]; //标记是否在集合S中
int p[maxn]; //保存路径
int n; //顶点数
void dijkstra(){
fill(d, d + n, INF);
fill(v, v + n, 0);
fill(p, p + n, -1);
d[s] = 0;
while(1){
int u = -1;
for(int j = 0; j < n; j++)//寻找一个距离最近的没有使用过的点
if(!v[j] && (u == -1 || d[j] < d[u]))
u = j;
if(u == -1) break; //所有的点都被使用过了,就break
v[u] = 1; //将最近点u放入集合s中
for(int j = 0; j < n; j++){ //在集合s中放入u后,对于每个点再次维护
d[j] = min(d[j], d[u] + e[j][u]);
p[j] = u;
}
}
}
//获取起始点到顶点t的最短路径
vector<int> Getpath(int t){
vector<int> path;
while(t != 1){
path.push_back(t);
t = p[t];
}
reverse(path.begin(), path.end());
return path;
}
2 )Dijkstra+邻接表+堆优化
struct edge{
int to, w;
}
vector<edge> G[maxn];
typedef pair<int, int> P;
int d[maxn]; //从起点到某一点的最短距离
int n; //顶点数
void dijkstra(int s){
priority_queue<P, vector<P>, greater<P> > q;
fill(d, d + n, INF);
d[s] = 0;
q.push(P(s, 0)); //加源点入最小堆
while(!q.empty()){
P p = q.top; //取出堆顶的点,也就是距离最小的点
q.pop();
int v = p.first;
if(d[v] < p.second) continue; //加入队列前更新过,就不必再更新
for(int i = 0; i < G[v].size(); i++){
edge e = G[v][i];
if(d[e.to] > d[v] + e.w){ //能更新其他点,被更新的点加入队列
d[e.to] = d[v] + e.w;
q.push(P(e.to, d[e.to]));
}
}
}
}
3.Bellman-Ford算法
适用范围:带负权图单源最短路
缺点:效率低
时间复杂度:O(VE)
算法思想:连续进行松弛,在每次松弛时把每条边都更新一下,若在n-1次松弛后还能更新,则说明有负环。
优化:SPFA算法(队列优化)
int u[maxn], v[maxn], w[maxn];//从顶点u[i]到顶点v[i]这条边的权值为w[i]
for(int k = 1; k <= n - 1; k++)
for(int i = 1; i <= m; i++)
d[v[i]] = min(d[v[i]], d[u[i]] + w[i]);
//检测负权回路
bool flag = 0;
for(int i = 1; i <= m; i++)
if(d[v[i]] > d[u[i]] + w[i]) flag = 1;
if(flag) printf("含有负权回路\n");
4.SPFA算法
适用范围:单源最短路,可判负环
时间复杂度:O(kE)
缺点:效率不稳定,最好情况每个结点只入队一次,变为BFS,复杂度为O(E);最坏情况每个结点入队(V-1)次,退化为Bellman-ford算法,复杂度为O(VE).
原理:在Bellman-ford基础上加上队列优化
1)SPFA+手动实现队列
void spfa(int s){ //求单源点s到其他顶点的最短距离
fill(d, d + n, INF);
d[s] = 0;
v[s] = 1;
q[1] = s;
int u, head = 0, tail = 1;
while(head < tail){ //队列非空
head++;
u = q[head]; //取队首元素
v[u] = 0; //释放队首元素,可能还被用来松弛其他结点
for(int i = 0; i <= n; i++)
if(e[u][i] > 0 && d[i] > d[u] + e[u][i]){
d[i] = d[u] + e[u][i];
if(!v[i]){
tail++;
q[tail] = i;
v[i] = 1;
}
}
}
}
2)SPFA+邻接矩阵+BFS
//特点:判负环不稳定
bool spfa_bfs(int s){
queue<int> q;
fill(d, d + n, INF);
fill(t, t + n, 0);
fill(v, v + n, 0);
d[s] = 0;
q.push(s);
v[s] = 1;
t[s] = 1;
while(!q.empty()){
int u = q.front();
q.pop();
v[u] = 0;
for(int i = 0; i <= n; i++){
if(d[i] > d[u] + e[u][i]){
d[i] = d[u] + e[u][i];
if(!v[i]){
q.push(i);
v[i] = 1;
t[i]++;
if(t[i] > n)
return false;
}
}
}
}
return true;
}
3)SPFA+邻接矩阵+DFS
//DFS判负环很快
bool spfa_dfs(int u){
v[u] = 1;
for(int i = 0; i <= n; i++){
if(d[i] > d[u] + e[u][i]){
d[i] = d[u] + e[u][i];
if(v[i]) return 1;
else if(spfa_dfs(i)) return 1;
}
}
v[u] = 0;
return 0;
}
例题:
POJ 3255
次短路问题
到某个顶点v的次短路要么是到其他某个顶点u的最短路再加上u->v的边,要么是到u的次短路加上u->v的边,因此求到所有顶点的最短路和次短路。用Dijkstra+heap,用dis1[]和dis2[]保存最短路和次短路即可。