专栏文章

图论:从入门到提高

算法·理论参与者 1已保存评论 0

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
0 条
当前快照
1 份
快照标识符
@min8b2ag
此快照首次捕获于
2025/12/01 22:12
3 个月前
此快照最后确认于
2025/12/01 22:12
3 个月前
查看原文

0.小引

图论是个非常好玩的东西,无论是简单的 B3862 还是困难的 P10787甚至是 A+B Problem 都可以用图论解决。

1.图的概念

这里不想用那种肥肠专业的术语,不易理解。
只介绍一些关键的概念。
设一个图 GG 具有 nn 个节点和 mm 条边,其中点编号为 1,2,,n1,2,\dots ,n,边编号为 1,2,m1,2,\dots m
以下节点简称为点。

1.1 点集与边集

1.1.1 点集

设一个集合 V=1,2,nV={1,2\dots ,n},即这个集合里面包含 GG 的所有点,那么就称 VV 是图 GG点集

1.1.2 边集

设一个集合 E=1,2,mE={1,2\dots ,m},即这个集合里面包含 GG 的所有边,那么就称 EE 是图 GG边集此时边数 m=Em=|E|
那么我们就可以把图 GG 表示为 G=(V(G),E(G))G=(V(G),E(G))

1.2 图的类型

1.2.1 有向图

故名思意,有方向的图。
当图 GG 为有向图时,取其中的两个点 u,vu,v,满足 u,vVu,v\in V
那么如果有一条边 eEe\in E 连接了 u,vu,v,那么表示为 e=uve=u\to v,也就是 uu 可以到 vv,但是 vv 到不了 uu,结合图片更好理解。
Ca9bnX.png
此时 ee 是一条有向边。

1.2.2 无向图

故名思意,没有方向的图。
当图 GG 为无向图时,取其中的两个点 u,vu,v,满足 u,vVu,v\in V
那么如果有一条边 eEe\in E 连接了 u,vu,v,那么表示为 e=(u,v)e=(u,v),也就是 uu 可以到 vv,而且 vv 也可以到 uu,结合图片更好理解。
Ca9g9m.png
此时 ee 是一条无向边。

1.2.3 混合图

既有有向边又有无向边的图。

1.3 度

1.3.1 度的基本

与一个顶点 vv 相关联的边的数量称为这个点的度,记作 d(v)d(v),对于一条无向边 (u,v)(u,v),每一条边都对 d(v)d(v)22 的贡献。
无向图的基本定理:每条边都会产生 22 的贡献,记作 uVd(u)=2E\sum_{u\in V}d(u)=2 |E|
如果此时一个点 vv 的度 d(v)=1d(v)=1,则称这个点是叶子节点。

1.3.2 入度与出度

对于一个点 vv,称以 vv 为终点的边数为点 vv 的入度,记作 d(v)d^-(v);称以 vv 为起点的边数为点 vv 的出度,记作 d+(v)d^+(v)
定理1:入度与出度之和等于度,记作 d(v)=d+(v)+d(v)d(v)=d^+(v)+d^-(v)
定理2:入度等于出度等于边数,记作 uVd+(v)=uVd(v)=E\sum_{u\in V}d^+(v)=\sum_{u\in V}d^-(v)=|E|

1.4 路径

  • 途径:连接了若干个相邻的点的序列,记作 u0u1uku_0\to u_1 \dots \to u_k
  • 迹:不经过重复边的途径。
  • 回路:起点等于终点的迹。
  • 环:对于一条回路,除了 u0=uku_0=u_k,没有其他的 ui=uj(i=j,i0,k,j0,k)u_i=u_j(i=j,i\neq 0,k,j\neq 0,k)

2.图的存储

以下的时间复杂度无特殊说明都为遍历图的复杂度。

2.1 邻接矩阵

