i->j
有边不一定满足j->i
有边i->j
有边一定满足j->i
有边主要是根据题目要求来建单向边还是双向边
如果是双向边,我们只需要把他拆成i->j
和j->i
的两条单向边就行
无论是单向边还是双向边,对我们最短路都没有什么影响,学会算法以后直接套就行
int
型的二维数组tr[i][j]
,用来表示i
到j
之间的距离O(1)
判断i
和j
之间是否有边tr[n][n]
的数组的开销很大,需要根据题目给的空间范围来判断是否可行i
,我们去遍历与它相邻的所有的点的时候,需要O(n)
去枚举每个位置,看tr[i][j]
是否有值,这样n个点就是 O ( n 2 ) O(n^2) O(n2),时间开销很大#define MAX 1050
int tr[MAX][MAX];
for(int i = 1; i <= n; ++i){//枚举i
for(int j = 1; j <= n; ++j){//枚举每个点j,判断是否和i相连
if(tr[i][j]){
}
}
}
u
相连的点的信息,其他的我们并不在乎,所以链式前向星的本质是用数组模拟了n
个单链表,每个单链表都维护的是起点为i
的边的信息int
数组head
,head[i]
表示i
点的单链表的头u->v
,权值为c
u
v
c
head[u]
的单链表维护的是所有与u
相连的边,所以head[u]
其实就已经记录了u
的信息了int
型的变量to
,我们令to = v
,即to
记录的是v
的信息int
型的变量c
,记录的是边的权值c
int
型的变量nex
,指向上一条起点为u
的边的id
,用于进行节点的跳转,即维护单链表tot
//建图
int tot;
int head[MAX];
struct ran{
int to, nex, val;
}tr[MAX];
inline void add(int u, int v, int c){
tr[++tot].to = v;
tr[tot].val = c;
tr[tot].nex = head[u];
head[u] = tot;
}
//初始化
tot = 0;
memset(head, 0, sizeof(head));
for(int u = 1; u <= n; ++u){
for(int i = head[u]; i; i = tr[i].nex){//遍历与u相连的所有点
int v = tr[i].to;
int c = tr[i].val;
//u->v,c
}
}
i
都搞一个单链表,维护的是所有起点为i
的边的信息vector
来维护就行,不需要数组模拟vector>G[MAX]
,即开一个存pair
类型的vector
数组,对于一条边(u,v,c)
,我们只需要往G[u]
里push一个(v,c)
的pair
就行//建图
#define m_p(a,b) make_pair(a, b)
typedef pair <int,int> pii;
#define MAX 100050
vector<pii>G[MAX];
for(int i = 1; i <= m; ++i){
cin >> x >> y >> z;
G[x].push_back(m_p(y, z));
// G[y].push_back(m_p(x, z));
}
for(int u = 1; u <= n; ++u){
for(auto [v, d] : G[u]){
}
}
A: 即处理起点
s
已知,求从s
出发到其他任意点的最短距离
点的数量:
n
,边的数量:m
源点、起点:
s
,终点:t
最短路数组
dis[i]
,表示从起点s
到i
点的最短距离标记数组
vis[i]
,不同算法的标记数组的意义不同
(u,v,c)
表示u
到v
有一条长度为c
的边
该算法的特点是从起点开始,采用贪心算法的策略,采用加点的的方式,每次遍历到与起始点距离最近且从未被访问过的顶点的邻接节点t,将该点t加入集合B
中,直到扩展到终点位置。
A
,另一个就是还没确定最短路的集合B
,这个可以用vis[i]
来表示,vis[i]=1
是A
, vis[i]=0
是B
B
中找到距离起点s
最近的点,计作u
点,把他放到A
集合中,此时的dis[i]
就是s
到i
的最短路的长度u
点,去遍历u
点所有的邻边(u,v,c)
,如果dis[v]>dis[u]+c
,则更新一下dis[v]
的值从步骤2可以看出,迪杰斯特拉算法的本质是贪心,每一步都是取未确定的点中距离最小的点,然后去更新其他点。这样就可以保证到目前为止,每个点的最短距离一定是由已经确定最短路的点决定的
即我们假设s
到i
的最短路的路径是s->a->b->c->i
,当i
的最短路确定的之前,在s
到i
的路径中的每个点的最短路一定是在这之前就确定的了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nJbepVrp-1668158087918)(https://cdn.jsdelivr.net/gh/Chelseatr/image@main/uPic/IMG_6485C6692F5B-1.jpeg)]
int n, m, k, x;
int tot;
int head[MAX];
struct ran{
int to, nex, val;
}tr[MAX];
inline void add(int u, int v, int c){
tr[++tot].to = v;
tr[tot].val = c;
tr[tot].nex = head[u];
head[u] = tot;
}
int dis[MAX];
bool vis[MAX];
void dijkstra(int s){
memset(dis, inf, sizeof(dis));
memset(vis, 0, sizeof(vis));
dis[s] = 0;
for(int i = 1; i <= n; ++i){//进行n次循环
int id = -1, minx = inf;
for(int j = 1; j <= n; ++j){//找当前B集合中最短路最小的点
if(vis[j] == 0 && dis[j] < minx){
minx = dis[j];
id = j;
}
}
vis[id] = 1;
for(int i = head[id]; i; i = tr[i].nex){
int v = tr[i].to;
if(dis[v] > dis[id] + tr[i].val){
dis[v] = dis[id] + tr[i].val;
}
}
}
for(int i = 1; i <= n; ++i)cout << dis[i] << ' ';
}
O ( n 2 ) O(n^2) O(n2)
定义方式: apair
,p,q
可以是任意类型的变量
比较常用的是:p=q=int
,此时pair
等同于:
struct node{
int x, y;
};
定义方式:pair
访问方式:a.first
和a.second
产生一个pair
类型的变量的方法是:make_pair(x, y)
普通队列是先进先出,优先队列是优先级高的元素先出队列,并非按照先进先出的要求
什么样的元素的优先级高?这个可以由我们自己决定
默认版本的优先队列是priority_queue
,队列的元素是一个int
型的,队顶是最大的元素,俗称大根堆
如果想要最小的元素放在堆顶,则写成priority_queue
,俗称小根堆
如果想塞结构体什么的,就得自己重载运算符,这里就不展开了,想知道的可以自己去学一学
如果优先队列塞的是pair
,会根据pair的排序方法来确定,pair应该是先第一位,在第二位
简单分析一下,我们的第一层循环是不能优化掉的,因为每次只能确定一个点的最短路,所以要进行n次,但是第二层循环,我们做的是找出一个集合中
dis
最小的点,然后去更新与他相邻的所有的点所以我们其实可以用一个容器去快速地去维护最小的
dis[i]
的i
即我们优先队列需要放两个元素,一个是
dis[i]
,另一个是i
,我们取dis[i]
最小的那个,所以想要的是小根堆用
pair
的小根堆,定义方式如下:priority_queue<pair<int, int>, vector<pair<int, int> >, greater<pair<int, int> > >q;
q.top()
就是堆顶的元素,是pair
类型,是(dis[i], i)
其实也可以用大根堆
priority_queue
,由于pair第一维度最小,所以我们只需要塞>q (-dis[i], i)
结构体作为优先队列的元素也是可以的
#define m_p(a,b) make_pair(a, b)
typedef pair <int,int> pii;
int tot;
int head[MAX];
struct ran{
int to, nex, val;
}tr[MAX];
void add(int u, int v, int c){
tr[++tot].to = v;
tr[tot].val = c;
tr[tot].nex = head[u];
head[u] = tot;
}
int dis[MAX];
bool vis[MAX];
void dijkstra(int s){
priority_queue<pii, vector<pii>, greater<pii>>q;
mem(dis, 0x3f);
q.push(m_p(0, s));
dis[s] = 0;
while (!q.empty()) {
int u = q.top().second;q.pop();
if(vis[u])continue;
vis[u] = 1;
for(int i = head[u]; i; i = tr[i].nex){
int v = tr[i].to;
if(dis[v] > dis[u] + tr[i].val){
dis[v] = dis[u] + tr[i].val;
q.push(m_p(dis[v], v));
}
}
}
}
迪杰斯特拉的贪心算法真的正确吗?
给个例子:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v258EHhD-1668158087919)(https://cdn.jsdelivr.net/gh/Chelseatr/image@main/uPic/image-20221111091902915.png)]
迪杰斯特拉的贪心策略是,每次都找一个距源点最近的点,然后将该距离定为这个点到源点的最短路径;但如果存在负权边,那么直接得到的最短路不一定是最短路径,因为可能先通过并不是距源点最近的一个次优点,再通过一个负权边,使得路径之和更小,这样就出现了错误。
P4779 【模板】单源最短路径(标准版)
Bellman-Ford
算法是求含负权图的单源最短路径的一种算法,效率较低,代码难度较小。
迭代n次,每次都遍历所有边(u, v, c)
,去更新dis[v] = min(dis[v], dis[u] + c)
其原理为连续进行松弛,在每次松弛时把每条边都更新一下,最多进行n-1
次松弛操作
为什么是n-1
?
因为一共n
个点,从起点到终点的简单路径中最多有n-1
条边
所以如果在 n-1 次松弛后还能更新,则说明图中有负环。
O(nm)
对Bellman-Ford 算法的优化
每次迭代的时候不是所有边都能进行有效的更新
只有当dis[v]>dis[u]+c
的时候才有更新的必要
也就是只有dis[u]
变小了,(u, v, c)
的边才有意思
所以我们可以搞一个队列存下来所有dis
变小的节点u
只要我们队列不为空,即还存在变小的dis[u]
没去更新,我们就需要去更新
更新的时候,如果dis[v] > dis[u]+c
,则说明dis[v]
变小了,我们就放进队列里面,但是如果有些点已经在队列里面了,我们就不需要把他放进去,所以搞一个vis[i]
数组进行标记,如果在队列里就赋1,否则是0
int tot;
int head[MAX];
struct ran{
int to, nex, val;
}tr[MAX];
inline void add(int u, int v, int c){
tr[++tot].to = v;
tr[tot].val = c;
tr[tot].nex = head[u];
head[u] = tot;
}
bool vis[MAX];
int dis[MAX];
void SPFA(){
queue<int>q;
mem(dis, inf);
q.push(1);dis[1] = 0;
while (!q.empty()) {
int u = q.front();q.pop();vis[u] = 0;
for(int i = head[u]; i; i = tr[i].nex){
int v = tr[i].to;
if(dis[v] > dis[u] + 1){
dis[v] = dis[u] + 1;
if(!vis[v]){
vis[v] = 1;
q.push(v);
}
}
}
}
}
不过SPFA很容易被网格图卡成O(n*m)
,有很多玄学优化的方式,但是出题人要是想卡,每种方式都能卡掉
P3371 【模板】单源最短路径(弱化版)
多源最短路问题一般是求任意两点间的最短路径
在学会单源最短路的情况下,我们其实可以跑n
次单源最短路求出多源最短路,复杂度是O(nmlogm)
在n
很小,m
很大的情况下,就不适合了
我们可以用Floyed
算法来求解
Floyed
本质是动态规划
我们用dis[k][i][j]
表示i
到j
经过若干个编号不超过k
的节点的最短路径的长度
那要求dis[k][i][j]
这个状态的答案,我们可以考虑将其分成两个子状态:
i
到j
的最短路中不包含k
点,则dis[k][i][j] = dis[k-1][i][j]
i
到j
的最短路中包含k
点,则dis[k][i][j] = dis[k-1][i][k] + dis[k-1][k][j]
则状态转移方程式是:
dp[k][i][j] = min(dp[k-1][i][j], dp[k-1][i][k]+dp[k-1][k][j])
时间复杂度 O ( n 3 ) O(n^3) O(n3)
空间复杂度也是 O ( n 3 ) O(n^3) O(n3)
可以发现计算第k
层的时候只用到了第k-1
层,所以可以省略掉这一维度,直接写成dp[k][i][j] = min(dp[i][j], dp[i][k]+dp[k][j])
,减少空间的浪费
由于是dp问题,我们必须解决初始化的问题:如果i->j
存在长度为c
的边,则dp[i][j] = c
,其他的我们就赋个1e9,表示没有边
for(int k = 1; k <= n; ++k){
for(int i = 1; i <= n; ++i){
for(int j = 1; j <= n; ++j){
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
}
}