朴素版Dijkstra算法
Dijkstra解决的是最短路径(权重和)问题:
从图中的某个顶点出发到达另外一个顶点所经过的边的权重和最小的一条路径,称为最短路径。
包含三个数组dist,vis,path。
dist[i]表示i到原点的最短距离;path[i][j]是i,j两节点的距离(也可以说是权重),这个数组是在初始化节点关系时候用的;vis[i]判断i节点是否被访问。
算法执行方式:
找到当前所有节点中距离原点路径最短的点,标记它vis[i]=true,说明这个点我们访问过了,然后以这个最短点为起点更新这个最短点所有相邻点到原点的距离。循环进行,直到所有节点都被访问过。
Dijkstra算法执行的思想是贪心,证明的话我大概能理解,但说不清楚(之前写过,但感觉说的很不清楚,就删掉了,建议b站找个视频看一下该算法如何运作的,应该能理解。)
关键代码
int Dijkstra()
{
dist[1] = 0;
for (int i = 1; i <= n; i++){
int t = -1;
for (int j = 1; j <= n; j++)
if (!vis[j] && (t == -1 || dist[j] < dist[t]))
t = j;
vis[t] = true;
for (int j = 1; j <= n; j++)
dist[j] = min(dist[j],dist[t] + path[t][j]);
}
return dist[n];
}
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 510;
int n, m, path[N][N], dist[N];
//path存两点的距离,dist存第i节点到1节点的最短距离
bool vis[N];
int Dijkstra()
{
dist[1] = 0;
for (int i = 1; i <= n; i++)//对剩余n-1个节点更新最短距离
{
int t = -1;
for (int j = 1; j <= n; j++)
if (!vis[j] && (t == -1 || dist[j] < dist[t]))
t = j;//找到未访问过的节点中距离源点最小的节点
vis[t] = true;
for (int j = 1; j <= n; j++)
dist[j] = min(dist[j],dist[t] + path[t][j]);
//min是选择j到源点多条路径里距离最小的路径
//更新节点t相邻未访问过的边的最小值
//一开始我先判断了j是否被访问过,但发现一点不影响,如果j被访问过,j一定在t之前或者就是t,所以dist[j]<=dist[t]
}
return dist[n];
}
int main()
{
cin >> n >> m;
//Inti
memset(path, 0x3f, sizeof path);//为什么这里是0x3f,后面有解释
memset(dist, 0x3f, sizeof dist);
int x, y, v;
while (m--)
{
cin >> x >> y >> v;
path[x][y]=min(v,path[x][y]);
}
int ret = Dijkstra();
if (ret != 0x3f3f3f3f) cout << ret;
else cout << -1;
return 0;
}
- 关于定义无穷为0x3f3f3f3f而不取0x7fffffff详细见这篇博客
-参考二,我觉得这个更清楚
简单说就是用0x7fffffff+0x7fffffff会使无穷+无穷数据溢出,变成负数,而0x3f3f3f3f+0x3f3f3f3f=0x7e7e7e7e不会溢出变负。 - 关于memset( dist ,0x3f,sizeof dist ):
memset 按照字节赋值,因此我们把 4 个 0011 1111 填充到 32 位的 int 上范围是109左右。
如果我们想要将某个数组清零,我们通常会使用memset(a,0,sizeof(a))这样的代码来实现(方便而高效),但是当我们想将某个数组全部赋值为无穷大时(例如解决图论问题时邻接矩阵的初始化),就不能使用memset函数而得自己写循环了(写这些不重要的代码真的很痛苦),我们知道这是因为memset是按字节操作的,它能够对数组清零是因为0的每个字节都是0,现在好了,如果我们将无穷大设为0x3f3f3f3f,那么奇迹就发生了,0x3f3f3f3f的每个字节都是0x3f!所以要把一段内存全部置为无穷大,我们只需要memset(a,0x3f,sizeof(a))。
memset( dist ,0x3f,sizeof dist ):是无穷大
memset( dist ,-0x3f,sizeof dist ):是无穷小
堆排序优化版Dijkstra
关于优先队列priority_queue函数的使用:
头文件#include<queue>
定义:priority_queue<Type, Container, Functional>
Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式。
greater<Type>是小根堆,less<Type>是大根堆
//升序队列,小顶堆
2 priority_queue <int,vector<int>,greater<int> > q;
3 //降序队列,大顶堆
4 priority_queue <int,vector<int>,less<int> >q;
代码
思路和朴素版的其实一样
关键代码
int Dijkstra()
{
heap.push({ 0,1 });
while (!heap.empty()) {
PII temp = heap.top();
heap.pop();
int node = temp.second, distance = temp.first;
if (vis[node]) continue;
vis[node] = true;
for (int i = h[node]; i != -1; i = ne[i])
if (distance + W[i] < dist[e[i]]) {
dist[e[i]] = W[i] + distance;
heap.push({ dist[e[i]],e[i] });
}
}
return dist[n];
}
完整代码
#include<iostream>
#include<algorithm>
#include<vector>
#include<queue>//通过优先队列实现堆排序减小时间复杂度
#include<cstring>
using namespace std;
typedef pair<int, int>PII;
const int N = 1.5 * 1e5 + 5;
int h[N], e[N], ne[N], W[N], idx, n, dist[N];
bool vis[N];
priority_queue<PII, vector<PII>, greater<PII>>heap;
//第一个元素存的是dist,第二个存的是节点,先比较第一个元素再比较第二个元素
//e寸的仍然是节点,W存的是节点到该条链头结点的距离(两节点之间的距离)
void add(int x, int y, int w)//x->y
{
//这个是前向星,我之前喜欢用前向星,然后有一阵子不用了就给忘了,后来比较习惯用vector嵌套vector来记录节点之间的距离
e[idx] = y; ne[idx] = h[x]; W[idx] = w; h[x] = idx++;
}
int Dijkstra()
{
heap.push({ 0,1 });
dist[1] = 0;
while (!heap.empty())
{
PII temp = heap.top();//最小堆直接得到dist最小的节点
heap.pop();
int node = temp.second, distance = temp.first;
if (vis[node]) continue;//用来去除同一个节点不同的dist,因为最小的dist在堆顶,所以不影响
vis[node] = true;
for (int i = h[node]; i != -1; i = ne[i])
{
if (dist[e[i]] > distance + W[i]) {
dist[e[i]] = distance + W[i];
heap.push({dist[e[i]],e[i]});
//后面dist更小得会出现在堆的上面,被输出的优先级高与距离大的那次存储
}
}
}
return dist[n];
}
int main()
{
memset(h, -1, sizeof h);
memset(dist, 0x3f, sizeof dist);
int m, x, y, w;
cin >> n >> m;
while (m--)
{
cin >> x >> y >> w;
add(x, y, w);
}
int ret = Dijkstra();
if (ret == 0x3f3f3f3f) cout << -1;
else cout << ret;
return 0;
}
贝尔曼-福特算法
解析
Bellman-Ford(下面简称BF算法)算法和Dijkstra的一大不同就是 ,Dijkstra算法是通过一个dist最小的节点对与它相邻节点的dist进行更新,是通过节点更新dist(我是这么理解的),而BF算法则是通过边对dist进行更新,而且每次更新是在上一次dist的基础上,对所有边关联的节点进行dist的更新。
for(int i=0;i<k;i++)
copy上一次更新的dist;
for (int j = 0; j < m; j++
更新所有边结尾的dist;
这里解释一下最外层循环的k和内层循环的m:
外侧循环的k指的是最多进行k条边,这个是根据题目意思设定的,如果题目没有要求k条边,那k就是n-1。n个节点,节点1到节点n的路径上最多边数是n-1,每个节点都在路径上,这样就可以阻止一直循环走负环。
例如:
如果k=5(最多经过5条边),最短路径为3(1->4->6->5->3->6)
如果k=3(最多经过3条边),最短路径为6(1->2->3->6)
如果k=2(最多经过2条边),最短路径为7(1->4->6)
所以BF算法并不是说完全不走负边。
如果把图变成这样,那么dist[7]=2(1->2->3->6->5->3->6)
所以BF算法也不是完全不走负环,只能避免一直走负环,而采用Dijkstra算法就可能出现一直走负环的现象,所以Dijkstra使用条件前提就是图中无负环。
关于实现原理,简单说一下吧。
每一次的迭代(更新)都会产生到某一个节点的最短距离(这个不知道怎么证明比较好,好像理解了,但说不清楚),n个节点最多进行n-1次迭代,n个节点除了源点一共n-1个节点,所以正好剩下的每一个节点都取得了最终的最短路径。
代码
关键代码
int Bellman_Ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < k; i++) {
memcpy(Copy, dist, (n + 1) * sizeof(int));
for (int j = 0; j < m; j++)
{
int x = Edges[j].x, y = Edges[j].y, z = Edges[j].z;
dist[y] = min(dist[y], Copy[x] + z);
}
}
return dist[n];
}
完整代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 1e4 + 5;
struct {
int x, y, z;//x->y
}Edges[N];//记录m条边
int n, m, k, dist[505],Copy[505];
int Bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < k; i++) {//k为最短路径限制的边数
memcpy(Copy, dist, (n+1)*sizeof(int));
//每一次的迭代是在上一次的基础上进行的,以防同一次迭代中节点互相影响,用一共copy数组存上一次迭代的结果
for (int j = 0; j < m; j++) {//m为边数
int x = Edges[j].x, y = Edges[j].y, z = Edges[j].z;
dist[y] = min(Copy[x] + z, dist[y]);
}
}
return dist[n];
}
int main()
{
int x, y, z;
cin >> n >> m >> k;
for (int i = 0; i < m; i++){
cin >> x >> y >> z;//x->y, w=z;
Edges[i] = { x,y,z };
}
int ret = Bellman_ford();
if (ret >= 0x3f3f3f3f / 2) cout << "impossible";
//为什么是0x3f3f3f3f/2,是因为负边是可以走的,无穷+负边<无穷。
else cout << ret;
return 0;
}
Spfa算法
求最短路径
感觉长得和bfs挺像,实现原理就是对每一个在上一次更新过程中dist改变的节点对它的相邻节点进行更新(只有之前被更新过,才有能更新出更小的路径),从源节点开始一直循环次操作,知道图中所有节点都不能再被更新位置(队列q为空)这就表示n节点已经得到最短路径
题目链接–spfa求最短路
这题这么写是因为题目声明了只有负边,不存在负环
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int N = 1e5 + 5;
int n, m, e[N], ne[N], h[N], w[N], idx, dist[N];
void add(int x, int y, int z)
{
e[idx] = y; ne[idx] = h[x]; w[idx] = z; h[x] = idx++;
}
int spfa()
{
dist[1] = 0;
queue<int>q;
q.push(1);
while (!q.empty()) {//更新到不能跟新为止
int temp = q.front();
q.pop();
for (int i = h[temp]; i != -1; i = ne[i])
if (dist[temp] + w[i] < dist[e[i]])
{
dist[e[i]] = dist[temp] + w[i];
q.push(e[i]);
}
}
return dist[n];
}
int main()
{
memset(dist, 0x3f, sizeof dist);
memset(h, -1, sizeof h);
int x, y, z;//x->y
cin >> n >> m;
while (m--)
{
cin >> x >> y >> z;
add(x, y, z);
}
int ret = spfa();
if (ret == 0x3f3f3f3f) cout << "impossible";
else cout << ret;
return 0;
}
判断负环
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 1e4 + 5;
int n, m, e[N], ne[N], h[2005], w[N], idx, dist[2005], cnt[2005];
bool vis[2005];
queue < int>q;
void add(int x, int y, int z)
{
e[idx] = y; ne[idx] = h[x]; w[idx] = z; h[x] = idx++;
}
bool spfa()
{
while (!q.empty())
{
int temp = q.front();
vis[temp]=false;
q.pop();
for (int i = h[temp]; i != -1; i = ne[i])
{
if (dist[e[i]] > dist[temp] + w[i])
{
dist[e[i]] = dist[temp] + w[i];
cnt[e[i]] = cnt[temp] + 1;
if (cnt[e[i]] >= n) return true;
if(!vis[e[i]]){
q.push(e[i]);
vis[e[i]]=true;
}
}
}
}
return false;
}
int main()
{
memset(h, -1, sizeof h);
memset(dist, 0x3f, sizeof dist);
int x, y, z;
cin >> n >> m;
for (int i = 1; i <= n; i++)//防止负环与源点不关联
q.push(i);
while (m--)//x->y
{
cin >> x >> y >> z;
add(x, y, z);
}
if (spfa()) cout << "Yes" << endl;
else cout << "No" << endl;
return 0;
}
Floyd算法
1->2->3的距离小于1->3的距离,更新1->3的距离为2
这个算法简单说一下我的理解,最外层的循环是节点数,后面两个循环分别是起始节点和终点,也就是对任意两个节点找一个中间点,判断是否经过中间点后的距离更短,如果是就更新。
在每一次取中间点的过程对于任意两点都会有一次更新,那么在这其中的一次更新中就会得到起始点到中间点和中间点到终点的最短距离,这样就能在接下来的更新中判断对于起始点到终点的距离是否需要更新。
但我这边有个问题,如果更新起始点到中间点和中间点到终点的最短距离在对中点进行更新之后那么通过中点得到起始点到终点的最短距离如何实现,问题先放着,以后想通了再继续写。
继续更新
关于上面这个问题我手动模拟了一下,总算明白了,一开始我觉想要1->4的最短距离,由图得到路径1->5->2->4(1->2->4,5作为1->2的中间点)
根据代码1->2的最短路径经过5,但是5作为中间点进行更新在4后面,自然而然就觉得无法得到1->4的最短距离,但是模拟之后我发现…1->4的最短路径可以是1->5->4,那么2作为中间点就在4之前更新了…我真是个傻子。
关键代码
//inti
for (int j = 1; j <= n; j++)
{
if (i == j) dist[i][j] = 0;
else dist[i][j] = INF;
}
//floyd
for (int t = 1; t <= n; t++)//最外层是中间节点
for (int i = 1; i <= n; i++)//起始节点
for (int j = 1; j <= n; j++)//中止节点
dist[i][j] = min(dist[i][j], dist[i][t] + dist[t][j]);
完整代码
#include<iostream>
using namespace std;
const int N = 205, INF = 1e9;
int dist[N][N];
int main()
{
int n, m, k, x, y, z;
cin >> n >> m >> k;
//Inti
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
{
if (i == j) dist[i][j] = 0;
else dist[i][j] = INF;
}
for (int i = 0; i < m; i++)
{
cin >> x >> y >> z;
dist[x][y] = min(z, dist[x][y]);//x->y
}
for (int t = 1; t <= n; t++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
dist[i][j] = min(dist[i][j], dist[i][t] + dist[t][j]);
while (k--)
{
cin >> x >> y;
if (dist[x][y] > INF / 2) cout << "impossible" << endl;
else cout << dist[x][y] << endl;
}
return 0;
}