作者:指针不指南吗
专栏:算法篇🐾算法理解没有用,会思路,会敲代码才OK🐾
每种情况下,都有一个最合适的算法
n 表示顶点的个数,m表示边的个数
单源最短路:1号点到所有点的最短路;多元最短路:任意两个点之间的最短路
稠密图和稀疏图:m是 10510^5105 级别的话就是稠密图,m是n级别的就是稀疏图
⭐️难点
出题的重难点在于建图 -> 把题目抽象成最短图问题,重点学习算法的实现,原理了解即可(比赛是不会考原理),做到会应用,会做题。无向图没有专门的算法,无向图即两个有向边,这里讲有向图。
⭐️牢记时间复杂度
注意时间复杂度,看一下数据范围,可以给我们很多提示
⭐️学习
算法理解没有用,会思路,会敲代码;背模板,压短写算法的时间压短,你的代码水平就会有很高的提升。
⭐️调代码技巧:printf大法(出现错误答案时);删代码(出现段错误时)
集合S为已经确定最短路径的点集。
- 初始化距离
一号结点的距离为零,其他结点的距离设为无穷大(看具体的题)。- 循环n次,每一次将集合S之外距离最短X的点加入到S中去(这里的距离最短指的是距离1号点最近。
点X的路径一定最短)。然后用点X更新X邻接点的距离。- 两层循环,时间复杂度为 O($n^2 $)。
s[] ; //表示当前已确定的最短距离
//1.初始化距离
dis[1]=0,dis[i]=inf; //dis表示到起点的最短距离,inf表示正无穷
//2.迭代过程,就是用个循环
for(int i=0;idis(t)+w[x] //用 t 更新其他点的距离
略微模糊,但是思路清晰
求 a 到 b 的最短路
从编号 1 开始 ,找起点到每个编号的最短路
红点表示已经确定最短路的顶点,
蓝色表示可以走到,但不能确定是最短路
绿色的线表示,更新当前顶点的邻接点的距离
⭐️题目
链接: 849. Dijkstra求最短路 I - AcWing题库
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 −1。
数据范围
1≤n≤500,
1≤m≤10510^5105 ,
图中涉及边长均不超过10000。输入样例:
3 3 1 2 2 2 3 1 1 3 4
输出样例:
3
⭐️题解
#include
using namespace std;const int N=510;
int g[N][N]; //用一个邻接矩阵来存储稠密图
int dist[N]; //表示每个点到起点的最短路
bool st[N]; //true表示已经确定最短路,属于s集合
int n,m;int dijkstra()
{//给距离初始化memset(dist,0x3f,sizeof dist); //每个顶点到起点的距离是无限大dist[1]=0; //起点到起点的距离是 0//迭代即循环过程for(int i=1;i<=n;i++) //遍历每一个顶点每次可以确定一个点到起点的最短路{int t=-1; //t 来存储当前访问这个点for(int j=1;j<=n;j++)if(!st[j]&&(t==-1||dist[t]>dist[j])) //j到起点的最短路还没有确定并且t没有被更新过,或者找到比t 还短的距离t=j;st[t]=true; //已经确定当前 点 t 的最短路for(int j=1;j<=n;j++) //利用距离最小的点,去更新其他点到其他点到起点的距离{dist[j]=min(dist[j],dist[t]+g[t][j]); }}if(dist[n]==0x3f3f3f3f) return -1; //如果起点到达不了n号节点,则返回-1return dist[n]; //否则,直接返回最短路的值}int main()
{scanf("%d%d",&n,&m);memset(g,0x3f,sizeof g); //初始化,所有的边,无限大while(m--){int a,b,c;scanf("%d%d%d",&a,&b,&c); //存边,邻接矩阵,因为是稠密图g[a][b]=min(g[a][b],c); //可能会出现重边的情况,存下最小的边}printf("%d",dijkstra()); return 0;
}
⭐️思路
堆优化版的dijkstra是对朴素版dijkstra进行了优化,在朴素版dijkstra中时间复杂度最高的寻找距离
最短的点O(10510^5105 )可以使用最小堆优化。
- 一号点的距离初始化为零,其他点初始化成无穷大。
- 将一号点放入堆中。
- 不断循环,直到堆空。每一次循环中执行的操作为:
弹出堆顶(与朴素版diijkstra找到S外距离最短的点相同,并标记该点的最短路径已经确定)。
用该点更新临界点的距离,若更新成功就加入到堆中。
⭐️堆的实现
方式 | 手写堆 | 优先队列(STL) |
---|---|---|
区别 | 可以保证n个数,支持修改堆中任意元素,使用映射 | 不支持修改,每次修改需要新加一个数,有m个数,好写方便 |
⭐️稠密图与稀疏图
- 连线很多的时候,对应的就是稠密图,显然易见,稠密图的路径太多了,所以就用点来找,也就是抓重点;
- 点很多,但是连线不是很多的时候,对应的就是稀疏图,稀疏图的路径不多,所以按照连接路径找最短路,这个过程运用优先队列,能确保每一次查找保留到更新到队列里的都是最小的,同时还解决了两个点多条路选择最短路的问题。
⭐️题目
链接: 850. Dijkstra求最短路 II - AcWing题库
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 −1。
数据范围
1≤n,m≤1.5×10510^5105 ,
图中涉及边长均不小于 0,且不超过 10000。
数据保证:如果最短路存在,则最短路的长度不超过 10910^9109 。输入样例:
3 3 1 2 2 2 3 1 1 3 4
输出样例:
3
⭐️题解
#include
using namespace std;typedef pair PII;const int N=1.5*1e5+10; int n,m;
int h[N],w[N],ne[N],e[N],idx; //稀疏图用邻接表来存
int dist[N]; //表示该点到起点的距离
bool st[N]; //表示该点的到起点的最短距离有没有被确定void add(int a,int b,int c) //存边和边权值
{e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++; //记住这个顺序,不可以变!!
}int dijkstra()
{memset(dist,0x3f,sizeof dist); //初始化各点到起点的最短距离dist[1]=0; priority_queue,greater> heap; //定义根小堆,内部根据距离排序,保证堆顶是距离最小的顶点;这个顺序不能倒,pair排序时是先根据first,再根据second,heap.push({0,1}); //把起点放进去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]; //j 当前相邻顶点的编号,i只是一个下标而不是编号if(dist[j]>distance+w[i]) //更新 相邻顶点到起点的最短距离{dist[j]=distance+w[i];heap.push({dist[j],j}); //距离变小则入队}}}if(dist[n]==0x3f3f3f3f) return -1; //如果n到起点的距离是正无穷,说明n与起点不通return dist[n]; //否在,返回n到起点的最短路
}int main()
{scanf("%d%d",&n,&m);memset(h,-1,sizeof h); //初始化邻接表while(m--){int a,b,c;scanf("%d%d%d",&a,&b,&c); //加边add(a,b,c);}printf("%d",dijkstra());return 0;
}
⭐️为什么 Dijkstra 算法不能解决负权边?
dijkstra要求每个点被确定后st[j] = true,dist[j]就是最短距离了,之后就不能再被更新了(一锤子买卖),而如果有负权边的话,那已经确定的点的dist[j]不一定是最短了。
dijstra算法基于贪心思想,当有负权边时,局部最优不一定是全局最优
⭐️什么是Bellman-Ford算法?
- Bellman - ford 算法是求含负权图的单源最短路径的一种算法,效率较低,代码难度较小。其原理为连续进行松弛,在每次松弛时把每条边都更新一下,若在n-1 次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。
- 通俗的来讲就是:假设 1 号点到 n 号点是可达的,每一个点同时向指向的方向出发,更新相邻的点的最短距离,通过循环 n-1 次操作,若图中不存在负环,则 1 号点一定会到达 n 号点,若图中存在负环,则在 n-1 次松弛后一定还会更新。
⭐️步骤
for n次 //表示更新n条边,还要记得备注for 所有边 a,b,w (松弛操作)dist[b] = min(dist[b],back[a] + w)//注意:back[] 数组是上一次迭代后 dist[] 数组的备份,由于是每个点同时向外出发,因此需要对 dist[] 数组进行备份,若不进行备份会因此发生串联效应,影响到下一个点
⭐️是否能到达n号点的判断中需要进行if(dist[n] > INF/2)判断
在下面代码中,是否能到达n号点的判断中需要进行if(dist[n] > INF/2)判断,而并非是if(dist[n] == INF)判断,原因是INF是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,dist[n]大于某个与INF相同数量级的数即可
比如5号节点距离起点的距离是无穷大,利用5号节点更新n号节点距离起点的距离,将得到10510^5105 −2,它 虽然小于10910^9109 , 但并不存在最短路,(在边数限制在k条的条件下)。
⭐️只要题中没有负环就可以用这个算法
有负权回路,是没有最短路的,负权回路的最小路就是负无穷
⭐️但是如果有边数限制的话,求最短路有负权回路也就无所谓了,也只能使用Bellman-Ford算法
举个例子:比如乘客从某个城市到另一个城市,之间可以进行周转,但是没周转一次,乘客的心情就会变坏一次,所以限制周转的次数为k次,这样就限制了边的次数
⭐️题目
链接: 853. 有边数限制的最短路 - AcWing题库
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从 11 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出
impossible
。注意:图中可能 存在负权回路 。
输入格式
第一行包含三个整数 n,m,k。
接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
点的编号为 1∼n。
输出格式
输出一个整数,表示从 1 号点到 n 号点的最多经过 k 条边的最短距离。
如果不存在满足条件的路径,则输出
impossible
。数据范围
1≤n,k≤500,
1≤m≤10000,
1≤x,y≤n,
任意边长的绝对值不超过 10000。输入样例:
3 3 1 1 2 1 2 3 1 1 3 3
输出样例:
3
⭐️题解
#include
using namespace std;const int N=510,M=10010;int n,m,k;
int dist[N],backup[N]; //dist表示该点到起点的最短路,backup用来备份,具体看函数内部实现struct Edge{int a,b,w;
}edges[M]; //a到b的距离为 wint bellman_ford()
{memset(dist,0x3f,sizeof dist); //初始化 每个顶点到起点的最短路dist[1]=0;for(int i=0;imemcpy(backup,dist,sizeof dist); //备份for(int j=0;jint a=edges[j].a,b=edges[j].b,w=edges[j].w; //松弛,更新边最短路dist[b]=min(dist[b],backup[a]+w);}}if(dist[n]>0x3f3f3f3f/2) return 0x3f3f3f3f/2+1; //n到起点的距离是无穷,表示n到起点不通return dist[n]; //否则返回 n到起点的最短路
}int main()
{scanf("%d%d%d",&n,&m,&k);for(int i=0;iint a,b,w;scanf("%d%d%d",&a,&b,&w);edges[i]={a,b,w}; //加边}int t=bellman_ford();if(t>0x3f3f3f3f/2) puts("impossible");else printf("%d\n",t);return 0;
}
⭐️分析
SPFA算法仅仅只是对该Bellman-ford算法的一个优化。
我觉得吧,区别就是Bellman-Ford没有用邻接表,而SPFA用了邻接表
Bellman_ford算法会遍历所有的边,但是有很多的边遍历了其实没有什么意义,我们只用遍历那些到源点距离变小的点所连接的边即可,只有当一个点的前驱结点更新了,该节点才会得到更新;因此考虑到这一点,我们将创建一个队列每一次加入距离被更新的结点。
⭐️注意
st数组的作用:判断当前的点是否已经加入到队列当中了;已经加入队列的结点就不需要反复的把该点加入到队列中了,就算此次还是会更新到源点的距离,那只用更新一下数值而不用加入到队列当中。
即便不使用st数组最终也没有什么关系,但是使用的好处在于可以提升效率。
SPFA算法看上去和 Dijstra 算法长得有一些像但是其中的意义还是相差甚远的:
Dijkstra算法中的st数组保存的是当前确定了到源点距离最小的点,且一旦确定了最小那么就不可逆了(不可标记为true后改变为false);
SPFA算法中的st数组仅仅只是表示的当前发生过更新的点,且spfa中的st数组可逆(可以在标记为true之后又标记为false)。顺带一提的是BFS中的st数组记录的是当前已经被遍历过的点。
Dijkstra算法里使用的是优先队列保存的是当前未确定最小距离的点,目的是快速的取出当前到源点距离最小的点;
SPFA算法中使用的是队列(你也可以使用别的数据结构),目的只是记录一下当前发生过更新的点。
Bellman-ford算法里最后return-1的判断条件写的是dist[n]>0x3f3f3f3f/2;而spfa算法写的是dist[n]==0x3f3f3f3f;
其原因在于Bellman_ford算法会遍历所有的边,因此不管是不是和源点连通的边它都会得到更新;但是SPFA算法不一样,它相当于采用了BFS,因此遍历到的结点都是与源点连通的,因此如果你要求的n和源点不连通,它不会得到更新,还是保持的0x3f3f3f3f。
Bellman_ford算法可以存在负权回路,是因为其循环的次数是有限制的因此最终不会发生死循环;但是SPFA算法不可以,由于用了队列来存储,只要发生了更新就会不断的入队,因此假如有负权回路请你不要用SPFA否则会死循环。
由于SPFA算法是由Bellman_ford算法优化而来,在最坏的情况下时间复杂度和它一样即时间复杂度为 O(nm),假如题目时间允许可以直接用SPFA算法去解Dijkstra算法的题目。
⭐️限制
没有负环就可以使用,99%的题没有负环,但是可以用来判断负环
阴险的出题人会把SPFA卡掉,使达到最坏O(nm), 网格图可能会卡
⭐️步骤
由Bellman-Ford算法用宽搜做优化
queue <- 1
while queue 不空 //队列里面放的是 待更新的点 t=q.frontq.pop()更新t的所有边 t-w->bqueue <- b
给定一个有向图,如下,求A~E的最短路。
源点A首先入队,然后A出队,计算出到BC的距离会变短,更新距离数组,BC没在队列中,BC入队
B出队,计算出到D的距离变短,更新距离数组,D没在队列中,D入队。然后C出队,无点可更新。
D出队,计算出到E的距离变短,更新距离数组,E没在队列中,E入队。
E出队,此时队列为空,源点到所有点的最短路已被找到,A->E的最短路即为8
⭐️题目
链接: 851. spfa求最短路 - AcWing题库
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出
impossible
。数据保证不存在负权回路。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出
impossible
。数据范围
1≤n,m≤10510^5105 ,
图中涉及边长绝对值均不超过 10000。输入样例:
3 3 1 2 5 2 3 -3 1 3 4
输出样例:
2
⭐️题解
#include
using namespace std;const int N=1e5+10;int n,m;
int h[N],e[N],ne[N],w[N],idx;
int dist[N];
bool st[N]; //表明该点已经在 队列里面了,防止队列里面出现重复元素void add(int a,int b,int c)
{e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}int spfa()
{memset(dist,0x3f,sizeof dist);dist[1]=0;queue q;q.push(1); //把起点放进去st[1]=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];if(!st[j]) //该点没有在队列里面,则放进去,否则会出现重复{q.push(j);st[j]=true;}}}}if(dist[n]>0x3f3f3f3f/2) return 0x3f3f3f3f/2+1; //记住有负权边的最短路,最后都这样处理return dist[n];}int main()
{scanf("%d%d",&n,&m);memset(h,-1,sizeof h); //链表初始化for(int i=0;iint a,b,w;scanf("%d%d%d",&a,&b,&w);add(a,b,w);}int t=spfa();if(t>0x3f3f3f3f/2) puts("impossible");else printf("%d",t);return 0;
}
⭐️思路
用cnt
记录每个点到起点的边数,当cnt[i] >= n 表示出现了边数>=结点数,必然有环,而且一定是负环
根据抽屉原理,可得。
抽屉原理:如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有n+1个元素放到n个集合中去,其中必定有一个集合里至少有两个元素。
⭐️题目
链接: AcWing 852. spfa判断负环 - AcWing
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你判断图中是否存在负权回路。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
如果图中存在负权回路,则输出
Yes
,否则输出No
。数据范围
1≤n≤2000,
1≤m≤10000,
图中涉及边长绝对值均不超过 10000。输入样例:
3 3 1 2 -1 2 3 4 3 1 -4
输出样例:
Yes
⭐️题解
#include
using namespace std;const int N=2010,M=1e4+10;int n,m;
int h[N],e[M],w[M],ne[M],idx;
int dist[N],cnt[N];
//dist 存的是当前从1号点到n号点的长度
//cnt 表示从1到x的最短路径中经过的点数
bool st[N];void add(int a,int b,int c)
{e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}bool spfa()
{queue q; //这里不用初始化距离,因为咱不是求最短路滴for(int i=1;i<=n;i++) //把所有的点放进队列里面去,可能存在负环,无法到达起点1,所有遍历所有点{st[i]=true;q.push(i);}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];cnt[j]=cnt[t]+1; //当前到该点的边数+1if(cnt[j]>=n) return true; //cnt>=n,说明有点走了两遍,一定存在负环if(!st[j]){st[j]=true; //入队q.push(j);}}}}return false; //走到这了,函数还没结束,意味着边数一直小于结点数,不存在负环
}int main()
{scanf("%d%d",&n,&m);memset(h,-1,sizeof h);while(m--){int a,b,w;scanf("%d%d%d",&a,&b,&w);add(a,b,w);}if(spfa()) puts("Yes");else puts("No");return 0;
}
不能应用于含负权回路的图
d[i][j] //邻接矩阵,存的是 i到j 的最短路问题for(k=1;k<=n;k++)for(i=1;i<=n;i++)for(j=1;j<=n;j++)d[i][j]=min(d[i][j],d[i][k]+d[k][j]) //更新
⭐️题目
链接: 854. Floyd求最短路 - AcWing题库
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,边权可能为负数。
再给定 k 个询问,每个询问包含两个整数 x 和 y,表示查询从点 x 到点 y 的最短距离,如果路径不存在,则输出
impossible
。数据保证图中不存在负权回路。
输入格式
第一行包含三个整数 n,m,k。
接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
接下来 k 行,每行包含两个整数 x,y,表示询问点 x 到点 y 的最短距离。
输出格式
共 k 行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出
impossible
。数据范围
1≤n≤200,
1≤k≤n2n^2n2
1≤m≤20000,
图中涉及边长绝对值均不超过 10000。输入样例:
3 3 2 1 2 1 2 3 2 1 3 1 2 1 1 3
输出样例:
impossible 1
⭐️题解
#include
using namespace std;const int N=210,INF=1e9;int n,m,k;
int d[N][N];void floyd()
{for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}int main()
{scanf("%d%d%d",&n,&m,&k);for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(i==j) d[i][j]=0;else d[i][j]=INF;while(m--){int a,b,w;scanf("%d%d%d",&a,&b,&w);d[a][b]=min(d[a][b],w);}floyd();while(k--){int a,b;scanf("%d%d",&a,&b);if(d[a][b]>INF/2) puts("impossible");else printf("%d\n",d[a][b]);}return 0;
}
⭐️Dijkstra-朴素 O(n^2)
- 初始化距离数组, dist[1] = 0, dist[i] = inf;
- for n次循环 每次循环确定一个min加入S集合中,n次之后就得出所有的最短距离
- 将不在S中dist-min的点->t
- t->S加入最短路集合
- 用t更新到其他点的距离
⭐️Dijkstra-堆优化 O(mlogn)
- 利用邻接表,优先队列
- 在priority_queue[HTML_REMOVED], greater[HTML_REMOVED] > heap;中将返回堆顶
- 利用堆顶来更新其他点,并加入堆中类似宽搜
⭐️Bellman-ford O(nm)
- 注意连锁想象需要备份, struct edge { int a , b , c } Edge[M];
- 初始化 dist , 松弛 dist [x.b] = min ( dist [x.b] , backup [x.a] + x.w ) ;
- 松弛 k 次,每次访问 m 条边,有边数限制
⭐️SPFA O(n)~O(nm)
- 利用队列优化仅加入修改过的地方
- for k次
- for 所有边利用宽搜模型去优化 Bellman-ford 算法
- 更新队列中当前点的所有出边
⭐️Floyd O(n3n^3n3)
- 初始化d
- k, i, j 去更新d
部分解释及图示转自acwing