「Note」图论方向 - 图论基础
1. 差分约束
1.1. 简介
差分约束算法用于解决如下问题:给出若干形如 \(x_a-x_b\le c\) (均为整数,可以为负数)的不等式,求一组解 \(\{x_i\}\),若不存在解则判断无解。
考虑将原式变形,变为 \(x_a\le x_b+c\)。观察到这与单源最短路里的三角形不等式 \(dis_a\le dis_b+w\) (\(w\) 为节点 \(a,b\) 之间的某边边权)相似。我们使 \(x_i\) 为从源点到节点 \(i\) 的最短路径,对于不等式 \(x_a\le x_b+c\) 我们从节点 \(b\) 向节点 \(a\) 连一条边权为 \(c\) 的有向边。特殊地,我们建立一个源点 \(0\),从源点向所有节点连一条边权为 \(0\) 的有向边,并以源点作为最短路起点。
我们一般采用 SPFA 进行最短路,若图中存在负环,则差分约束系统无解。
特殊地,有些题也可转化为最长路形式进行拓扑排序,因题而异。
1.2. 常见技巧
1.2.1. 常见变形
原式 | 变形 | 建边 |
---|---|---|
\(x_a-x_b\le c\) | \(x_a\le x_b+c\) | add(b,a,c) |
\(x_a-x_b\ge c\) | \(x_b\le x_a-c\) | add(a,b,-c) |
\(x_a-x_b<c\) | \(x_a\le x_b+c-1\) | add(b,a,c-1) |
\(x_a-x_b>c\) | \(x_b\le x_a-c-1\) | add(a,b,-c-1) |
\(x_a=x_b\) | \(x_a-x_b\le0,x_b-x_a\le0\) | add(a,b,0),add(b,a,0) |
1.2.2. 简单性质
显著的,将我们得出的一组解 \(x_i\) 整体加减一个常量不影响正确性,因为都消掉了。
1.3. 例题
\(\color{limegreen}{P5960}\)
差分约束模板题。
$\text{Code}$:
#define LL long long
#define UN unsigned
#include<bits/stdc++.h>
using namespace std;
//--------------------//
const int N=1e5+2,M=1e5+2;
int n,m;
//----------//
//Edge
struct Edge
{
int to,w,nex;
}edge[M];
int tot,head[N];
void add(int from,int to,int w)
{
edge[++tot].nex=head[from];
head[from]=tot;
edge[tot].to=to;
edge[tot].w=w;
return;
}
//--------------------//
//SPFA
bool vq[N];
int v[N],dis[N];
queue<int>q;
void SPFA(int st)
{
//printf("ST:%d\n",st);
memset(v,0,sizeof(v));
memset(vq,0,sizeof(vq));
memset(dis,0x3f,sizeof(dis));
dis[st]=0;
vq[st]=true;
q.push(st);
while(!q.empty())
{
int now=q.front();
//printf("now:%d\n",now);
q.pop();
vq[now]=false,v[now]++;
if(v[now]>n)
printf("NO"),exit(0);
for(int to,w,i=head[now];i;i=edge[i].nex)
{
to=edge[i].to,w=edge[i].w;
//printf("to:%d %d %d\n",to,dis[to],dis[now]+w);
if(dis[now]+w<dis[to])
{
dis[to]=dis[now]+w;
//printf("%d %d %d\n",dis[to],dis[now],w);
if(!vq[to])
q.push(to),vq[to]=true;
}
}
}
}
//-------------------//
int main()
{
scanf("%d%d",&n,&m);
for(int from,to,w,i=1;i<=m;i++)
{
scanf("%d%d%d",&to,&from,&w);
add(from,to,w);
}
for(int i=1;i<=n;i++)
add(n+1,i,0);
SPFA(n+1);
for(int i=1;i<=n;i++)
{
if(dis[i]==0x3f3f3f3f)
dis[i]=0;
printf("%d ",dis[i]);
}
return 0;
}
\(\color{royalblue}{P3275}\)
将不等式列出之后注意转化、建边细节,这道题用 SPFA 会被卡,需要转化为最长路用拓扑排序求解。
\(\color{blueviolet}{P3530}\)
非常好题目。
一个强连通分量答案固定,多个之间互不影响,考虑一个强连通分量贡献。
考虑此题一个特点,两个点点权相差不超过 \(1\),也就是说强连通分量中点权极值 \(v_x\) 到 \(v_y\) 中每个值都能去到,自然而然答案就是最长路加一,即 \(\min\{dis(i,j) + 1\}\)(\(i,j\) 都为其中的点),Floyd 求解即可。
2. 有向图连通性:强连通分量
2.1. 强连通定义
- 强连通:对于有向图两点 \(x,y\),若他们互相可达,则称 \(x,y\) 强连通,这种性质为强连通性。
- 强连通图:满足任意两点强连通的有向图称为强连通图。
- 强连通分量:有向图的极大强连通子图称为强连通分量(SCC)。
显著的,强连通性具有传递性,并且强连通的两者等价。当在做某些只关心连通性的问题时,一个强连通分量内所有节点等价,便于做题。
2.2. 有向图 DFS 树
遍历每一个节点,若此节点未被访问,则以此节点为根进行 DFS,对于整个图搜索完后可以得到一个有向图 DFS 森林。
对于一棵 DFS 树,主要有以下 \(4\) 种边:
- 树边:每次从父节点向子节点进行 DFS 的边。
- 返祖边:DFS 时访问到当前节点祖先的边。
- 横叉边:DFS 时访问到非当前节点子树内且不是当前节点祖先的点的边。
- 前向边:DFS 时访问到当前节点子树内已经访问过的边。
对于两点 \(x,y\),设 \(d_{x,y}=lca(x,y)\),\(fa_i\) 为节点 \(i\) 的父节点,\(T_i\) 为节点 \(i\) 的子树,\(dfn_i\) 为节点 \(i\) 的时间戳。
我们不难发现如下性质:
- 对于返祖边 \(x\to y\),它使得 \(x\) 到 \(y\) 的 DFS 树上路径上的节点强连通。
- 对于横叉边 \(x\to y\),有 \(dfn_y<dfn_x\)。
- 对于前向边 \(x\to y\),删去它并不会影响连通性。
横叉边、前向边均可减小时间戳,而树边和前向边会增大时间戳。由于删去前向边并不会影响连通性,在接下来的证明中,我们忽略前向边的影响。
为了研究横叉边对连通性的影响,我们假设存在节点 \(x,y\) 使得 \(dfn_y<dfn_x\),并且从节点 \(y\) 可达节点 \(x\)。考虑到 \(dfn_y<dfn_x\),而且我们的图中只有树边可以增大时间戳(前向边已经被忽略),所以 \(y\) 到 \(x\) 的路径上一定存在至少一条树边,且一定是通过某一条树边从节点 \(fa_x\) 达到 \(x\)。
所以若 \(y\) 可达 \(x\) 并且 \(dfn_y<dfn_x\),那么 \(y\) 可达 \(fa_x\),那么 \(y\) 一定可达 \(d_{x,y}\)。
相反地,若 \(y\) 可达 \(d_{x,y}\),显然 \(y\) 可达 \(d_{x,y}\)。(别忘了前提 \(dfn_y<dfn_x\)。)
可推出结论:
- 若 \(dfn_y<dfn_x\),\(y\) 可达 \(x\) 当且仅当 \(y\) 可达 \(d_{x,y}\)。
重新考虑横叉边对连通性的影响,设横叉边 \(x\to y\),利用上述结论,不难发现当且仅当 \(y\) 可达 \(d_{x,y}\) 时,\(x,y\) 强连通。
进一步推出:
- 若 \(x,y\) 强连通,则 \(x,y\) 路径上的所有点强连通。
2.3. Tarjan 求有向图 SCC
对于每一个 SCC,定义它在 DFS 树上的最浅的那个节点为其“关键点”,我们尝试在一个关键点出求出它所对应的 SCC。显著的,一个关键点有且只有一个对应的 SCC。
对于一个点 \(x\),若它不是关键点,一定有 \(y\in T_x\) 使得有边 \(y\to z\),保证 \(dfn_z<dfn_x\)(这条边是返祖边或者横叉边)。
我们定义 \(low_x\) 为以下节点的 \(dfn\) 最小值:
- \(T_x\) 中的节点。
- 一条返祖边 \(y\to z,y\in T_x\),其指向的节点 \(z\)。
- 一条横叉边 \(y\to z,y\in T_x\),若 \(z\) 可达 \(d_{y,z}\),则包括节点 \(z\)。
若 \(x\) 为关键点,则一定有 \(low_x=x\)(我们把 \(low_x\) 初始化为 \(x\))。比较显著的特性,考虑分类讨论每种边的情况即可,这里省略证明。
我们已知如何寻找关键点,现在考虑求强连通分量。
考虑强连通分量的性质,每个 DFS 树中,强连通分量都是弱连通的(显著)。
根据此性质我们每次找到深度最大的强连通分量,将它与它子树内未被删除的点作为一个强连通分量,然后再删除整个强连通分量。
我们可以用栈维护深搜过的节点,一旦找到整个强连通分量就将其弹出(弹到关键点弹出为止,关键点是深度最小的),使弹出的所有点成为一个 SCC。
现在唯一的问题就是如何求解 \(low\),在回溯时用搜过的直系节点的 \(low,dfn\) 更新当前节点的 \(low\),仍然考虑分类讨论边的种类。
对于现在搜索的到的节点 \(x\),存在一条边 \(x\to y\)。
- 若其为树边,\(low_x\leftarrow\min\{low_x,low_y\}\)。
- 若其为返祖边,\(low_x\leftarrow\min\{low_x,dfn_y\}\)。
- 若其为横叉边,需要判断 \(y\) 是否与 \(x\) 强连通,即判断 \(y\) 是否在栈中,若强连通,\(low_x\leftarrow\min\{low_x,dfn_y\}\)。
- 若其为前向边,\(low_x\leftarrow\min\{low_x,dfn_y\}\),这并不会改变 \(low_x\) 的值,因为一定有 \(dfn_y>low_x\)。
对于返祖边 \(x\to y\) 来说,\(y\) 一定未出栈,所以在实现时可以采用与横叉边相同的判断方式;对于前向边,如何处理都无所谓。
综上,我们得到了求 \(low\) 的方式。
- \(low_x\) 初始值设为 \(dfn_x\)。
- 若当前边 \(x\to y\) 为树边,\(low_x\leftarrow\min\{low_x,low_y\}\)。
- 若当前边 \(x\to y\) 为非树边,并且 \(y\) 未出栈,\(low_x\leftarrow\min\{low_x,dfn_y\}\)。
2.4. 常用技巧
2.4.1. 缩点
在一定条件下,对于只关心连通性的问题时,一个 SCC 内所有节点等价,因此我们可以将一个 SCC 看做一个点,建出一张新图(一定是一张 DAG),进一步计算。
2.4.2. 优化拓扑排序
对于两个 SCC \(S_1,S_2\),若 \(S_1\) 可达 \(S_2\),则 \(S_1\) 比 \(S_2\) 后出栈。我们按照出栈顺序依次为 SCC 编号,便可以得到 SCC 的拓扑序,所以倒序遍历 SCC,相当于拓扑排序遍历缩点后 DAG,省去了拓扑排序。
2.5. 例题
\(\color{limegreen}{P3387}\)
缩点模板,把 SCC 缩起来后在新图(DAG)上 DP 即可。
\(\color{limegreen}{B3609}\)
强连通分量模板。
\(\color{royalblue}{P3627}\)
简单题,跟板子没啥区别,缩点后 DAG 上 DP。
3. 无向图连通性:双连通分量
3.1. 基本定义
- 割点:无向图中,删去此节点使得连通分量数量增加,则称此节点为割点。
- 割边:无向图中,删去此边使得连通分量数量增加,则称此节点为割边。
连通分量也就是熟悉的连通块。
分量(极大子图):在满足一定条件下,当且仅当 \(\forall G''\) 满足条件使得 \(G'\subsetneq G''\subseteq G\),则称 \(G'\) 为满足此条件的分量。
- 点双连通分量:不存在割点的分量。
- 边双连通分量:不存在割边的分量。
特殊地,孤立点不是割点,是点双连通图,是边双连通分量;孤立边的端点不是割点,孤立边是割边,是点双连通图,不是边双连通图。
- 点双连通:若 \(x,y\) 处于同一个点双,则称 \(x,y\) 点双连通。
- 边双连通:若 \(x,y\) 处于同一个边双,则称 \(x,y\) 边双连通。
3.2. 基本性质
3.2.1. 边双
-
考虑一条割边 \((u, v)\),以及其两侧任意两点 \(x, y\),有任意 \(x, y\) 之间路径都包含割边 \((u, v)\)。
-
与强连通分量类似,考虑将一个边双中的所有点缩成一个新点,并建出新图,那么此图一定是一棵树,所有树边都是割边,在此性质基础上对于特定题目可以转换为树上问题。
-
边双连通具有传递性,若 \(a, b\) 边双连通,且 \(b, c\) 边双连通,则 \(a, c\) 边双连通。
3.2.2. 点双
- 考虑一个割点 \(u\),以及其两侧任意两点 \(x, y\),有任意 \(x, y\) 之间路径都包含割点 \(u\)。
- 点双交点一定是割点,割点一定是点双的交点。
- 点双连通不具有传递性,因为点双可以交于割点。
- 一条边恰好属于一个点双。
3.3. Tarjan 求割点
将根节点与非根节点分开考虑,时刻记住是在无向图 DFS 树上进行处理,基于无向图 DFS 树的性质,会对理解有很大帮助。
非根节点:
若非根节点 \(x\) 是割点,则其子树内存在点不通过 \(x\) 能到达的所有点均在 \(x\) 子树内,若其不是,那么一定其子树内任意节点可以通过非树边到达某个已经被访问过的点 \(y\),一定有 \(dfn_y < dfn_x\),所以定义 \(low_i\) 表示 \(i\) 子树内的点通过非树边能达到的 \(dfn\) 值最小的一点。\(x\) 是割点当且仅当任意一个子节点 \(u\) 使得 \(low_u < dfn_x\),等价于存在一个子节点 \(u\) 使得 \(low_u \ge dfn_x\)。
根节点:
因为是无向图 DFS 树,若根节点 \(x\) 有大于一个子节点,则删去 \(x\) 后各个子树不连通,\(x\) 为割点。
3.4. Tarjan 求割边
仍然考虑在无向图 DFS 树上处理,因为删掉非树边不影响连通性,所以割边一定是树边。
设边 \(e = (u, v)\) 为树边,并且 \(u\) 为 \(v\) 父节点,\(e\) 为割边当且仅当 \(low_v > dfn_u\),这与求割点是类似的。
在求割边时应注意当重边的影响,所以在判断非树边的时候应以边的编号为标准,而不是以端点是否是父节点为标准。
3.5. 例题
边双模板。
点双模板。
割点模板。
首先考虑进行边双缩点,于是得到一棵树,题意转化为给定一棵树,问添加多少边使得整张图变为一个点双。
易证添加的边连叶子是最优策略,考虑这样缩成点双影响到的点是最多的。
先将叶子任意两两匹配,剩余一个的处理方式是简单的,考虑是否存在无解。
对于一条边 \((u, v)\),设其两侧子图分别为 \(U, V\),显然 \(U, V\) 中各有偶数个叶子,否则 \((u, v)\) 一定会被覆盖。
调整方式即拆开一对连在 \(U\) 或 \(V\) 内的匹配,令其跨 \((u, v)\) 相连,这样显然不劣,故一定有解,答案为缩点后叶子个数除以二上取整。
非割点的贡献是简单的,考虑割点答案构成。
去掉割点后在子树内会形成若干连通块,对于一个大小为 \(x\) 的连通块贡献为 \(x (n - x)\),特别的还要考虑不在割点子树的那个连通块,实现是简单的。