使用二维数组 g 存边。
  • 对于一条无权边 gi,j=1g_{i,j}=1 表示存在一条边 e=(u,v)e=(u,v)
  • 对于一条有权边 gi,j=wg_{i,j}=w 表示存在一条边 e=(u,v)e=(u,v) 的边权为 ww
空间复杂度:O(n2)O(n^2),时间复杂度:O(n2)O(n^2)
代码CPP
while(m--){
    int u,v,w;
    cin>>u>>v>>w;
    g[u][v]=w;
}

2.2 邻接表

使用 vector<int>g[N] 存边。
  • 对于无权图,g 中一般存放终点。
  • 对于有权图,vector<int>g[N] 可以变为 vector<edge>g[N],结构体中一般存放终点和边权。
空间复杂度:O(m)O(m),时间复杂度:O(n+m)O(n+m)
代码CPP
while(m--){
    int u,v,w;
    cin>>u>>v>>w;
    g[u].push_back({v,w});
}

2.3 前向星

用链表数据存储的邻接表,常数较小。
代码CPP
int head[N],nxt[M<<1],to[M<<1],g[M<<1],cnt=1; 
void add(int u,int v,int w){
	nxt[++cnt]=head[u];
	head[u]=cnt;
	to[cnt]=v;
	g[cnt]=w;
}

2.4 例题

3.图的遍历

3.1 遍历出边

邻接表代码CPP
for(int i=0;i<g[u].size();i++)
	int v=g[i].v,w=g[i].w;
	//...
}
前向星代码CPP
for(int i=head[u];i;i=nxt[i]){
	int v=to[i],w=g[i];
	//...
}

3.2 深搜 DFS

一条路走到底,不撞南墙不回头。
通过递归实现,一般代码结构如下:
CPP
void dfs(int u){
	vis[u]=1;//经过v,打上标记
	for(...){ //遍历出边
		int v=...;
		if(!vis[v]){//如果当前节点没经过 
			dfs(v);
		}
	}
}
如果想了解其底层原理,参考 OI Wiki——DFS(搜索)

3.3 广搜 BFS

一层一层的遍历图。在 BFS 结束时,每个节点都是通过从起点到该点的最短路径访问的。
所以许多最短路算法都是通过 BFS 实现的,见第 4 章。
通过队列实现,一般代码结构如下:
CPP
void bfs(int s){
	queue<int>q;
	vis[s]=1;
	q.push(s);
	while(!q.empty()){
		int u=q.front();q.pop();
		for(...){//遍历
			if(!vis[v]){
				q.push(v);
				vis[v]=1;
			}
		}
	} 
}

4.最短路问题

点与点之间的长度最短的路径。

4.1 Floyd 算法

一种求多源/全源最短路的算法。
fk,i,jf_{k,i,j} 表示从 iijj 只允许经过 1,2,,k1,2,\dots,k 的最短路径。
所以当我们想求 iijj 的最短路时,答案就是 fn,x,yf_{n,x,y}
容易发现,其实 kk 的第一维对答案没有影响。
证明
每个 f[k][i][j] 都是由 f[k-1][i][j] 转移而来的,容易发现 f[k][i][j] 一定不大于 f[k-1][i][j],所以其实 f[k][i][j] 可以直接覆盖,即可以省略第一维,类似 DP 的状态压缩。
所以容易发现,当前的 fi,jf_{i,j} 可以看作 iikk 的最短路径与 kkjj 的最短路径之和再和 fi,jf_{i,j} 取最小值,即 f[i][j]=min(f[i][j],f[i][k]+f[j][k])
代码CPP
void floyd(){
	for(int k=1;k<=n;k++){
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				f[i][j]=min(f[i][j],f[i][k]+f[j][k]);
			}
		}
	}
}

例题

把最短路径化作能否到达,即 if(f[i][k]&&f[k][j]) f[i][j]=1;
以上都是 Floyd 的板子。
以上是变种题,有一定思维难度。

评论

0 条评论,欢迎与作者交流。

正在加载评论...