图论初步
一、图
1. 图的概念
在 OI 中,图是一种数据结构,由节点和连接它们的边构成。
一般使用 \(G = (V, E)\) 表示一个点集为 \(V\),边集为 \(E\) 的图。
一般使用 \((u, v)\) 表示一条从节点 \(u\) 连向节点 \(v\) 的有向边。
连接顶点 \(u\) 的边的个数叫做顶点 \(u\) 的的度。
2. 图的分类
-
按照点或边是否有限:
有限图、无限图。
-
按照边是否有方向:
有向图(所有边都有方向);
无向图(所有边都没有方向);
混合图(一部分边有方向,另一部分没有)。
-
按照边是否有权:
有权图、无权图。
-
按照边权的正负:
负权图、非负权图。
3. 简单图
- 自环:对于有向边 \(e = (u, v)\),若 \(u = v\),那么 \(e\) 叫做自环,即自己指向自己的边。
- 重边:连接同一组顶点的边互为重边。
- 如果一个图没有自环或重边,那么这个图叫做简单图。反之,这个图叫做多重图。
4. 图的连通性
(待学习)
5. 图的存储
在 OI 中,一般使用下列方法来存储图。
1)邻接矩阵
使用一个数组 g 来存边。
如果点 \(u\) 与点 \(v\) 之间存在一条有向边 \((u, v)\) ,其权为 \(w\) ,那么可以使用邻接矩阵表示为 g[u][v]=w 。
特别地,如果如果点 \(u\) 与点 \(v\) 之间不存在边,那么可以将 g[u][v] 与 g[v][u] 的值设置为一个特殊值(如 0 或 INT_MAX )。
优点:查询方便。
缺点:占用空间太大;只适用于没有重边的情况。
适用于稠密图的存储,不适用于稀疏图的存储。
2)邻接表
使用一个动态数组 vector<Edge>g[N] 来存边,其中类型 Edge 为边的类型。
其中 g[u] 存储了点 \(u\) 的所有出边。
邻接表存图几乎没有缺点。
3)链式前向星
是使用一个链表实现邻接表的存图方式。
优点:空间占用非常小(是三种方式中空间相对最小的一个,如果怕炸空间可以用这个)。
缺点:不能快速查询一条边是否存在。
6. 图的遍历
图一般有两种遍历方式:DFS 与 BFS。
1)DFS
指深度优先搜索。
其思想通俗一点说,就是一条道走到黑,即每次都尝试向更深的节点走。
我们一般使用递归法实现图的 DFS 遍历。
vector<int>g[N];
int vis[N];
void DFS(int u){
vis[u]=1;
for(int v:g[u]){
if(vis[v])continue;
DFS(v);
}
}
2)BFS
指广度优先搜索。
该算法每次都访问同层的节点(这是一个重要的性质)。如果该层的节点全都被访问,就访问下一层的节点。
这样的算法设计使得 BFS 在无权图中会始终找到两点间的最短合法路径。
一般使用队列实现图的 BFS 遍历。
vector<int>g[N];
int vis[N];
queue<int>q;
void BFS(int s){
vis[s]=1;
while(!q.empty()){
int u=q.front();
q.pop();
for(int v:g[u]){
if(vis[v])continue;
q.push(v);
vis[v]=1;
}
}
}
7. 特殊的路径
1)最短路
对于一条路径,若它是从节点 \(u\) 到节点 \(v\) 所有路径中边权和最小的那条,那么这条路径叫做从节点 \(u\) 到节点 \(v\) 的最短路。反之,若边权和最大,那么这条路径叫做从节点 \(u\) 到节点 \(v\) 的最长路。
单源最短路指的是从一个指定的源点到其他所有顶点的最短路。
多远最短路指的是图中任意两点间的最短路。
对于寻找两点间的最短路或最长路这类问题,我们可以使用多种算法求解。
A. 两种算法实现单源最短路
a. Dijkstra 算法
是一种基于松弛操作的,用于非负权图上寻找单源最短路的强大算法。
不妨定义 \(dis_u\) 表示从源点到点 \(u\) 的最短路长度。
考虑对于一条有向边 \(e=(u, v)\) ,边权为 \(w\) ,如果 \(dis_u+w<dis_v\) ,那么证明从源点走到点 \(u\) ,经过边 \(e\) 再到达点 \(v\) ,存在一条未发现过的,且比原认为的最短路径还要短的路径。
那么我们就可以更新数组,使得 \(dis_v←dis_u+w\) 。这样的操作被称作松弛。
Dijkstra 算法的一般过程为:
- 将源点的 \(dis\) 值设为 \(0\) ;
- 寻找所有未访问的顶点中 \(dis\) 值最小的;
- 松弛该顶点的所有连边;
- 标记该顶点;
- 重复步骤 2~4 ,直至不存在可处理的点。
该算法的时间复杂度是 \(O(n^2+m)\) 的,其中 \(n\) 为点数,\(m\) 为边数。
b. 堆优化的 Dijkstra 算法
因为我们每次松弛时都要维护未访问顶点中 \(dis\) 值最小的那个,所以我们可以使用一个堆记录这样的点,以及指向该点的边权。
容易证明每条边最多入堆或出堆一次,那么时间复杂度是 \(O(mlogm)\) 的。
值得注意的是,对于稠密图而言,\(n^2\) 与 \(m\) 的数量级相同,此时堆优化的 Dijkstra 算法要明显劣于不加优化的 Dijkstra 算法。所以我们需要根据题目情况选择是否使用堆优化。
c. SPFA 算法
SPFA 在 NOI2018 D1T1「归程」中因被卡常而有"已死"一说。
该算法是经队列优化的 Bellman-Ford 算法。后者因极高的时间复杂度,此处不予介绍。
SPFA 算法的大致步骤如下:
- 将源点的 \(dis\) 值设为 \(0\) 并放入队列;
- 松弛从队头顶点出发的所有边,弹出队头;
- 将松弛成功的边放入一个队列中;
- 重复步骤2~3,直至队列为空。
松弛过程中可能会有同一个点多次入队,这导致算法效率变低。其时间复杂度为 \(O(km)\) 的,其中 \(k\) 是一个常数,通常小于图中顶点数 \(n\) ,但最坏情况下(如"菊花挂网格")时间复杂度可能会变为 \(O(nm)\) 的。
值得注意的是,该算法是支持负权图的。在没有负权边的情况下,Dijkstra 算法的表现一般要比 SPFA 更好。
B. Floyd 算法实现多源最短路
全称 Floyd-Warshall 算法,主体思想是基于动态规划的。
定义数组 dis[u][v] 表示点 \(u\) 到点 \(v\) 的最短路长度。
考虑枚举点 \(k\) ,使得点 \(u\) 到点 \(v\) 的某一路径经过了点 \(k\) ,那么容易得到状态转移方程:
注意:应当将 dis 数组初始化为极大值。
2)环
对于一条路径,若起点与终点相同,且这组点对是唯一重复出现的点对,那么这条路径叫做一个环。
如果一个环所有边的权值为负,那么这个环叫做负环。
可以使用 SPFA 算法寻找图中负环。
先证明一个引理:如果一个图中存在从源点能到达的负环,那么在这个图中一定不存在源点到该负环上任意一点的最短路。
可以考虑如下的图:
(1,2)=5
(2,3)=-3
(3,1)=-4
在这个图中,从顶点 \(1\) 出发,每绕负环走一圈,任何顶点的 \(dis\) 值都会变得比原来更小。这样一直绕圈,\(dis\) 就会越来越小,直到趋于负无穷,导致不存在一个实际的最短路径。
我们再证明一个引理:对于一个“不存在源点能到达的负环”的图,从源点出发到达它所有能到达的顶点中,至少存在一条最短路,使得这条最短路上的点数小于或等于总点数 \(n\)。
可以反证:假设存在符合上述特征的最短路,使得其点数大于 \(n\),那么一定有某些顶点被走了多次。同时我们知道不存在源点能到达的负环,所以这种走法一定比“不走重复走过的顶点”代价更大。
于是命题得证。
综上所述,对于 SPFA 算法,如果存在一个元素,使得它的进队次数超过 \(n\),那么当前从源点到该点的最短路径上的点数就超过了 \(n\),即存在负环。
8. 生成树问题
(待完善)
二、模板
三、习题
(一)图上 DP 与最短路
-
定义
f[u]表示走到从第1个节点走到第u个节点所需的最少时间。对于节点
u的前驱节点v,一定有f[u]=min(f[v])+a[u]。所以我们需要对图进行拓扑排序,在此基础上,保证
f[v]的值在f[u]之前被更新,然后就能进行状态转移了。图上DP拓扑排序的一般写法如下:
int DFS(int u){ if(f[u])return f[u];//如果搜过就直接返回,记忆化搜索。 for(auto v:g[u]){ //找最值 } //转移 return f[x];//返回结果 } -
题意简述:给你一个有向有权图,可以使用
k次机会使得某边的边权变为0,求最短路。需要用到分层图。因为我们有
k次走免费边的机会,所以我们可以将原图复制k份,并将处于不同层的图中互相对应的两点以权为0的有向边连接,这就相当于走免费边。因为我们复制了k份,而且连接的是单向的权为0的边,所以恰好最多有k次机会走免费边。代码上,使用
u+k*n存层数为k-1的节点u。然后跑 Dijkstra 即可。 -
注意到这题的数据范围小的离谱,所以可以先把原图的 Floyd 跑出来,然后两两枚举要加传送门的点,然后跑 Floyd 即可。
一定要关注数据范围,它会决定你所使用的算法。
-
回顾 Floyd 板子,一共有三层循环。其中,第一层循环
k代表了断点。所有的非直接最短路都是从断点转移而来的。本题中,如果某一村庄未重建完成,那么该村庄就不能当作断点来进行状态转移。所以需要在 Floyd 里面加一个特判。
注意到
t数组给定即升序,那么只需一边 Floyd,一边加合法断点即可。 -
我们先统计多源最短路条数。
累计答案时,若
dis[i][k]+dis[k][j]==dis[i][j],则说明节点k恰好经过了i到j的最短路,然后按照题意累计答案即可。 -
定义
dis[x]表示获得药水x的最小价格,那么它必定从x的两个子节点转移而来。各点dis的初始值均为各点的点权。开一个数组
f[u][v],用于记录节点u与v共同指向的那个节点。我们有两种方案:
- 可以使用
u和v药水合成f[u][v]药水; - 直接购买
f[u][v]药水(即dis[f[u][v]]的初始值)。
dis[f[u][v]]必定从dis[u]与dis[v]松弛而来。因此把所有节点放到队列里,跑 Dijkstra 最短路即可。至于方案计数,参阅“最短路计数”。
- 可以使用
-
先打出来 Dijkstra 板子,然后往最短路函数里传一个参数
limit,表示这次求最短路的过程中只能经过点权小于或等于limit的点。将每条道路上扣的血量作为边权。
对于二分答案
check函数的部分,如果dis[n]>b,那么表明走到终点时,扣的血量比原有血量多,即血量降低为负数,进而不合法。所以
check函数中,在以mid作为limit进行 Dijkstra 之后,返回dis[n]<=b即可。二分边界
l=起点和终点点权的最大值,r=所有点权中的最大值。注意,我们需要在最开始没有限制地(即把
limit设置为一个极大值)跑一边最短路,如果1到n之间没有通路,那么就不合法。 -
P1948 [USACO08JAN] Telephone Lines S
和上一题基本一样,但是这道题并不是说在图中不能走任何边权大于
limit参量的边,而是仅有k次机会走。先打 Dijkstra 板子,然后传限制参量
limit。不妨将小于或等于
limit的边的边权视为0,大于的视为1。贡献转换:合法的贡献视为
0,不合法的贡献视为1,就可以统计出产生不合法的贡献的次数。那么此时求出的
dis[u],就是以limit为当前的最大花费的情况下,走到点u所需要的请求支援的次数。如果
dis[n]<=k,则证明请求支援的次数不超过限制,是合法的答案,反之则不合法。所以可以二分答案,找到最小的
mid,输出即可。

浙公网安备 33010602011771号