博客转载
忘了强烈建议看看前三篇博客,可以节省不少理解的时间,知识点直击重点
相关知识总结
n 阶图 : n
个顶点的图
有限图 :一个图的点集和边集都是有穷集的图。
零图 :边集为空集的图
无向图 :每条边都是无向边的图
有向图 :每条边都是有向边的图
完全图 :图中每对不同的顶点之间都恰连有一条边相连。n
个端点的完全图有 n
个端点以及n(n−1)/2
条边
通路 :顶点与其关联边的交替序列
回路 :首尾相连的通路。
这里回路好像和离散数学里的概念不大一样,注意区分
自环 :若一条边所关联的两个结点重合,则称此边为自环
度 :一个顶点的度是指与该点相关联的边的条数,顶点 v
的度记作 deg(v)
注意:一个自环的端点度数为 2
孤立点 :度都为 0 的点
叶(悬挂顶点) :度为 1 的点
入度 :在有向图中 ,一个顶点 v 的入度是指与该边相关联的入边(即边的终点是 v )
的条数,记作 deg+(v)
出度 :在有向图中 ,一个顶点 v 的出度是指与该边相关联的出边(即边的始点是 v )
的条数,记作 deg-(v)
疏松图&稠密图 :是一个相对概念。点较少边较多的图示稠密图,反之则是疏松图。
一般而言,E*2 接近 V 的大概率是稠密图
定理一
即在任意图 中,度数之和等于边数的 2 倍
推论 :(1)在任意图中,度数为奇数的点必然有偶数个.
(2)在任意图中,只有两个奇度的点必定是相连的
定理二
即在有向图 中,所有点入度之和等于出度之和
无向图是没有入度和出度的概念的
图的存储方式
还没那水平讲明白,详细讲解 就先看看大佬的博客吧
图的结构比较复杂,任意两个顶点之间都可能存在关系,不能用简单的顺序存储结构来表示。
如果运用多重链表,即一个数据域多个指针域组成的结点表示图中的一个结点,则会造成大量的空间损失。
邻接矩阵
邻接矩阵是表示顶点之间相邻关系的矩阵。它用两个数组保存数据:一个数组存储途中顶点信息,一个二维数组存储图中边或者弧的信息
无向图
边连着的两个端点都可以互相串通,所以存入边的时候是两个方向同时存入
特点 :(1)1 表示有边,0 表示无边
(2)顶点的度是行内数组之和
(3)求取顶点邻接点,将行内元素遍历即可
(4)二维数组是一个对称的数组
有向图
有向图有入度和出度区分,存边不再是两个方向同时存入,而是按照指定方向单方向存入
特点 :(1)1 表示有边,0 表示无边
(2)各行之和是出度,各列之行是入度
一般存边都是每行每行的存所以如此,如果反过来存边那么上述规律恰好相反
(3)求取顶点邻接点,将行内元素遍历即可
(4)二维数组不再是一个对称的数组
带权有向图
若是带有权值,那么就把每个表示有边的1换为权值数,而无边的 0 换位无穷大(因为权值可能为任意数,包括 0 和负值)
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void init () { for (int i=0 ;i<n;i++){ for (int j=0 ;j<n;j++) if (j!=i)graph[i][j]=inf; } } void add (int u,int v,int w) { graph[u][v]=w; graph[v][u]=w; } signed main () { init(); int n;cin >>n; for (int i=0 ;i<n;i++){ int u,v,w;cin >>u,v,w; add(u,v,w); } return 0 ; }
优缺点
优点
缺点
易于理解/代码简短
复杂度高、对于稀疏图浪费高
对于确定边效率高
对于不确定边效率极低
易处理重边
比如想知道某个结点出发的所有边需要逐一判断
邻接表
引入 :由于邻接矩阵消耗空间大,容易造成浪费。为了减小空间消耗采用动态分布内存的思想,不再定义数组长度
不再是通过矩阵记录相邻点的关系了了,而是一种动态数组和链表相结合的存储方法,指记录相连的两个点的关系:动态数组存储定点信息,链表存储从个点出发能到达的其他点
特点 :(1)图中顶点用一个一维数组存储
(2)图中每个顶点 Vi 的所有邻接点构成一个线性表
竖列为一维数组,记录的是每一个结点。横向就是 vector
模仿的链表,记录的是同一个起点出发的路,第 i
个链表的第 j
个值存储的是从顶点i出发到顶点 list[j]
的边的情况。
邻接表一般存储有向带权图,其中出度表叫邻接表,而入度表就是逆邻接表
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const int maxn=1005 ;struct node { vector <int > val; vector <int > to; }ver[maxn]; void init () { for (int i=0 ;i<maxn;i++) ver[i].to.clear(); } void add (int u,int v,int w) { ver[u].to.push_back(v); ver[u].val.push_back(w); } signed main () { init(); int n;cin >>n; for (int i=0 ;i<n;i++){ int u,v,w; cin >>u>>v>>w; add(u,v,w); } return 0 ; }
优缺点
优点
缺点
易于理解/代码简短
对重边处理不当
内存占用率低
需要遍历所有的边。一般情况下使用邻接表存图是会存储重边的,不会做重边的判断
对不确定边效率高
对确定边的效率低
比如,要遍历从某点出发的所有边,不会遍历到不存在的
比如,我想知道从 a 到 b 的距离,只能遍历
邻接矩阵和邻接表对比
思路浑然发生了改变,准确的说是为了节省空间人们找到了一种新的高效存图方法,根据这一新的方法又产生了链式前向星
链式前向星
值得骄傲的是这是中国独创的一种较中和的方法
链式前向星表也是一维数组和链式组合结构,每个结点 i
都有一个链表,链表存储的是以 i
为起点的所有边的集合 (邻接表存储的是从i出发的所有顶点的集合 )
即这种存图方法是以边为基础。但是不再是用 vector
实现链表了,而是用一个动态数组记录但是再要存入权值时结构体还是要用的
注意:注意和邻接表不同,链式前向星在加边时是在原有的链表结构的头部插入这条边,这样使得每次插入的时间复杂度为 O(1) ,所以链表的边的顺序和读入顺序正好是逆序的
相当于正向存边逆向遍历
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 const int V=1005 ;const int E=5005 ;int cnt;struct node { int next; int to; int w; }; node edge[E]; int head[V]void init () { cnt=0 ; memset (head,-1 ,sizeof (head)); } void add (int u,int v,int w) { cnt++; edge[cnt].next=head[u]; head[u]=cnt; edge[cnt].to=v; edge[cnt].w=w; } void trav (int k) { for (int i=head[k];i!=-1 ;i=edge[i].next) cout <<k<<"->" <<edge[i].to<<"w" <<edge[i].w<<endl ; } signed main () { init(); int n;cin >>n; for (int i=0 ;i<n;i++){ int u,v,w; cin >>u,v,w; add(u,v,w);add(v,u,w); } int k;cin >>k; trv(k); return 0 ; }
邻接表和链式前向星对比
总结
邻接矩阵与邻接表相比,疏松图多用邻接表,稠密图多用邻接矩阵
链式前向星其实是一种较好替代邻接表来存图的数据结构,在邻接表存图不能使用时可以使用,几乎可以用于全部图论题目
推荐前期使用邻接矩阵入门,后期再使用链式前向星
图的遍历
邻接矩阵
看看这篇博客理论实现 就好,代码写的太复杂了没必要看
BFS模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 const int inf=0x3f3f3f3f ;const int maxn=1005 ;int g[maxn][maxn];bool vis[maxn];void init () { memset (vis,0 ,sizeof (vis)); for (int i=0 ;i<maxn;i++){ for (int j=0 ;j<maxn;j++){ if (i==j)g[i][j]=0 ; else g[i][j]=inf; } } } void add (int u,int x,int w) { g[u][v]=w; g[v][u]=w; } void bfs (int u) { queue <int > q; q.push(u); vis[u]=1 ; while (!q.size()){ int temp=q.front(); q.pop(); for (int i=1 ;i<=n;i++){ if (!vis[i]&&g[temp][i]!=inf){ q.push(i); vis[i]=1 ; } } } } void bfs () { for (int i=1 ;i<=n;i++){ if (!vis[i]){ bfs(i); } } } signed main () { init(); int n; cin >>n; for (int i=0 ;i<n;i++){ int u,v,w;cin >>u,v,w; add(u,c,w); } bfs(); return 0 ; }
DFS模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const int inf=0x3f3f3f3f ;const int maxn=1005 ;int g[maxn][maxn];bool vis[maxn];void dfs (int u,int depth) { vis[i]=1 ; for (int i=0 ;i<n;i++){ if (!vis[i]&&g[u][i]!=inf){ dfs(i,depth); } } } void dfs_trav () { for (int i=1 ;i<=n;i++){ if (!vis[i]) dfs(i,1 ); } }
链式前向星
BFS模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const int maxn=1005 ;bool vis[maxn];void bfs (int u) { queue <int > q; q.push(u); vis[u]=1 ; while (!q.empty()){ int temp=q.front(); q.pop(); vis[temp]=1 ; for (int i=head[temp];i!=-1 ;i=edge[i].next){ if (vis[edge[i].next]) q.push(edge[i].to); } } } void bfs_trav () { for (int i=1 ;i<=n;i++){ if (!vis[i]) bfs(i); } }
DFS模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void dfs (int x) { vis[x]=true ; for (int i=head[x];i!=-1 ;i=edge[i].next){ if (!vis[edge[i].next]) dfs(edge[i].to); } } void dfs_trav (int n) { for (int i=1 ;i<=n;i++){ if (!vis[i]) dfs(i); } }
图的最短路径问题
概念 :简而言之就是找出两点之间权值最小的路径(权值一般代表长度嘛)
若是无向图其实让求最短路径的时候就是默认了两点之间的路径权值为 1
基本思想 :无论什么算法,都是基于一个最基本的思想——松弛原理,即刷新最短路径
以上图为例,假如点 1 到点 2 的最短路是 2 ,点 2 到点 3 的最短路是 1 ,而图中点 1 直接到点 3 的距离是 6 ,那么,很明显点 1 到点 2 到点 3 比点 1 直接到点 3 短得多。那么,我们的最短路 dis[1][3]
就等于dis[1][2]+dis[2][3]
。可以这么理解:点 1 到点 2 这条边的边权已经是点 1 到 2 点的最短路了,同样,点 2 到点 3 也是,那我们就可以用这两条最短路来刷新点 1 到点 3 的最短路,如果以后遇到更短的最短路方案再次刷新,这就是用最短路来松弛最短路了。
Floyd算法
原理 :枚举每一个点作为中转点,然后利用动态规划的思想不断刷新每两点之间的最短距离。看似简单粗暴,实际上可以巧妙地缩小规模,无论有多少个中转点,我们都是从少到多往下推
适用问题 :无负环的稠密图(边的条数|E|接近|V|²,称为稠密图)求多源最短路径问题的最优算法
特点 :只要跑一遍Floyd算法,在其维护生成的 dis 二维数组里任意 dis[i][j]
就是顶点 i
到顶点 j
的最短路径(多源)
假设我们现在不允许有中转点,那么各个点之间的最短路应该是题目给出来的边权
现在我们只允许通过1号节点来进行转移,注意是1号而不是1个,因为我们现在考虑的是枚举的顺序,即怎么枚举。现在我们只需要枚举 i 和 j ,比较 dis[i][1]+dis[1][j]
和 dis[i][j]
的大小,如果小于则刷新 dis 数组
接着,我们允许通过 1 号点和 2 号点进行转移,注意这里的“和”,一方面,2 号点本身可以独自松弛一些路径,另一方面,之前通过1号点松弛的一些路径中,可能有和2号点连着的。比如说,在上一轮我们用 dis[5][1]+dis[1][2]
松弛了 dis[5][2]
,然后我们再用这个路径去松弛其他路径 ,实际上就相当于通过1号点和2号点进行转移,所以我们的条件是“允许通过1号点和2号点”。不必考虑中转点的数量,用编号更大的点做中转点时,实际上也包含了编号小的中转点 。所以,这里我们仍然只需要枚举 i
和 j
,用 dis[i][2]+dis[2][j]
去尝试松弛 dis[i][j]
就行了,这里的中转点我们可以用k
来表示
依次类推到 k=n
的情况,即可以用 1~n
所有的点进行中转
实现 :定义一个数组 f[k][x][y]
,表示只允许经过结点1到k,结点x
到结点y
的最短路径。很显然,f[n][x][y]
就是x
到y
的最短路径。我们现在利用dp思想来更新求出这个数组:f[k][x][y]=min(f[k-1][x][y],f[k-1][x][k]+f[k-1][k][y])
我们发现实际上一维数组没有用,直接改写为:f[x][y]=min(f[x][y],f[x][k]+f[k][y])
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void Floyd () { for (int i=1 ;i<=n;i++){ for (int j=1 ;j<=n;j++) dp[i][j]=graph[i][j]; } for (int k=1 ;k<=n;k++){ for (int i=1 ;i<=n;i++){ for (int j=1 ;j<=n;j++){ if (dp[i][j]>dp[i][k]+dp[k][j]) dp[i][j]=dp[i][k]+dp[k][j]; } } } }
优缺点
优点
缺点
易理解/代码短
时间复杂度太高了 O(n3 )
跑一次任意两点间距离都出来了
一般顶点数超过 1000 个就 TLE 了
Dijkstra算法
引入 :Floyd算法时间复杂度太高,只能处理顶点比较少的问题,如果顶点数太多又该怎么求最短路呢?
思考 :Floyed 慢的原因是它无法保证当前求得的最短路一定是最终最优的(因为dp本来就是不断更新当前最优的过程),那我们能不能保证每次求得最短路是最优的呢?答案是可以的,但是需要舍弃一些东西
原理 :BFS 思想+贪心思想
适用 :无负权图求单源最短路径问题
特点 :只适用于非负权图,但是时间复杂度非常优秀 O(n2 ) ,是用来求单源最短路径的算法
模板
这个板子利用了贪心求两点最小以达到所求两点间距离最小。但是循环时不会停止,也就是每一次尝试都是后面的点和遍历的点的距离都更新一次。 也就是说每次找一个最近点得花费不少时间
课件上有过程演示,想不明白了就去看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 const int maxn=1005 ;const int inf=0x3f3f3f3f ;int n;int dis[maxn];int graph[maxn][maxn];bool vis[maxn];void init () { memset (vis,0 ,sizeof (vis)); } void dij (int start) { int x; int minn; vis[start]=1 ; for (int i=1 ;i<=n;i++) dis[i]=graph[start][i]; for (int i=1 ;i<n;i++){ minn=inf; for (int j=1 ;j<=n;j++) if (!vis[j]&&dis[j]<minn){ minn=dis[j]; x=j; } vis[x]=1 ; for (int j=1 ;j<=n;j++){ if (!vis[j]&&dis[j]>dis[x]+graph[x][j]) dis[j]=dis[x]+graph[x][j]; } } }
Dijkstra算法堆优化
引入 :普通版本的dijkstra算法时间复杂度为 O(n2 ) ,而且是实打实的 O(n2 ) ,不会提前结束循环。如果顶点数超过10000 可能还是会爆时间。
最起码比 Floyd 好了一点了
思考 :我们发现每次找一个最近点有点耗时,能不能加快寻找最近点的速度?我们发现好像前面讲的图的 bfs 遍历好像更适合实现dijkstra算法,但是我如何保证队列里面取出的元素就是队列里面最小的呢?
解决 :我们已经学了神奇的STL,STL里面有个东西叫 priority_queue ,可以实现自动排序。说到 bfs 遍历和队列就又想到了链式前向星这个非常适合图遍历的数据结构,而链式前向星存的是某点出发的边集合,很适合求单源最短路的 dijkstra 算法,于是就诞生了——链式前向星实现优先堆优化的 Dijkstra
独特的优点 :若是题目给出的两点的边有多条,其他的算法在存入的时候都需要和已存入的路做一下比较然后更新选择权值最低的边,而用此算法则不需要
原因:链式前向星不同于其他的存法,它是以边为基础存入,所以在整个更新的过程中它不得不将每一个存入的边用来更新 diskstra ,不管是重复的边还是没有重复的边
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 #include <bits/stdc++.h> using namespace std ;const int inf=0x3f3f3f3f ;typedef pair <int ,int > P;const int V=1e5 +5 ;const int E=2e5 +5 ;int dis[V],head[V];struct node { int nex,to,w; }edge[E]; int cnt;void init () { cnt=0 ; memset (head,-1 ,sizeof (head)); } inline void add (int u,int v,int w) { cnt++; edge[cnt].nex=head[u]; head[u]=cnt; edge[cnt].to=v; edge[cnt].w=w; } void dij (int start) { memset (dis,inf,sizeof (dis)); priority_queue <P,vector <P>,greater<P> > que; dis[start]=0 ; que.push(P(0 ,start)); while (!que.empty()){ P p = que.top(); que.pop(); int v=p.second; if (dis[v]<p.first)continue ; for (int i=head[v];i!=-1 ;i=edge[i].nex){ node e=edge[i]; if (e.w+dis[v]<dis[e.to]){ dis[e.to]=e.w+dis[v]; que.push(P(dis[e.to],e.to)); } } } } signed main () { int n,m; while (cin >>n>>m){ if (n==0 &&m==0 )break ; init(); for (int i=0 ;i<m;++i){ int u,v,w; cin >>u>>v>>w; add(u,v,w); add(v,u,w); } dij(1 ); cout <<dis[n]<<endl ; } return 0 ; }
优缺点
优点
缺点
跑单源点可以高达 10000
多源点跑多次
复杂度比较适中
不能跑有负权的图
Bellman-Ford算法
类似于链式前向星表的发现,总可以换个角度去解决问题
问题引入 :上面的两个算法一直着重于找中转点 来进行松弛,我们能不能不管中转点呢?当然可以。我们之前的策略是找一个中转点,然后用它连的边实现松弛。我们现在不用中转点,我们可以找路径,比如dis[i]
就是一个路径,它代表从出发点到i
点的路径的距离
思路1 :我们每次拿我们已知的可能的最短路径 dis[k] 来进行松弛:如果 dis[k]+w[k][j]<dis[j]
,则刷新 dis[j]
。这样一轮下来,我们可以得到一些新的最短路径,我们再利用这些最短路径来松弛,又可以得到新的最短路径,通过这样的方式一层一层建立起整个最短路的结构
还是松弛操作,但是不再是枚举点取松弛而是枚举边
每次松弛操作实际上是对相邻节点的访问,那么第 n
次松弛操作保证了所有深度为n
的路径最短
思考2 :这样的操作要进行多少轮呢?不难想到,n
个点的图,最短路径上最多有 n-1
条边(链),所以最多进行n-1
轮。但是,我们可以在一轮结束以后判断:当前这一轮进行松弛,如果有,那才有继续下一轮的必要,如果没有,那就可以提前退出了,因为你已经没有任何一条路径可以再进行松弛了,你再下一轮只是再进行一轮完全相同的操作
这篇Bellman-ford解决最短路和负权环 的博客有详细的思路讲解和图示
注意:因为以边为对象,所以该算法要通过链式前向星存边实现
寻找最短路径
1 2 3 4 5 6 7 for (int k=1 ;k<=n-1 ;k++){ for (int i=1 ;i<m;i++){ if (dis[v[i]]>dis[u[i]]+w[i]) dis[v[i]]= dis[u[i]]+w[i]; } }
注意:这里是应用链式前向星表以边为主 体进行的计算。整个算法用一句话概括就是:对所有的 m
个边进行 n-1
次松弛操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <bits/stdc++.h> using namespace std ;const int inf=0x3f3f3f3f ;signed main () { int u[100 ],v[100 ],w[100 ],dis[100 ],n,m; cin >>n>>m; for (int i=1 ;i<=m;i++) cin >>u[i]>>v[i]>>w[i]; for (int i=1 ;i<=n;i++) dis[i]=inf; dis[1 ]=0 ; for (int k=1 ;k<=n-1 ;k++){ for (int i=1 ;i<=m;i++) if (dis[v[i]]>dis[u[i]]+w[i]) dis[v[i]]=dis[u[i]]+w[i]; } for (int i=1 ;i<=n;i++) cout <<dis[i]<<" " ; return 0 ; }
寻找负权环路
除此之外,bellman-ford算法还可以检测出一个图是否含有负权回路。如果进行 n-1
轮松弛操作之后仍然存在
1 2 if (dis[v[i]]>dis[u[i]]+w[i])dis[v[i]] = dis[u[i]] + w[i];
的情况,也就是说在进行n-1
轮松弛后,仍可以继续成功松弛,那么此图必然存在负权回路。如果一个图没有负权回路,那么最短路径所包含的边最多为 n-1
条,即进行 n-1
轮松弛操作后最短路不会再发生变化。如果在 n-1
轮松弛后最短路仍然可以发生变化,则这个图一定有负权回路
模板
显然Bellman-Ford算法的时间复杂度是 O(nm) ,这个时间复杂度貌似比Dijkstra算法还要高,我们还可以对其进行优化。在实际操作中,Bellman-Ford算法经常会在未达到 n-1
轮松弛前就已经计算出最短路,之前我们已经说过,n-1
其实是最大值。因此可以添加一个一维数组用来备份数组 dis
。如果在新一轮的松弛中数组 dis
没有发生变化,则可以提前跳出循环,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <bits/stdc++.h> using namespace std ;const int INF = 0x3f3f3f3f ;signed main () { int u[100 ],v[100 ],w[100 ],dis[100 ],n,m,ck,flag; cin >>n>>m; for (int i=1 ;i<=m;i++) cin >>u[i]>>v[i]>>w[i]; for (int i=1 ;i<=n;i++) dis[i]=INF; dis[1 ]=0 ; for (int k=1 ;k<=n-1 ;k++){ ck=0 ; for (int i=1 ;i<=m;i++){ if (dis[v[i]]>dis[u[i]]+w[i]){ dis[v[i]]=dis[u[i]]+w[i]; ck=1 ; } } if (ck==0 )break ; } flag=0 ; for (int i=1 ;i<=m;i++) if (dis[v[i]]>dis[u[i]]+w[i])flag=1 ; if (flag==1 ) printf ("此图包含有负权回路\n" ); else { for (int i=1 ;i<=n;i++) printf ("%d " ,dis[i]); } return 0 ; }
Bellman-Ford算法堆优化( SPFA )
思路 :很多时候我们并不需要那么多无用边去尝试进行松弛操作。这时候就不再看边了,而是改用看点,如果 dis[i]
刷新了,那么说明这个 i
点可以为我们接下来的松弛提供帮助,我们应该将它记录起来。那么我们用队列来维护“可能会引起松弛操作的点”,进而就可以只访问必要的边了,从而省去没必要的操作。
注意:这个队列里的元素是可能重复入队的,因为一个点的最短路径可以不断更新。另外当一个点已经在队列里的时候,我们是不用将它入队的,因为我们记录这个点的目的是等会要使用它去更新其他点,所以我们只用记录一次编号
这里一定要和 dijkstra 的堆优化分清楚, dijkstra 堆优化的队列里面存的是某一长度的边。而我们这里不需要,所以就算这个点的最短路径中间可能会变但是不影响我们的结果,我们只需要记住这个点可能可以作为松弛的中转点,也就是我们可以用它进行松弛就够了。具体实现操作看课件很详细
寻找最短路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 void SPFA (ll s) { memset (dis,inf,sizeof (dis)); memset (vis,0 ,sizeof (vis)); dis[s]=0 ; vis[s]=1 ; queue <ll> q; q.push(s); while (!q.empty()){ ll now=q.front(); q.pop(); vis[now]=0 ; for (int i=head[now];i!=-1 ;i=nex[i]){ if (dis[to[i]]>dis[now]+weight[i]){ dis[to[i]]=dis[now]+weight[i]; if (!vis[to[i]]){ q.push(to[i]); vis[to[i]]=1 ; } } } } }
寻找负权环路
Bellman-Ford算法可以寻找负权回路,当然SPFA
算法也可以了,而且思路都差不多
对于思路一网上有人说他不好,因为如果出现输入自己到自己的边,或者出现重边的情况(可以自己思考一下为什么)就会误判,所以用思路二更加保险
思想 :首先我们要知道,对于一个不存在负环的图,从起点到任意一个点最短距离经过的点最多只有 n
个。这样的话,我们用 tot[i]
表示从起点(假设就是 1)到 i
的最短距离包含点的个数,初始化 tot[1]=1
,那么当我们能够用点 u
松弛点 v
时,松弛时同时更新 tot[v]=cnt[u]+1
,若发现此时 tot[v]>n
,那么就存在负环
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include <bits/stdc++.h> using namespace std ;typedef long long ll;const int inf=0x3f3f3f3f ;const ll V=1005 ;const ll E=5005 ;ll weight[E],to[E],nex[E]; bool vis[E];ll cnt=0 ; ll n,m,dis[V]; ll head[V]; bool tot[V];void add (ll u,ll v,ll w) { cnt++; to[cnt]=v; weight[cnt]=w; nex[cnt]=head[u]; head[u]=cnt; } void init () { cnt=0 ; memset (head,-1 ,sizeof (head)); } bool SPFA (ll s) { memset (dis,inf,sizeof (dis)); memset (vis,0 ,sizeof (vis)); dis[s]=0 ; vis[s]=1 ; tot[s]=1 ; queue <ll> q; q.push(s); while (!q.empty()){ ll now=q.front(); q.pop(); vis[now]=0 ; for (int i=head[now];i!=-1 ;i=nex[i]){ if (dis[to[i]]>dis[now]+weight[i]){ dis[to[i]]=dis[now]+weight[i]; tot[to[i]]=tot[now]+1 ; if (tot[to[i]]>n) return 1 ; if (!vis[to[i]]){ q.push(to[i]); vis[to[i]]=1 ; } } } } return 0 ; }
优缺点
优点
缺点
也可以跑顶点数较多的图
最坏复杂度太高跑稠密图可能被卡 TLE
可以跑带有负权的图
最短路算法注意问题
注意初始化问题,到底哪些数组、变量需要初始化,什么时候初始化。包括 dijkstra 算法、SPFA 算法等。还有编号是从 1 开始还是 0 开始
有的题会有重边,邻接矩阵需要将输入值和已经存在的边权取一下 min
因为 SPFA
算法最坏情况下的时间复杂度是 O(NM) ,所以有时会被恶意数据卡掉(网格图),如果没有负边权的话还是建议优先使用Dijkstra
在有负边权的图上跑单源最短路一般使用 SPFA
判断负权环路用 SPFA
超级源点使用以及最短路回溯路程
比较总结
顶点数少多源点的题就用 Floyd
顶点数多且没有负权的正常题 dijkstra 足矣(不容易被卡掉)
带负权的图或者找负权回路就 SPFA
常见拓展
超级源点
最短路回溯路程
就是想办法把找到的最短路的走法记下来
模板题:给出 n
个点 m
条边找出最短路径和走过的路程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <bits/stdc++.h> using namespace std ;typedef long long ll;const int N=1e3 ,inf=0x3f3f3f3f ;int n;int a[N][N],b[N];int way[N][N];int graph[N][N],dp[N][N];void Floyd () { for (int i=1 ;i<=n;i++){ for (int j=1 ;j<=n;j++) dp[i][j]=graph[i][j]; } for (int k=1 ;k<=n;k++){ for (int i=1 ;i<=n;i++){ for (int j=1 ;j<=n;j++){ if (dp[i][k]+dp[k][j]+b[k]<dp[i][j]) dp[i][j]=dp[i][k]+dp[k][j]+b[k],way[i][j]=way[i][k]; else if (dp[i][k]+dp[k][j]+b[k]==dp[i][j]&&way[i][k]<way[i][j]) way[i][j]=way[i][k]; } } } } signed main () { ios::sync_with_stdio(false ),cin .tie(0 ); while (cin >>n&&n){ for (int i=1 ;i<=n;i++){ for (int j=1 ;j<=n;j++){ int x;cin >>x; if (x==-1 )graph[i][j]=inf; else graph[i][j]=x; way[i][j]=j; } } for (int i=1 ;i<=n;i++) cin >>b[i]; Floyd(); int x,y; while (cin >>x>>y){ if (x==y&&y==-1 )break ; printf ("From %d to %d :\nPath: " ,x,y); int ans=x; while (ans!=y){ printf ("%d-->" ,ans); ans=way[ans][y]; } printf ("%d\nTotal cost : %d\n\n" ,y,dp[x][y]); } } return 0 ; }
记录前继节点后,从终点向起点遍历并通过 stack
颠倒一下顺序输出就好了
注意:一维数组记录后继节点直接从起点遍历输出想法很好但是大错特错
模板题:给出 n
个点 m
条边找出最短路径和走过的路程,没有输出 -1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 #include <bits/stdc++.h> using namespace std ;typedef long long ll;const ll inf = 0x3f3f3f3f3f3f ;typedef pair <ll,ll> P;const ll V = 2e5 +5 ;const ll E = 2e5 +5 ;ll dis[V],head[V],pre[V]; bool vis[V];struct node { ll nex,to,w; }edge[E]; ll cnt; void init () { cnt=0 ; memset (head,-1 ,sizeof (head)); memset (pre,0 ,sizeof pre); } inline void add (ll u,ll v,ll w) { cnt++; edge[cnt].nex=head[u]; head[u]=cnt; edge[cnt].to=v; edge[cnt].w=w; } void dij (ll start) { memset (vis,0 ,sizeof (vis)); memset (dis,inf,sizeof (dis)); priority_queue <P,vector <P>,greater<P> > que; dis[start]=0 ; que.push(P(0 ,start)); while (!que.empty()){ P p = que.top(); que.pop(); ll v=p.second; if (dis[v]<p.first) continue ; for (ll i=head[v];i!=-1 ;i=edge[i].nex){ node e=edge[i]; if (e.w+dis[v]<dis[e.to]){ dis[e.to]=e.w+dis[v]; pre[e.to]=v; que.push(P(dis[e.to],e.to)); } } } } signed main () { ll n,m; while (cin >>n>>m){ init(); for (ll i=0 ;i<m;++i){ ll u,v,w; cin >>u>>v>>w; add(u,v,w); add(v,u,w); } dij(1 ); if (dis[n]>=inf)cout <<-1 <<endl ; else { ll ans=n; stack <ll> sta; while (pre[ans]){ sta.push(pre[ans]); ans=pre[ans]; } while (!sta.empty()){ cout <<sta.top()<<" " ; sta.pop(); } cout <<n<<endl ; } } return 0 ; }