图论初步

一、图

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] 的值设置为一个特殊值(如 0INT_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[u][v]=min(dis[u][v],dis[u][k]+dis[k][v]) \]

注意:应当将 dis 数组初始化为极大值。

2)环

对于一条路径,若起点与终点相同,且这组点对是唯一重复出现的点对,那么这条路径叫做一个

如果一个环所有边的权值为负,那么这个环叫做负环

可以使用 SPFA 算法寻找图中负环。

先证明一个引理:如果一个图中存在从源点能到达的负环,那么在这个图中一定不存在源点到该负环上任意一点的最短路。

可以考虑如下的图:

(1,2)=5
(2,3)=-3
(3,1)=-4

在这个图中,从顶点 \(1\) 出发,每绕负环走一圈,任何顶点的 \(dis\) 值都会变得比原来更小。这样一直绕圈,\(dis\) 就会越来越小,直到趋于负无穷,导致不存在一个实际的最短路径。

我们再证明一个引理:对于一个“不存在源点能到达的负环”的图,从源点出发到达它所有能到达的顶点中,至少存在一条最短路,使得这条最短路上的点数小于或等于总点数 \(n\)

可以反证:假设存在符合上述特征的最短路,使得其点数大于 \(n\),那么一定有某些顶点被走了多次。同时我们知道不存在源点能到达的负环,所以这种走法一定比“不走重复走过的顶点”代价更大。

于是命题得证。

综上所述,对于 SPFA 算法,如果存在一个元素,使得它的进队次数超过 \(n\),那么当前从源点到该点的最短路径上的点数就超过了 \(n\),即存在负环。

8. 生成树问题

(待完善)

二、模板

  1. 图的遍历:P5318

  2. 最长路:P1807

  3. 单源最短路:P4779

  4. 多源最短路:B3611

  5. 最小生成树:P3366

  6. 负环:P3385

  7. 最短路计数:P1144

三、习题

(一)图上 DP 与最短路

  1. P1113 杂务

    定义 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];//返回结果
    }
    
  2. P4568 [JLOI2011] 飞行路线

    题意简述:给你一个有向有权图,可以使用 k 次机会使得某边的边权变为 0,求最短路。

    需要用到分层图。因为我们有 k 次走免费边的机会,所以我们可以将原图复制 k 份,并将处于不同层的图中互相对应的两点以权为 0 的有向边连接,这就相当于走免费边。因为我们复制了 k 份,而且连接的是单向的权为 0 的边,所以恰好最多有 k 次机会走免费边。

    代码上,使用 u+k*n 存层数为 k-1 的节点 u。然后跑 Dijkstra 即可。

  3. P6464 [传智杯 #2 决赛] 传送门

    注意到这题的数据范围小的离谱,所以可以先把原图的 Floyd 跑出来,然后两两枚举要加传送门的点,然后跑 Floyd 即可。

    一定要关注数据范围,它会决定你所使用的算法。

  4. P1119 灾后重建

    回顾 Floyd 板子,一共有三层循环。其中,第一层循环 k 代表了断点。所有的非直接最短路都是从断点转移而来的。

    本题中,如果某一村庄未重建完成,那么该村庄就不能当作断点来进行状态转移。所以需要在 Floyd 里面加一个特判。

    注意到 t 数组给定即升序,那么只需一边 Floyd,一边加合法断点即可。

  5. P2047 [NOI2007] 社交网络

    我们先统计多源最短路条数。

    累计答案时,若 dis[i][k]+dis[k][j]==dis[i][j],则说明节点 k 恰好经过了 ij 的最短路,然后按照题意累计答案即可。

  6. P1875 佳佳的魔法药水

    定义 dis[x] 表示获得药水x的最小价格,那么它必定从x的两个子节点转移而来。各点 dis 的初始值均为各点的点权。

    开一个数组 f[u][v],用于记录节点 uv 共同指向的那个节点。

    我们有两种方案:

    1. 可以使用 uv 药水合成 f[u][v] 药水;
    2. 直接购买 f[u][v] 药水(即 dis[f[u][v]] 的初始值)。

    dis[f[u][v]] 必定从 dis[u]dis[v] 松弛而来。因此把所有节点放到队列里,跑 Dijkstra 最短路即可。

    至于方案计数,参阅“最短路计数”。

  7. P1462 通往奥格瑞玛的道路

    先打出来 Dijkstra 板子,然后往最短路函数里传一个参数 limit,表示这次求最短路的过程中只能经过点权小于或等于 limit 的点。

    将每条道路上扣的血量作为边权。

    对于二分答案 check 函数的部分,如果 dis[n]>b,那么表明走到终点时,扣的血量比原有血量多,即血量降低为负数,进而不合法。

    所以 check 函数中,在以 mid 作为 limit 进行 Dijkstra 之后,返回 dis[n]<=b 即可。

    二分边界 l=起点和终点点权的最大值r=所有点权中的最大值

    注意,我们需要在最开始没有限制地(即把 limit 设置为一个极大值)跑一边最短路,如果 1n 之间没有通路,那么就不合法。

  8. P1948 [USACO08JAN] Telephone Lines S

    和上一题基本一样,但是这道题并不是说在图中不能走任何边权大于 limit 参量的边,而是仅有 k 次机会走。

    先打 Dijkstra 板子,然后传限制参量 limit

    不妨将小于或等于 limit 的边的边权视为 0,大于的视为 1

    贡献转换:合法的贡献视为 0,不合法的贡献视为 1,就可以统计出产生不合法的贡献的次数。

    那么此时求出的 dis[u],就是以 limit 为当前的最大花费的情况下,走到点 u 所需要的请求支援的次数。

    如果 dis[n]<=k,则证明请求支援的次数不超过限制,是合法的答案,反之则不合法。

    所以可以二分答案,找到最小的 mid,输出即可。

posted @ 2025-10-17 13:11  L-Coding  阅读(12)  评论(0)    收藏  举报