这里写目录标题
- 单源最短路
- 边权都为正数
- Dijkstra 算法朴素版
- Dijkstra 算法堆优化版
- 存在负权边
- bellman-ford算法,时间复杂度 O(nm)
- SPFA算法,时间复杂度O(m)- O(nm)
- 输出最短路径
树是一种特殊的图,与图的存储方式相同。
对于无向图中的边ab,存储两条有向边a->b, b->a。
图的存储:
稠密图 接矩阵:g[a][b] 存储边a->b
稀疏图 邻接表:
对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
int h[N], e[N], ne[N], idx;
// 添加一条边a->b
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
// 初始化
idx = 0;
memset(h, -1, sizeof h);
单源最短路
边权都为正数
Dijkstra 算法朴素版
当图为稠密图的时候用朴素Djkstra算法,时间复杂是 O(n2+m)O(n2+m), n 表示点数,m 表示边数
Dijkstra算法应用了 贪心 的思想
程序主要是维护两个集合,一个是已经确定最短路径的结点集合st,另一个是向外扩散的结点集合
大体过程如下:
(1)设起点为1,dist【i】数组是从1到i结点的最短距离
(2 遍历所有的结点,在还没找到最短路径的点的集合中找出距离当前结点最近的结点t
(3)用结点t来更新其他的结点
(4)将找到最短路的结点t放入st集合中
更新过程(松弛操作)
int g[N][N]; // 存储每条边
int dist[N]; // 存储1号点到每个点的最短距离
bool st[N]; // 存储每个点的最短路是否已经确定
// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < n - 1; i ++ )
{
int t = -1; // 在还未确定最短路的点中,寻找距离最小的点
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
// 用t更新其他点的距离
for (int j = 1; j <= n; j ++ )
dist[j] = min(dist[j], dist[t] + g[t][j]);
st[t] = true;
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
Dijkstra 算法堆优化版
相比于朴素版Dijkstra算法,堆优化版的效率更高,用优先队列代替寻找最小点的for循环,将时间复杂度降了下来,但是在更新每一个结点的最短距离时需要将集合中的结点重新推入优先队列中
时间复杂度为O(mlogn)O(mlogn), nn 表示点数,mm 表示边数
typedef pair<int, int> PII;
int n; // 点的数量
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N]; // 存储所有点到1号点的距离
bool st[N]; // 存储每个点的最短距离是否已确定
// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, 1}); // first存储距离,second存储节点编号
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];
if (dist[j] > distance + w[i])
{
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
存在负权边
bellman-ford算法,时间复杂度 O(nm)
当存在负权边的时候就不能用Dijkstra算法,bellman—ford算法是求负权边的基础算法
bellman的算法思路,首先定义一个数组dist【】,定义是距离起点1最近的距离,dist数组初始化为一个非常大的数,然后进行多轮循环,每一轮循环遍历所有的边,如果边a-b的终点到起点的距离即dist[b]大于边的起点到起点的距离dist[a]加上边的权值w,即dist[b]>dist[a]+w,便更新dist[b]为dist[a]+w。这个操作称为一次松弛,在Dijkstra算法中更新每个结点的操作也是松弛操作。每一轮遍历都至少会让一个结点得到一个最短路径,所以最多有n轮,总时间复杂度为O(nm)
bellman-ford算法判断负环,如果在n次迭代以后仍然存在松弛操作,就说明存在一条长度为n+1的最短路径,说明存在两个相同的结点,则存在负环
具体操作是开出一个变量update,存遍历的次数,如果遍历次数大于n,说明有负圈,退出循环
int n, m; // n表示点数,m表示边数
int dist[N]; // dist[x]存储1到x的最短路距离
struct Edge // 边,a表示出点,b表示入点,w表示边的权重
{
int a, b, w;
}edges[M];
// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
// 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
for (int i = 0; i < n; i ++ )
{
for (int j = 0; j < m; j ++ )
{
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
if (dist[b] > dist[a] + w)
dist[b] = dist[a] + w;
}
}
if (dist[n] > 0x3f3f3f3f / 2) return -1;
return dist[n];
}
SPFA算法,时间复杂度O(m)- O(nm)
SPFA是用队列来对bellman-ford进行优化,
bellman-ford的核心操作是每次遍历所有的边,进行松弛操作,其中很多此操作是毫无变化的,效率很低,但根据原理,当每一个结点u更新过和起点的最短距离后,紧接着更新u的 邻居结点 邻居结点肯定会有新的值更新,这样减少了很多无效的操作。
SPFA的操作可以用队列来进行
(1)起点s入队,计算它所有邻居到起点s的距离,将s出队,状态有更新的邻居入队,没更新的不入队,这样队列中都是状态有变化的结点,只有这些结点才会影响所有点最短路径的计算
(2)弹出u,更新u所有结点的变化
(3)弹出u以后,在后面的计算中u可能会再次更新状态,所以,u可能需要重新入队列,即,如果u变化了,就将u重新入队
(4)重复以上过程,直到队列为空,即没有结点更新。
int n; // 总点数
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N]; // 存储每个点到1号点的最短距离
bool st[N]; // 存储每个点是否在队列中
// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int 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])
{
dist[j] = dist[t] + w[i];
if (!st[j]) // 如果队列中已存在j,则不需要将j重复插入
{
q.push(j);
st[j] = true;
}
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
输出最短路径
如果需要输出最短路径,只要定义一个前驱数组pre【】来记录前驱结点,然后输出路径
void print_path(int s,int t)
{
if(s==t){print("%d",s);return ;}//打印起点
print_path(s,pre[t]);//打印前一个点
print("%d",t);//打印当前点,最后打印终点
}