图论略谈
感谢此 blog。
1.定义
图:一张图 \(G\) 由若干个点和连接这些点的边构成。点的集合称为 点集 \(V\),边的集合称为 边集 \(E\),记 \(G=(V,E)\)。
阶:图 \(G\) 的点数 \(|V|\) 称为 阶,记作 \(|G|\)。
无向图:若 \(e\in E\) 没有方向,则 \(G\) 称为 无向图。无向图的边记作 \(e = (u, v)\),\(u,v\) 之间无序。
有向图:若 \(e\in E\) 有方向,则 \(G\) 称为 有向图。有向图的边记作 \(e = u\to v\) 或 \(e=(u,v)\),\(u,v\) 之间有序。无向边 \((u,v)\) 可视为两条有向边 \(u\to v\) 和 \(v\to u\)。
重边:端点和方向(有向图)相同的边称为 重边。
自环:连接相同点的边称为 自环。
相邻:在无向图中,称 \(u,v\) 相邻 当且仅当存在 \(e=(u,v)\)。
邻域:在无向图中,点 \(u\) 的 邻域 为所有与之相邻的点的集合,记作 \(N(u)\)。
邻边:在无向图中,与 \(u\) 相连的边 \((u,v)\) 称为 \(u\) 的 邻边。
出边 / 入边:在有向图中,从 \(u\) 出发的边 \(u\to v\) 称为 \(u\) 的 出边,到达 \(u\) 的边 \(v\to u\) 称为 \(u\) 的 入边。
度数:一个点的 度数 为与之关联的边的数量,记作 \(d(u)\),\(d(u) = \sum_{e\in E} ([u = e_u] + [u = e_v])\)。点的自环对其度数产生 \(2\) 的贡献。
出度 / 入度:在有向图中,从 \(u\) 出发的边数称为 \(u\) 的 出度,记作 \(d^+(u)\);到达 \(u\) 的边数称为 \(u\) 的 入度,记作 \(d^-(u)\)。
途径:连接一串相邻结点的序列称为 途径,用点序列 \(v_{0..k}\) 和边序列 \(e_{1..k}\) 描述,其中 \(e_i = (v_{i - 1}, v_i)\)。常写为 \(v_0\to v_1\to \cdots \to v_k\)。
迹:不经过重复边的途径称为 迹。
回路:\(v_0 = v_k\) 的迹称为 回路。
路径:不经过重复点的迹称为 路径,也称 简单路径。不经过重复点比不经过重复边强,所以不经过重复点的途径也是路径。注意题目中的简单路径可能指迹。
环:除 \(v_0 = v_k\) 外所有点互不相同的途径称为 环,也称 圈 或 简单环。
简单图:不含重边和自环的图称为 简单图。
基图:将有向图的有向边替换为无向边得到的图称为该有向图的 基图。
有向无环图:不含环的有向图称为 有向无环图,简称 DAG(Directed Acyclic Graph)。
完全图:任意不同的两点之间恰有一条边的无向简单图称为 完全图。\(n\) 阶完全图记作 \(K_n\)。
树:不含环的无向连通图称为 树,树上度为 \(1\)
的点称为 叶子。树是简单图,满足 \(|V| = |E| + 1\)。若干棵(包括一棵)树组成的连通块称为 森林。
稀疏图 / 稠密图:\(|E|\) 远小于 \(|V|^2\) 的图称为 稀疏图,\(|E|\) 接近 \(|V|^2\) 的图称为 稠密图。用于讨论时间复杂度为 \(\mathcal{O}(|E|)\) 和 \(\mathcal{O}(|V|^2)\) 的算法。
子图:满足 \(V'\subseteq V\) 且 \(E'\subseteq E\) 的图 \(G' = (V', E')\) 称为 \(G = (V, E)\) 的 子图,记作 \(G'\subseteq G\)。要求 \(E'\) 所有边的两端均在 \(V'\) 中。
导出子图:选择若干个点以及两端都在该点集的所有边构成的子图称为该图的 导出子图。导出子图的形态仅由选择的点集 \(V'\) 决定,记作 \(G[V']\)。
生成子图:\(|V'| = |V|\) 的子图称为 生成子图。
极大子图(分量):在子图满足某性质的前提下,子图 \(G'\) 称为 极大 的,当且仅当不存在同样满足该性质的子图 \(G''\) 使 \(G'\subsetneq G''\subseteq G\)。\(G'\) 称为满足该性质的 分量。例如,极大的连通的子图称为原图的连通分量,也就是我们熟知的连通块。
连通:对于无向图的两点 \(u,v\),若存在途径使得 \(v_0 = u\) 且 \(v_k = v\),则称 \(u, v\) 连通。
弱连通:对于有向图的两点 \(u, v\),若将有向边改为无向边后 \(u, v\) 连通,则称 \(u, v\) 弱连通。
连通图:任意两点连通的无向图称为 连通图。
弱连通图:任意两点弱连通的有向图称为 弱连通图。
可达:对于有向图的两点 \(u, v\),若存在途径使得 \(v_0 = u\) 且 \(v_k = v\),则称 \(u\) 可达 \(v\),记作 \(u \rightsquigarrow v\)。
🈹点:在无向图中,删去后使得连通分量数增加的点称为 割点。
🈹边:在无向图中,删去后使得连通分量数增加的边称为 割边,也称 桥。
孤立点和孤立边的两个端点都不是割点,但孤立边是割边。非连通图的割边为其每个连通分量的割边的并。
为什么割点和割边这么重要?对于无向连通图上的非割点,删去它,图仍然连通,但删去割点后图就不连通了。因此割点相较于非割点对连通性有更大的影响。割边同理。
点双连通图:不存在割点的无向连通图称为 点双连通图。根据割点的定义,孤立点和孤立边均为点双连通图。
边双连通图:不存在割边的无向连通图称为 边双连通图。根据割边的定义,孤立点是边双连通图,但孤立边不是。
点双连通分量:一张图的极大点双连通子图称为 点双连通分量(V-BCC),简称 点双。
边双连通分量:一张图的极大边双连通子图称为 边双连通分量(E-BCC),简称 边双。
将某种类型的连通分量根据等价性或独立性缩成一个点的操作称为 缩点,原来连接两个不同连通分量的边在缩点后的图上连接对应连通分量缩点后形成的两个点。根据连通分量的类型不同,缩点可分为无向图上的点双连通分量缩点,边双连通分量缩点,以及有向图上的强连通分量缩点。
边双和点双缩点后均得到一棵树,而强连通分量缩点后得到一张有向无环图。
点双连通:若 \(u, v\) 处于同一个点双连通分量,则称 \(u, v\) 点双连通。一个点和它自身点双连通。由一条边直接相连的两点也是点双连通的。
边双连通:若 \(u, v\) 处于同一个边双连通分量,则称 \(u, v\) 边双连通。一个点和它自身边双连通,但由一条边直接相连的两点不一定边双连通。
点双连通和边双连通是无向图连通性相关最基本的两条性质。
点双连通和边双连通有若干等价定义,这里选取一个供参考。
强连通:对于有向图的两点 \(u,v\),若它们相互可达,则称 \(u,v\) 强连通,这种性质称为 强连通性。
显然,强连通是等价关系,强连通性具有传递性。
强连通图:满足任意两点强连通的有向图称为 强连通图。它等价于图上任意点可达其它所有点。
强连通分量:有向图的极大强连通子图称为 强连通分量(Strongly Connected Component,SCC)。
强连通分量在求解与有向图可达性相关的题目时很有用,因为在只关心可达性时,同一强连通分量内的所有点等价。
以下用 \(n\) 代表点集 \(|V|\) 的大小,用 \(m\) 代表边集 \(|E|\) 的大小
2.存图
vector
vector<ll> e[N],e1[N];
void add(vector<ll> &e,ll v){e.pb(v);}
链式前向星
struct edg{
ll hd[N],nt[N],l[N],to[N],cc=1;
void clear(){cc=1,memset(hd,0,sizeof(hd));}
void add(ll u,ll v){cc++,nt[cc]=hd[u],hd[u]=cc,to[cc]=v,l[cc]=u;}
void lj(ll u,ll v){add(u,v),add(v,u);}
}ed,ed1;
3.无向图联通:双联通分量
定义见1。
若要求边双、点双,我们需要先求割边、割点。
时间戳
在对图进行 \(DFS\) 时,按照每个节点第一次被访问的时间顺序,依次给予 \(N\) 个节点 \(1 \! \sim \!N\) 的整数标记,该标记就被称为“时间戳”,记为 \(dfn[x]\)。
搜索树
在无向连通图中任选一点进行 \(DFS\),已经经过的点不再访问,经过的所有边与点构成的树被称为“无向连通图的搜索树”。对于一般无向图可看成多个无向连通图构成了“搜索森林”。
不难发现割边都是非树边。
\(Tarjan\) 求割边与割点
以下默认边 \((u,v)\) 在搜索树中 \(u\) 为 \(v\) 的父节点。
若要判断一条边是不是割边,则只需判断是否存在一条从 \(v\) 出发,经过了非树边的路径,到达时间戳大于等于 \(dfn[u]\) 的节点。
若存在,则 \(u,v\) 之间至少存在两条路径,\((u,v)\) 一定不为割边,否则就只存在 \((u,v)\) 一条路径,因此 \((u,v)\) 一定为割边
因此,我们考虑引入一个叫“追溯值”的东东,记作 \(low[x]\),表示从以 \(x\) 为根的子树内任意节点出发,经过一条非树边能到达的节点的时间戳(没有则为其本身的时间戳)最小为多少。
计算 \(low[x]\) 可最开始将 \(low[x]\) 初始化为 \(dfn[x]\),对于从 \(x\) 出发的每条边 \(x\) 到 \(y\):
若 \(y\) 已访问过,则令 $low[x]=\min(low[x],dfn[y]) $。
若 \(y\) 未访问过,则访问 \(y\),后令 $low[x]=\min(low[x],low[y]) $。
由 \(low\) 与 \(dfn\) 的定义可得:
边 \((u,v)\) 为割边需满足:
点 \(x\) 为割点需满足:
- 若 \(x\) 不为根节点,则至少有一个 \(x\) 的子节点 \(y\) 满足:\[dfn[x]\le low[y] \]
- 若 \(x\) 为根节点,则须有至少两个满足。
代码:
求割边:
代码中la是连接父节点与当前节点的边的编号,为了防止重边。
ll dfn[N],low[N],dcnt,cut[N];
void Tarjan(ll x,ll la){
dfn[x]=low[x]=++dcnt;
for(int i=ed.hd[x];i;i=ed.nt[i]){
ll v=ed.to[i];
if(!dfn[v]){
Tarjan(v,(i^1));
low[x]=min(low[x],low[v]);
if(dfn[x]<low[v]){
cut[i]=cut[i^1]=1;
}
}
else if(i!=la) low[x]=min(low[x],dfn[v]);
}
}
调用:
for(int i=1;i<=n;i++) if(!dfn[i]) Tarjan(i,0);
求割点:
因为求割点是小于等于号,因此不用考虑父节点。
ll dfn[N],low[N],dcnt,cut[N],root;
void Tarjan(ll x){
dfn[x]=low[x]=++dcnt;
ll flag=0;
for(int i=ed.hd[x];i;i=ed.nt[i]){
ll v=ed.to[i];
if(!dfn[v]){
Tarjan(v);
low[x]=min(low[x],low[v]);
if(dfn[x]<=low[v]){
flag++;
if(flag>1||x!=root) cut[x]=1;
}
}
else low[x]=min(low[x],dfn[v]);
}
}
调用:
for(int i=1;i<=n;i++) if(!dfn[i]) root=i,Tarjan(i);
时间复杂度:\(\mathcal{O}(n+m)\)。
\(Tarjan\) 求边双
由边双定义可知,去掉所有割边后,剩下的每个连通块自成一个边双,且每个点只隶属于一个边双。因此只需求出所有割边,跑一遍 \(DFS\) 即可。
代码:
求出每个点所属的边双。
ll dfn[N],low[N],dcnt,cut[N];
void Tarjan(ll x,ll la){
dfn[x]=low[x]=++dcnt;
for(int i=ed.hd[x];i;i=ed.nt[i]){
ll v=ed.to[i];
if(!dfn[v]){
Tarjan(v,(i^1));
low[x]=min(low[x],low[v]);
if(dfn[x]<low[v]){
cut[i]=cut[i^1]=1;
}
}
else if(i!=la) low[x]=min(low[x],dfn[v]);
}
}
ll dcc[N],dcct;
void T_dfs(ll x){
dcc[x]=dcct;
for(int i=ed.hd[x];i;i=ed.nt[i]){
if(cut[i])continue;
ll v=ed.to[i];
if(!dcc[v]) T_dfs(v);
}
}
for(int i=1;i<=n;i++) if(!dfn[i]) Tarjan(i,0);
for(int i=1;i<=n;i++) if(!dcc[i]) dcct++,T_dfs(i);
边双缩点
没啥好说的,上代码:
for(int i=2;i<=cc;i++){
ll u=ed.l[i],v=ed.to[i];
if(c[u]==c[v])continue;
ed1.add(c[u],c[v]);
}
求点双
对于孤立点很明显是点双,因此只需考虑节点个数大于 \(1\) 的部分即可。
不难发现两个点双之间若有交,则交的所有点都为割点,且对于每一个不是割点的点,只隶属于一个点双。
因此我们可以考虑在搜索树上对于每个割点向下遍历,遍历到割点或叶节点就返回,所有遍历到的点就形成一个点双。
详细的,维护一个栈:
- 对于第一次访问的节点,入栈。
- 对于每一个满足割点判定法则的点 \(u,v\),不断弹出栈顶,直到弹出 \(v\)。
- 弹出的节点与 \(u\) 构成一个点双。
代码:
ll dfn[N],low[N],dcnt,cut[N],root;
ll zh[N],top,dcct;
vector<ll> dcc[N];
void Tarjan(ll x){
dfn[x]=low[x]=++dcnt;
if(x==root && !ed.hd[x]){
dcc[++dcct].pb(x);
return;
}
zh[++top]=x;
ll flag=0;
for(int i=ed.hd[x];i;i=ed.nt[i]){
ll v=ed.to[i];
if(!dfn[v]){
Tarjan(v);
low[x]=min(low[x],low[v]);
if(dfn[x]<=low[v]){
flag++;
if(flag>1||x!=root) cut[x]=1;
dcct++;
ll k=0;
do{
k=zh[top--];
dcc[dcct].pb(k);
}while(k!=v);
dcc[dcct].pb(x);
}
}
else low[x]=min(low[x],dfn[v]);
}
}
调用
for(int i=1;i<=n;i++) if(!dfn[i]) root=i,Tarjan(i);
点双缩点
可知割点是连接点双的桥梁,正如割边是连接边双的桥梁。用一个点代表一个点双,并将点双代表点向它包含的割点连边,得到 块割树(Block-Cut Tree,“点双连通块 - 割点” 树,和圆方树有细微差别)。
广义圆方树:将原图的点视为圆点,对于原图的每个点双,删去其中所有边,新建代表该点双的方点连向点双内所有圆点,形成的结构称为圆方树。每个点双缩成一张菊花图,多个菊花图通过原图割点连接(割点是点双的分隔点),类比边双缩点时,每个边双缩成一个点,多个边双通过原图割边连接。
缩点应用
当在原图上需要进行某些受环影响的操作时,可以考虑使用缩点使其变为一棵树。或是求必经点必经边、任意两点之间需要至少两条路径时也可用缩点,此外缩点还有许多应用,这里不再赘述。
4.有向图可达:强联通分量
定义
见上。
有向图 \(DFS\) 树
不同于无向图,对于弱连通图 \(G\),从某一点出发 \(DFS\),不一定能访问到图上的所有结点。一般而言,有向图的“\(DFS\) 树” 是一个森林:按任意顺序遍历每个结点,若当前点没有 \(DFS\) 过,则从该点开始 \(DFS\),并按构建无向图 \(DFS\) 树的方式构建以该点为根的有向图 \(DFS\) 树。这会形成若干棵 \(DFS\) 树,在研究强连通性时,它们之间相互独立。对于任意强连通的两点 \(u,v\),因其互相可达,所以第一次访问它们所在的强连通分量的 \(DFS\) 树一定同时包含 \(u,v\)。因此不同 \(DFS\) 树之间的任意两点不强连通,在求强连通分量时只需逐个考虑每棵 \(DFS\) 树。
考察有向图 \(DFS\) 树 \(T\) 的形态,从而得到判定 \(SCC\) 的准则。
在得到 \(DFS\) 树后,对于有向图的每条边,必然是以下四种之一:
- 树枝边:搜索树中的边。
- 前向边:从祖先指向后代的非树边。
- 后向边:从后代指向祖先的非树边。
- 横叉边:两端无祖先后代关系的非树边,若此种边由 \(u\) 指向 \(v\),则一定 \(dfn[v]<dfn[u]\)。
前向边 \(u\to v\) 并无何用,\(u\) 本就能到达 \(v\)。
后向边 \(u\to v\) 可使搜索树上从 \(v\) 到 \(u\) 之间的节点(包括 \(u,v\))都互相强连通。
横叉边 \(u\to v\) 若 \(v\) 可达 \(u\) 的祖先,则 \(u,v\) 之间强连通。
\(Tarjan\) 求强连通分量
由强连通的传递性可知一个点只隶属于一个强连通分量,且一个强连通分量对于求其他强连通分量是没有作用的。
因此考虑维护 \(low[x]\) 表示以 \(x\) 为根的子树中,通过后向边可达到的节点与通过横叉边可达到的能达到 \(x\) 祖先的节点的时间戳的最小值。
则强连通分量为每个以 \(dfn[x]=low[x]\) 的节点 \(x\) 为根的子树刨去其他强连通分量后剩下的点。
若 $dfn[x]>low[x] $,则以 \(x\) 为根的子树中有节点与 \(x\) 不强连通,无法更新答案。
若 $dfn[x]<low[x] $,则以 \(x\) 为根的子树刨去其他强连通分量后剩下的点形成的强连通图不是极大的,也无法更新答案。
综上所述,考虑维护一个栈,节点第一次被访问就将其入栈。
对于节点 \(x\):
-
初始化 $low[x]=dfn[x] $。
-
对于 \(x\) 遍历到的的节点 \(y\) 若 \(y\) 未被访问,则访问 \(y\),后令 $low[x]=\min(low[x],low[y]) $。
否则若 \(y\) 在栈中,则令 $low[x]=\min(low[x],dfn[y]) $。
- 回溯前若 \(x\) 满足 $low[x]=dfn[x] $,则不断弹出栈中节点,直到弹出 \(x\) 为止。
代码:
ll dfn[N],low[N],dcnt,top,scct,zh[N],zt[N];
vector<ll> scc[N];
void Tarjan(ll x){
dfn[x]=low[x]=++dcnt;
zh[++top]=x;zt[x]=1;
for(int i=ed.hd[x];i;i=ed.nt[i]){
ll v=ed.to[i];
if(!dfn[v]) Tarjan(v),low[x]=min(low[x],low[v]);
else if(zt[v]) low[x]=min(low[x],dfn[v]);
}
if(dfn[x]==low[x]){
ll k=0;
scct++;
do{
k=zh[top--];zt[k]=0;
c[k]=scct;scc[scct].pb(k);
}while(k!=x);
}
}
调用:
for(int i=1;i<=n;i++)if(!dfn[i])Tarjan(i);
时间复杂度:\(\mathcal{O}(n+m)\)。
强连通分量缩点
与边双缩点类似,缩完点图变成 \(DAG\),但有重边,注意判别。
强连通缩点的应用
在判定可达性、图需要变为 \(DAG\) 求解时,可以考虑使用缩点。
5. 最短路
最短路:在一张图上,称 \(s\) 到 \(t\) 的 最短路 为最短的连接 \(s\) 到 \(t\) 的路径。若不存在这样的路径(不连通或不可达),或最小值不存在(存在可经过的负环),则最短路不存在。
记 \(s\) 表示最短路起点,\(t\) 表示最短路终点。
单源最短路
问题描述:给定 源点 \(s\),求 \(s\) 到图上每个点 \(u\) 的最短路长度 \(D_u\)。
设 \(dis_u\) 表示 \(s\) 到 \(u\) 的估计最短路长度,初始化 \(dis_s=0\) 和 \(dis_u = +\infty\)(\(u!=s\)),算法结束时应有 \(dis_u=D_u\)。
接下来介绍该问题的几种常见解法。
Dijkstra
只能处理非负权图。
一个点向其他点扩展,假如扩展是有用的,则要保证 \(dis_u=D_u\)。什么点可保证呢?就是 \(dis\) 最小的点,因为其他点的 \(dis\) 已经比他大了,边权非负,则扩展到他一定不会更新。
因此我们可以每次选择 \(dis\) 最小的点扩展,每个点至多扩展一次。
取最小点可用优先队列优化,每取出最小点扩展,若一个点可被更新,则将其更新,随后入队。
一个点会入队出队多次,但扩展只需一次,所以用一个标记数组存储当前点是否被扩展便可。
时间复杂度:\(\mathcal{O}(m\log m)\)。
代码:
priority_queue<pair<ll,ll> > q,kd;
ll v[N];
void Dijkstra(ll qd,ll (&di)[N] ){
memset(di,0x3f,sizeof(di));
memset(v,0,sizeof(v));
di[qd]=0;q=kd;
q.push(mp(0,qd));
while(!q.empty()){
ll x=q.top().second;q.pop();
if(v[x])continue;
v[x]=1;
for(int i=0;i<e[x].size();i++){
ll y=e[x][i],ww=z[x][i];
if(di[y]>di[x]+ww){
di[y]=di[x]+ww;
q.push(mp(-di[y],y));
}
}
}
}
其中 \(qd\) 为源点,\(di\) 数组最后为 \(D_u\)。

浙公网安备 33010602011771号