搜索与图论之最短路算法
目录
2.memcpy()函数:memcpy_百度百科,memcpy函数效率很高
1.spfa算法实际上是对Bellman-Ford算法的一个优化
原理:对于一个n个点,m条边的有向图,如果到达一个点的最短路大于等于n,那么就一定存在负环。
1.朴素dijkstra算法
AC代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 510;
int g[N][N], dist[N]; //邻接矩阵,到节点i的最短路径
bool st[N]; //判断i点是否已经找到了最短路径
int n, m;
void djikstra()
{
memset(dist, 0x3f, sizeof dist); //初始化所有最短路径为最大值
dist[1] = 0; //以节点1为起点,所以到节点1的最短距离为0
for(int u = 1; u <= n; u ++ )
{
int k = -1;
for(int i = 1; i <= n; i ++ ) //找到当前状态下所有最短路径中最短的一个
if((k == -1 || dist[k] > dist[i]) && !st[i])
k = i;
st[k] = true; //标记这个节点的最短路径已经找到了
for(int i = 1; i <= n; i ++ ) //用这个最短路径更新所有最短路径
dist[i] = min(dist[i], dist[k] + g[k][i]);
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(g, 0x3f, sizeof g);
for(int i = 0; i < m; i ++ )
{
int a, b, c;
cin >> a >> b >> c;
g[a][b] = min(g[a][b], c); //通过这一步可以忽略掉重边并取得最小的边值
}
djikstra();
if(dist[n] == 0x3f3f3f3f) puts("-1"); //要判断是否有路
else cout << dist[n] << endl;
return 0;
}
2.堆优化版dijkstra算法
AC代码
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int N = 160010;
int h[N], ne[N], e[N], w[N], idx;
int dist[N];
bool st[N];
int n, m;
void add(int a, int b, int c)
{
w[idx] = c; //在赋权值的时候,要在idx++之前!
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void djikstra()
{
memset(dist, 0x3f, sizeof dist);
priority_queue<PII, vector<PII>, greater<PII> > heap;
heap.push({0,1}); //(距离,下标号)
dist[1] = 0;
while(heap.size()) //队列不为空
{
auto t = heap.top(); //取队头
heap.pop(); //出队
int ver = t.second, distance = t.first;
if(st[ver]) continue; //如果这个点已经确定了最短路径
st[ver] = true; //标记一下
for(int i = h[ver]; i != -1; i = ne[i])
{
int j = e[i]; //我们在邻接表存的是下标,通过e[i]得到顶点
if(dist[j] > distance + w[i]) //如果路径可以被优化
{
dist[j] = distance + w[i]; //更新路径
heap.push({dist[j], j}); //假如队列
}
}
}
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
while(m -- )
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
djikstra();
if(dist[n] == 0x3f3f3f3f) puts("-1");
else cout << dist[n] << endl;
return 0;
}
分析
- 使用优先级队列
在使用前需要先搞清楚优先级队列的时间复杂度,往一个长度为n的优先级队列中插入一个元素时间复杂度是O(logn)。那么把n个元素插入一个空的优先级队列中时间复杂度就为O(nlogn)。 - O(Log n)
如果在一个大小为n的循环中,循环变量是指数级的递增或递减,这个循环的复杂度就为O(Log n). - 堆优化,主要优化的是找当前状态下的最短的路径的时间,在朴素算法中,是通过一个for循环完成,时间复杂度是线性的,但是在堆优化版本中,使用优先队列可以在常数时间内取出这个最短的路径,即队头,不过需要注意的是维护优先队列的时间复杂度是为O(N)的
- 优先队列的实现方式通过一个pair,储存最短路和下标
为什么Dijkstra不能处理(有些)负权边
负权概念
不要把边权想象成距离,而是想象成代价(cost),就会好理解很多。
比如,cost(a, b)可以理解成开车从a到b的油耗。那么,如果a,b之间有一个加油站,油耗就可以是负的。
3.Bellman-Ford算法
AC代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 510, M = 10010;
int dist[N], backup[N]; //backup是dist的一个备份
int n, m, k;
struct Edge
{
int a, b, w;
}edge[M];
void Bellman_Ford()
{
//初始化
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for(int i = 0; i < k; i ++ ) //最大经过几条边就迭代几次
{
memcpy(backup, dist, sizeof dist);
for(int j = 0; j < m; j ++ ) //遍历所有边更新最短路
{
int a = edge[j].a, b = edge[j].b, w = edge[j].w;
dist[b] = min(dist[b], backup[a] + w); //每次更新到顶点b的边
}
}
}
int main()
{
cin >> n >> m >> k;
for(int i = 0; i < m; i ++ )
{
int a, b, w;
cin >> a >> b >> w;
edge[i] = {a, b, w};
}
Bellman_Ford();
if(dist[n] > 5000010) //一条边的最大距离
cout << "impossible" << endl;
else cout << dist[n] << endl;
return 0;
}
几点说明:
1.backup数组的作用:避免最短路串联
解释:如果我没不设置一个dist数组的拷贝backup数组,那么对于某些情况,例如样例:
3 3 1
1 2 2
1 3 4
2 3 1
如果K = 1,只迭代一次,那么dist[3]的距离应该是min(dist[3], dist[2] + 1),即min(4,正无穷),注意第一次更新时dist[2]的距离应该是正无穷而不是2,只要理解了这一点问题就迎刃而解了。
我们在第一次迭代的时候,事实上在更新dist[3]之前,我们已经把dist[2]更新成2了,那么再更新dsit[3],就有dist[3] = min(dist[3],dist[2] + 1) = min(3,4) = 3 ≠ 4,答案就错误了,错误的原因就在于我们把1->2和->3串联起来了,实际上1->2现在是没有路可走的。
因为我们不可能不更新dist数组,所以我们就要想办法得到一份dist的复制品
所以在每一次迭代的时候拷贝一份dist数组,在更新的时候使用backup数组就可以了,这样前面更新的值就不会影响后面的结果。
2.memcpy()函数:memcpy_百度百科,memcpy函数效率很高
3.为什么判断不存在通路的时候是dist[n] > 5000010而不是dist[n] == 0x3f3f3f3f
1节点无法走到n节点,不代表任意节点都走不到n节点,如果有节点可以走到n节点,n节点就可以被更新。
例如数据:
2 4
1 2 1
3 4 -2
此时图中只存在两条边(1,2)和(3,4),1节点是无法走到4节点的,但是3节点可以走到4节点
又因为(3,4)的权值为-2,所以4节点的最短路被更新为0x3f3f3f3f - 2。
4(1)spfa算法求最短路
AC代码
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
int h[N], e[N], ne[N], w[N], idx;
int dist[N], n, m;
bool st[N];
void add(int a, int b, int c) //邻接表存贮的都是顶点下标
{
w[idx] = c; //放在idx++前面
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void spfa()
{
//最短路初始化
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
//队列初始化,注意存放的是顶点而不是顶点下标
queue<int> q;
q.push(1);
st[1] = true; //标记入队
while(q.size())
{
auto t = q.front();
q.pop(); //队头取出来当然要删除
st[t] = false; //标记出队
for(int i = h[t]; i != -1; i = ne[i])
{
int j = e[i]; //得到顶点
if(dist[j] > dist[t] + w[i]) //区分这里下标j,t,i的含义
{
dist[j] = dist[t] + w[i];
if(!st[j]) //可以入队
{
q.push(j);
st[j] = true; //标记入队
}
}
}
}
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h); //邻接表初始化
for(int i = 0; i < m; i ++ )
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c); //邻接表不需要考虑重边和自环
}
spfa();
if(dist[n] == 0x3f3f3f3f) //不是dist[n] > INF / 2 !!!!!!!
cout << "impossible" << endl;
else cout << dist[n];
return 0;
}
几点说明:
1.spfa算法实际上是对Bellman-Ford算法的一个优化
在Bellman-Ford算法中,我们在更新边的时候是使用了一个for循环,但这个for循环中很多操作是多余的,因为很多边并没有更新甚至无法更新(不相连),这就降低了算法的时间效率,而spfa算法正是对Bellman-Ford算法中更新边这一步操作的一个优化,由边的更新公式 dist[j] = dist[t] + w[i];可以知道,如果dist[t]的值不发生改变,那么dist[j]的值也就不会发生改变,所以说J是T的后继节点,我们只需要在T更新的时候再更新所有T的后继节点就可以了。
2.区分顶点和顶点下标
基本上邻接表存储的都是顶点下标,而最短路径dist数组以及状态数组st,队列中的元素存储的都是顶点,不要搞混了。
3.如何判断最短路是否存在
在Bellman_Ford算法中,我们判断最短路存在与否用的是 dist[n] > INF / 2
在spfa算法中,我们用的是 dist == INF
这是因为spfa算法优化了Bellman_Ford算法中的一些无效优化操作
也体现spfa算法和djikstra算法的相似性
解释:st数组的作用
- 用来标记一下该顶点是否已经入队,防止重复入队,可以减少操作次数,提高效率,所以没有的话不影响答案的正确性只影响效率。
- 但是如果这只st数组,有队头出队的话,要再标记该顶点已经出队,否则就会出错
例如数据:
4 4
1 2 1
2 3 2
3 4 1
1 3 4
如果我们只标记元素入队而不标记元素出队的话的话,spfa流程是这样的:
初始化顶点1入队,st标记一下
只要队列不为空:
(1)队头(顶点1)出队,更新与顶点1相连的顶点2、3,更新距离:dist[2] = dist[1] + 1 = 1,dist[3] = dist[1] + 4 = 4。顶点2、3没有标记过,可以入队,标记顶点2、3。
(2)队头3出队,更新与顶点3相连的顶点4,更新距离:dist[4] = dist[3] + 1 = 5。顶点4没有标记过,入队,标记顶点4.
(3)顶点2出队,更新与顶点2相连的顶点3,更新距离:dist[3] = dist[2] + 2 = 3。因为顶点3已经标记过了,所以不再入队。
(4)队头4出队,没有相连边,退出。
(5)队列为空,结束。
当程序结束的时候,我们发现,dist[4] = 5,但其实dist[4]应该等于4
错误的原因就在于:顶点4的最短路应该是1->2->3->4,但这里是1->3->4,没有经过顶点2,说明dist[5]里面的dist[3]不是最短路径,所以dist[5]当然也不是最短路径,但最终dist[3]的结果又是正确的,这又是为什么呢?
因为我们使用的dist[3]是更新之前的dist[3],而不是更新过后的dist[3]。
在(2)(3)步骤中,我们先让3出队,直接更新了dist[4],但此时dist[3]不是最短路径,而3出队,2入队的时候,这时候dist[4]已经更新完了,我们才更新dist[3],又因为3已经入队过了,所以3不再入队,那么4就失去了更新的机会,导致答案出错。
不过如果把步骤(2)(3)颠倒一下,其实答案又是对的,但这显然是在瞎蒙。
综上所述:spfa算法中我们在设置st(state)数组时,不仅要标记元素入队,还要标记元素出队,让最短路径始终可以保持更新(即保证最短路径的子路径也是最优的)。
4(2)spfa算法判断负环
原理:对于一个n个点,m条边的有向图,如果到达一个点的最短路大于等于n,那么就一定存在负环。
AC代码
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
const int N = 10010;
int h[N], e[N], ne[N], w[N], idx;
int dist[N], cut[N]; //cut[i]用来存储从1到达顶点i走过的边数
int n, m;
bool st[N];
void add(int a, int b, int c)
{
w[idx] = c;
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
bool spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
for(int i = 1; i <= n; i ++ )
{
q.push(i);
st[i] = true;
}
while(q.size())
{
int t = q.front();
q.pop();
st[t] = false;
for(int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if(dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cut[j] = cut[t] + 1;
if(cut[j] >= n) return true;
if(!st[j])
{
st[j] = true;
q.push(j);
}
}
}
}
return false;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
for(int i = 0; i < m; i ++ )
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
if(spfa()) cout << "Yes" << endl;
else cout << "No" << endl;
return 0;
}
几点说明:
- 初始化队列时不能只放入顶点1,因为我们的最短路不一定是从点1开始的,其他顶点同理,所以初始化时要把所有顶点都放入队列
- 整体上是spfa找最短路算法的基础上加了一个cut数组,以及多了一步判断
5.Flord算法
思想(动态规划):对于每一个顶点v,遍历所有边(i,j),判断顶点v是否可以更新边(i,j)为(i,k,j)
AC代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 210, INF = 1e9;
int g[N][N];
int n, m, q;
void floyd()
{
for(int v = 1; v <= n; v ++ ) //遍历所有顶点
for(int i = 1; i <= n; i ++ )
for(int j = 1; j <= n; j ++ )
g[i][j] = min(g[i][j], g[i][v] + g[v][j]);
}
int main()
{
cin >> n >> m >> q;
for(int i = 1; i <= n; i ++ )
for(int j = 1; j <= n; j ++ )
if(i == j) g[i][j] = 0; //删除自环(权值等于0没有影响)
else g[i][j] = INF;
for(int i = 0; i < m; i ++ )
{
int a, b, c;
cin >> a >> b >> c;
g[a][b] = min(g[a][b], c); //如果有重边就只选最短的那一条
}
floyd();
while(q -- )
{
int a, b;
cin >> a >> b;
if(g[a][b] > INF / 2) cout << "impossible" << endl;
else cout << g[a][b] << endl;
}
return 0;
}
说明:
- 首先通过数据范围可以看出这是一个稀疏图,所以用稀疏矩阵存储边的数据
- 因为题目说明会有重边和自环,对于重边我们只要选取最短的那一条就行了,对于自环我们在初始化为0(加或减0对最短路没有影响),就相当于删除自环了。
- 最后在判断有没有最短路的时候还是不能用等于号。具体的看Bellman-Ford算法的说明3