前言
1.图的定义及基本概念
假设由一个图 G = ( V , E ) G=(V,E) G = ( V , E ) ,V V V 是点集,存储了图中所有的点;E E E 是边集,存储了图中所有的边。
说白了,你可以将图理解成有几个地点,其中某些地点间有道路连通。而这些道路,可能是单向道,也可能是双向道。这些地点对应图中的点,道路对应图中的边。
如果每条道路都是单向道,即边都是单向的,那么称这张图为有向图。否则称这张图为无向图。当然,在有向图中,可能存在两点间有两条边,使两点可以互相连通。
有时两点间有多条边连接,被称为重边 。
有时存在从一个点到它自己的边,称为自环 。
每条边可能会对应一个数值,称为边权 。
每个点可能会对应一个数值,成为点权 。
对于无向图:
对于有向图:
通向这个点的边的数量叫做入度 。
从这个点出发的边的数量叫做出度 。
例如,有如下两张图。左图为无向图,右图为有向图。
求左图点 1 1 1 的度,右图点 2 2 2 的入度、点 1 1 1 的出度。
答案:(1)3,(2)2,(3)2 (点击 ctrl+a 查看答案)
2.本文会用到的一些术语
本文中,我们会用 u → ( v , c ) u\to(v,c) u → ( v , c ) 表示一条从 u u u 通向 v v v 的边,权值为 c c c 。
一、常用的存图方式
1.邻接矩阵
一个二位数组 g g g ,其中 g i , j g_{i,j} g i , j 表示从 i i i 到 j j j 的边权。
空间复杂度 O ( n 2 ) O(n^2) O ( n 2 ) 。
缺点是空间复杂度极大,不能存储重边。但是,要查询一条从 i i i 通向 j j j 的边,时间复杂度为 O ( 1 ) O(1) O ( 1 ) ,更优于其它方式。
2. vector
通常是 vector<pair<int,int> > g[N]
的形式。其中 g i g_i g i 存储了 i i i 点的所有出边。
空间复杂度 O ( N ) O(N) O ( N ) 。
3. 邻接表
空间复杂度为O ( n ) O(n) O ( n ) 。但邻接表只用几个静态数组便可以存图,稍优于 vector。且邻接表更便于查询反向边。
h e a d i head_i h e a d i 存储 i i i 点的所有出边中,存储位置做靠右的那一条。
n x t i nxt_i n x t i 表示与 i i i 边相同入点的,存储在它左侧的,最靠近它的边(如果不存在,则 n x t i = 1 nxt_i=1 n x t i = 1 )。
t o i to_i t o i 表示 i i i 边的出点。
q z i qz_i q z i 表示 i i i 边的权值。
每次添加一条边时,将此边的 n x t nxt n x t 值指向这个入点的上一条边,并将此边入点的 h e a d head h e a d 值设为此边。
1 2 3 4 5 6 7 8 9 10 11 12 13 int head[N],to[M],qz[M],nxt[M];int cnt;void add_edge (int x,int y,int z) { cnt++; to[cnt]=y; qz[cnt]=z; nxt[cnt]=head[x]; head[x]=cnt; } for (int j=head[i];j;j=nxt[i])
二、单源最短路径问题
单元最短路径问题(简称 SSSP)。
顾名思义,此类问题给出一个起点,求出它到所有点的最短路径。
1、Dijkstra 算法
Dijkstra,基于贪心的思想,可用来解决单源最短路径问题。但前提是所有边权都非负。
· 大致思路
创建 d i s dis d i s 数组,其中 d i s i dis_i d i s i 表示从起点 s s s 到点 i i i 的最短距离。
维护两个顶点集合 S S S 与 Q Q Q 。其中 S S S 中点的 d i s dis d i s 值已经为最终值,无法继续修改。
将原点 s s s 的 d i s dis d i s 值设为 0 0 0 ,其他点的 d i s dis d i s 值设为 + ∞ +\infty + ∞ 。
取出 Q Q Q 集合中 d i s dis d i s 值最小的点 u u u ,将它放入 S S S 中,并遍历这个点所有的出边 p → ( q , c ) p \to (q,c) p → ( q , c ) ,将 q q q 的 d i s dis d i s 值更新为 max ( d i s q , d i s p + c ) \max(dis_q,dis_p+c) max ( d i s q , d i s p + c ) 。
重复执行步骤 4,直到 Q Q Q 集合为空。
下面这张动图完美地解释了 Dijkstra 算法的执行过程(0 0 0 为起点,圆圈里的数字为点的编号,红色的数字表示 d i s dis d i s 值):
(图片来源于网络)
关于 Dijkstra 算法的正确性
显然,每次从集合 Q Q Q 中取出的点的 d i s dis d i s 值只要不会再次被更新即可。
假设集合 Q Q Q 中 d i s dis d i s 值最小的点为 p p p 。现在将它从 Q Q Q 中取出。
集合 S S S 中的每一个点都曾用来更新过 p p p 点的 d i s dis d i s 值,因此 p p p 点的 d i s dis d i s 值不会再变小。
假设集合 Q Q Q 中的点 q q q 与点 p p p 相连,因为 d i s p ≤ d i s q dis_p\le dis_q d i s p ≤ d i s q ,那么当边权非负时, d i s q dis_q d i s q 加上边权也一定大于等于 d i s p dis_p d i s p 。
因此 p p p 的 d i s dis d i s 值不会再被更新。
而在上面的证明过程中,用到了边权非负的条件。显然,边权为负数时,证明不成立。因此,Dijkstra 只能处理边权全部非负的图。
Dijkstra 的代码实现以及优化
按照上文提到的大致思路模拟即可。代码中使用了 v i s vis v i s 数组。如果 v i s p vis_p v i s p 为假,则 p p p 点在 Q Q Q 集合中,否则 p p p 点在 S S S 集合中。
洛谷 P3371 代码:
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 #include <bits/stdc++.h> using namespace std;#define N 10010 #define M 500010 int n,m,s;int head[N],to[M],qz[M],nxt[M];int dis[N];bool vis[N];#define inf 2147483647 int cnt;void add_edge (int x,int y,int z) { cnt++; to[cnt]=y;qz[cnt]=z;nxt[cnt]=head[x];head[x]=cnt; } void dij () { for (int i=1 ;i<=n;i++) dis[i]=inf; dis[s]=0 ; for (int i=1 ;i<=n;i++) { int u=1 ,minn=inf; for (int j=1 ;j<=n;j++) if (!vis[j]&&dis[j]<minn) u=j,minn=dis[j]; vis[u]=1 ; for (int j=head[u];j;j=nxt[j]) { int v=to[j],c=qz[j]; dis[v]=min (dis[v],dis[u]+c); } } } int main () { ios::sync_with_stdio (0 ); cin.tie (0 );cout.tie (0 ); cin>>n>>m>>s; for (int i=1 ;i<=m;i++) { int u,v,c; cin>>u>>v>>c; add_edge (u,v,c); } dij (); for (int i=1 ;i<=n;i++) cout<<dis[i]<<" " ; return 0 ; }
假设 n n n 为点数,m m m 为边数。
在 dij \operatorname{dij} d i j 函数中,每一次的外层循环,会将一个点放入 S S S 集合中。所以,n n n 次循环后,所有的点都会在 S S S 集合中。循环中“找到 Q Q Q 集合中 d i s dis d i s 值最小的点”这个过程的时间复杂度是 O ( n ) O(n) O ( n ) ,所以总体时间复杂度为 O ( n 2 ) O(n^2) O ( n 2 ) 。在从 Q Q Q 集合中取出每个点时,遍历了这个点的所有出边,相当于在循环过程中遍历了所有的边。所以严格来说,时间复杂度为 Θ ( n 2 + m ) \Theta(n^2+m) Θ ( n 2 + m ) 。
这个时间复杂度非常不理想,考虑对它进行优化。而优化的入手点,便是“找到 Q Q Q 集合中 d i s dis d i s 值最小的点”这个过程。
可以使用 STL 中的优先队列(priority_queue),每当更新一个点的 d i s dis d i s 值时,就将它的 d i s dis d i s 值放入到队列中。显然,从队列取出的值,一定是 Q Q Q 集合中 d i s dis d i s 的最小值。
我们要这样使用优先队列,使其成为小根堆:
1 2 3 4 5 6 7 8 9 10 struct node { int dis,p; node (int _dis,int _p){dis=_dis,p=_p;} }; struct cmp { bool operator () (node a,node b) {return a.dis>b.dis;} }; priority_queue<node,vector<node>,cmp> Q;
洛谷 P4779 代码:
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 #include <bits/stdc++.h> using namespace std;#define N 100010 #define M 200010 struct node { int dis,p; node (int _dis,int _p){dis=_dis,p=_p;} }; struct cmp { bool operator () (node a,node b) {return a.dis>b.dis;} }; priority_queue<node,vector<node>,cmp> Q; int n,m,s;int head[N],to[M],qz[M],nxt[M];int dis[N];bool vis[N];#define inf 2147483647 int cnt;void add_edge (int x,int y,int z) { cnt++; to[cnt]=y;qz[cnt]=z;nxt[cnt]=head[x];head[x]=cnt; } void dij () { for (int i=1 ;i<=n;i++) dis[i]=inf; dis[s]=0 ; Q.push (node (0 ,s)); while (!Q.empty ()) { node t=Q.top ();Q.pop (); int u=t.p; if (vis[u]) continue ; vis[u]=1 ; for (int j=head[u];j;j=nxt[j]) { int v=to[j],c=qz[j]; if (dis[u]+c<dis[v]) { dis[v]=dis[u]+c; Q.push (node (dis[v],v)); } } } } int main () { ios::sync_with_stdio (0 ); cin.tie (0 );cout.tie (0 ); cin>>n>>m>>s; for (int i=1 ;i<=m;i++) { int u,v,c; cin>>u>>v>>c; add_edge (u,v,c); } dij (); for (int i=1 ;i<=n;i++) cout<<dis[i]<<" " ; return 0 ; }
堆优化 dijkstra 的时间复杂度为 O ( m log n ) O(m\log n) O ( m log n ) 。
--------待更新--------