迪杰斯特拉算法分析
一般用三种数据结构存图,即邻接矩阵,邻接表,链式前向星
1.邻接矩阵
用二维数组储存即可int graph[maxn][maxn];, graph[i][j]储存结点i到j边的权值优点
适合稠密图,编码非常简短,对边的存储,查询,更新等操作又快又简单,只需要一步就能访问和修改。
缺点
- 存储复杂度O(V^2)太高
- 一般情况下不能存储重边
2.邻接表
一些规模大的稀疏图一般用邻接表存储。优点
他的优点是存储效率非常高,只需要与边数成正比的空间,存储的复杂度是O(V+E),几乎已经达到了最优复杂度,而且能存储重边
缺点
编程比邻接矩阵麻烦一些,访问和修改也慢一些
一般用STL的vector(动态数组)实现了邻接表
- 首先对边赋值
- 用e[i]:存第i个结点连接的所有边
- 把边(a,b)存到结点a的邻接表中
- 结点u的邻居有e[u].size()个
3.链式前向星
链式前向星是在邻接表的基础上优化的,对于一些空间极其紧张的题,用链式前向星是最好的,它用静态数组模拟邻接表,没有任何浪费,因此它是空间效率最高的存储的方法
- 对于边而言,边的终点是to,权值是w,下一个边next,起点放在head[]中
- head[u]指向结点u的第一个边的存储位置
- 用cnt记录edge[]的末尾的位置,新加入的边放在末尾
链式前向星的优点是存储效率高,程序简单,能存储重边,缺点是不方便做删除操作
适用范围
迪杰斯特拉算法适用于——单元最短路径问题,单终点最短距离,单对顶点最短路径问题,每对顶点间最短路径问题
基本思想
- Dijkstra算法:一个安路径长度递增地次序产生最短路径的算法
- 每次扩展一个距离最短的点,更新与其相邻的点的距离
- 当所有边的权值都为正数时,由于不会存在一个距离更短的没扩展过的点,所以这个点的距离永远不会再被改变,保住了算法的正确性。如果存在负权值的路径,那么考虑spfa算法跑最短路。
- 有向图和无向图都可以使用这个算法,无向图的每条边可以看成相反的两条边
- 引入松弛操作,让源点s到顶点的距离dis[i]取一个很大的值,然后不断减小dis[i],当所有的dis[i]不能再减小时,就求出了s到所有点的最短路径。松弛操作的目的就是减小dis[i]的值,如果从s到达i有更优的路径则更新dis[i]。
if (dis[v] > dis[u] + e[i].w)
{
dis[v] = dis[u] + e[i].w; //松弛操作
q.push((node){
dis[v], v}); //把新遍历的到的点加入堆中
}
dijkstar的原理与流程
- dijkstra本质上的思想是贪心,它只适用于不含负权边的图.
- 我们把点分成两类,一类是已经确定最短路径的点,称为"白点",另一类是未确定最短路径的点,称为"蓝点"
- 初始化dis[start] = 0,dis[start]=0,其余节点的dis值为无穷大.
- 找一个disdis值最小的蓝点x,把节点x变成白点.
- 遍历x的所有出边(x,y,z),若dis[y] > dis[x] + z,则令dis[y] = dis[x] + z
- 重复2,3两步,直到所有点都成为白点…
- 时间复杂度为O(n^2)
举例
友情提示以下图片参考洛谷—最短路在此鸣谢~
1.(令start = 1)
2.开始时我们把dis[start]初始化为0,其余点初始化为inf
第一轮循环找到dis值最小的点1,将1变成白点,对所有与1相连的蓝点的dis值进行修改,使得dis[2]=2,dis[3]=4,dis[4]=7
第二轮循环找到dis值最小的点2,将2变成白点,对所有与2相连的蓝点的dis值进行修改,使得dis[3]=3,dis[5]=4
第三轮循环找到dis值最小的点3,将3变成白点,对所有与2相连的蓝点的dis值进行修改,使得dis[4]=4
接下来两轮循环分别将4,5设为白点,算法结束,求出所有点的最短路径
为什么dijkstra不能处理有负权边的情况
- 2到3的边权为-4,显然从1到3的最短路径为-2 (1->2->3)…但在循环开始时程序会找到当前dis值最小的点3,并标记它为白点
- 这时的dis[3]=1,然而1并不是起点到3的最短路径.因为3已经被标为白点,所以dis[3]不会再被修改了.我们在边权存在负数的情况下得到了错误的答案.
下面有一个例题(单源最短路径的模板题)
题目描述
编程实现Dijkstra算法,求一个有向加权图中,从源点出发到其他各个顶点的最短路径。
输入
第1行第1个值表示顶点个数,第2个值表示边个数;第2行开始为边(两个顶点,边的起点和终点)及权重。
输出
顶点0到每一个顶点的最短路径长度。
样例输入
5 7
0 1 10
0 3 30
0 4 100
1 2 50
2 4 10
3 2 20
3 4 60
样例输出
0 10 50 30 60
PS:这个题目是一个模板题,但是它默认源点是0点,所以在输入的时候没有输入源点,一般的代码都是输入三个数,顶点数,边数,与源点,代码基本一样,只要改一下一点点代码就行——如果想要源点从键盘输入,那么增加一个输入,并把起点放入队列或者动态数组里面就行。
迪杰斯特拉(朴素版)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 10;
const int inf = 0x3f3f3f3f;
struct node
{
int to, dis;
bool operator<(const node x) const
{
return dis > x.dis;
}
};
vector<node> w[maxn];
int dis[maxn], vis[maxn], n;
void dijkstra()
{
memset(dis, inf, sizeof(dis));
memset(vis, 0, sizeof(vis));
dis[0] = 0, vis[0] = 1;
for (int i = 0; i < w[0].size(); ++i)
{
int to = w[0][i].to;
dis[to] = w[0][i].dis;
}
for (int i = 1; i < n; ++i)
{
int minn = n;
for (int j = 1; j < n; ++j)
{
//找到未确定的距离0最近的点
if (!vis[j] && dis[j] < dis[minn])
minn = j;
}
vis[minn] = 1;//标记被确定的点
for (int i = 0; i < w[minn].size(); ++i)
{
int to = w[minn][i].to;//从minn点更新其他点
if (!vis[to] && dis[to] > dis[minn] + w[minn][i].dis)//如果发现路径0->minn->to比之前到to的路径的更优,更新
dis[to] = dis[minn] + w[minn][i].dis;
}
}
}
int main()
{
int m, a, b, c, v, num;
while (~scanf("%d%d", &n, &m))
{
node now;
for (int i = 1; i <= m; ++i)
{
scanf("%d%d%d", &a, &b, &now.dis);
now.to = b;
w[a].push_back(now);//vector存边,(数组也可以)
now.to = a;
w[b].push_back(now);
}
dijkstra();
for (int i = 0; i < n; ++i)
printf("%d%c", dis[i], i == n - 1 ? '\n' : ' ');
}
return 0;
}
定义一个优先队列加结构体重载小于号,就相当于一个小根堆了,重载把大的放在前面,然后再把排好序的数据放入优先队列那么大的就在根底了,也就形成了一个小根堆
迪杰斯特拉堆优化(邻接表存图)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 10;
const int inf = 0x3f3f3f3f;
struct node
{
int to, dis;
bool operator<(const node x) const
{
return dis > x.dis;
}
};
vector<node> w[maxn];
int dis[maxn], vis[maxn], n;
void dijkstra()
{
memset(dis, inf, sizeof(dis));
memset(vis, 0, sizeof(vis));
dis[0] = 0;
priority_queue<node> pq;
node now, ne;
now.to = 0, now.dis = 0;
pq.push(now);
while (!pq.empty())
{
now = pq.top(), pq.pop();
if (vis[now.to])
continue;
vis[now.to] = 1;
for (int i = 0; i < w[now.to].size(); ++i)
{
int to = w[now.to][i].to;
if (!vis[to] && dis[to] > now.dis + w[now.to][i].dis)
{
dis[to] = now.dis + w[now.to][i].dis;
ne.dis = dis[to];
ne.to = to;
pq.push(ne);
}
}
}
}
int main()
{
int m, a, b, c, v, num;
while (~scanf("%d%d", &n, &m))
{
node now;
for (int i = 1; i <= m; ++i)
{
scanf("%d%d%d", &a, &b, &now.dis);
now.to = b;
w[a].push_back(now); //vector存边,(数组也可以)
now.to = a;
w[b].push_back(now);
}
dijkstra();
for (int i = 0; i < n; ++i)
printf("%d%c", dis[i], i == n - 1 ? '\n' : ' ');
}
return 0;
}
迪杰斯特拉堆优化(链式前向星存图)
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 10;
const int inf = 0x3f3f3f3f;
struct edge
{
int to, next, w;
} e[maxn];
int head[maxn], cnt = 1, n, m, s, vis[maxn], dis[maxn];
struct node
{
int w, now;
bool operator<(const node x) const
{
//大根堆,大的在前
return w > x.w;
}
};
priority_queue<node> q; //定义一个大根堆
void add(int u, int v, int w) //链式前向星加边
{
e[cnt].to = v;
e[cnt].w = w;
e[cnt].next = head[u]; //存储该点的下一条边
head[u] = cnt++; //更新目前该点的最后一边
}
void dijkstra()
{
for (int i = 0; i < n; ++i)
dis[i] = inf;
dis[s] = 0; //初始化
q.push(node{
0, 0});
while (!q.empty()) //如果堆栈为空,那么所有点都已经更新
{
node x = q.top();
q.pop();
int u = x.now; //记录堆顶并将其弹出
if (vis[u])
continue;
vis[u] = 1;
for (int i = head[u]; i; i = e[i].next) //搜索堆顶所有的连边
{
int v = e[i].to;
if (dis[v] > dis[u] + e[i].w)
{
dis[v] = dis[u] + e[i].w; //松弛操作
q.push((node){
dis[v], v}); //把新遍历的到的点加入堆中
}
}
}
}
int main()
{
while (~scanf("%d%d", &n, &m))
{
for (int i = 1; i <= m; ++i)
{
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x, y, z);
}
dijkstra();
for (int i = 0; i < n; ++i)
printf("%d ", dis[i]);
printf("\n");
}
return 0;
}
最短路——spfa(链式前向星存图)
SPFA算法由于它上限 O(NM) = O(VE)的时间复杂度,被卡掉的几率很大.在算法竞赛中,我们需要一个更稳定的算法:dijkstra.
spfa算法参考链接
#include <bits/stdc++.h>
const long long inf = 2147483647;
const int maxn = 10005;
const int maxm = 500005;
using namespace std;
int n, m, s, num_edge = 0;
int dis[maxn], vis[maxn], head[maxm];
struct Edge
{
int next, to, dis;
} edge[maxm]; //结构体表示静态邻接表
void addedge(int from, int to, int dis) //邻接表建图
{
//以下是数据结构书上的标准代码,不懂翻书看解释
edge[++num_edge].next = head[from]; //链式存储下一条出边
edge[num_edge].to = to; //当前节点编号
edge[num_edge].dis = dis; //本条边的距离
head[from] = num_edge; //记录下一次的出边情况
}
void spfa()
{
queue<int> q; //spfa用队列,这里用了STL的标准队列
for (int i = 0; i < n; i++)
{
dis[i] = inf; //带权图初始化
vis[i] = 0; //记录点i是否在队列中,同dijkstra算法中的visited数组
}
q.push(0);
dis[0] = 0;
vis[0] = 1; //第一个顶点入队,进行标记
while (!q.empty())
{
int u = q.front(); //取出队首
q.pop();
vis[u] = 0; //出队标记
for (int i = head[u]; i; i = edge[i].next) //邻接表遍历,不多解释了(也可用vector代替)
{
int v = edge[i].to;
if (dis[v] > dis[u] + edge[i].dis) //如果有最短路就更改
{
dis[v] = dis[u] + edge[i].dis;
if (vis[v] == 0) //未入队则入队
{
vis[v] = 1; //标记入队
q.push(v);
}
}
}
}
}
int main()
{
cin >> n >> m;
for (int i = 0; i < m; i++)
{
int f, g, w;
cin >> f >> g >> w;
addedge(f, g, w); //建图,有向图连一次边就可以了
}
spfa(); //开始跑spfa
for (int i = 0; i < n; i++)
if (s == i)
cout << 0 << " "; //如果是回到自己,直接输出0
else
cout << dis[i] << " "; //否则打印最短距离
return 0;
} //结束
不必太纠结当下,也不必忧郁未来,当你经历过一些事情后,眼前的风景已经和从前不一样了。人生没有无用的经历,只要我们一直向前走,天总会亮。我想人生的每一段相遇,都会有它的意义,爱你的人带给你温暖,你爱的人带给你欢愉,不爱你的人教会你成长,而你不爱的人教会了你疏离,地球上有809个岛屿 ,204个国家 ,77亿人,相遇的概率只有2920万分之一,我和你相遇,已经是无比的幸运~ |
---|