引子:垃圾 f s y fsy fsy的图论比 f s y fsy fsy 还垃圾,于是来补一补
割点: x x x 是割点,当且仅当:
- x 是 d f s dfs dfs 树上的根结点且有两个以上的儿子
- l o w [ s o n ] ≥ d f n [ x ] low[son] \ge dfn[x] low[son]≥dfn[x]
因为大于等于说明能回到的最远点就是它
由于判断的是
l
o
w
[
s
o
n
]
≥
d
f
n
[
x
]
low[son]\ge dfn[x]
low[son]≥dfn[x],那么就不需要考虑父结点或重边的问题,从
x
x
x 出发的所有点的时间戳都可以用来更新
x
x
x
void dfs(int u){
low[u] = dfn[u] = ++sign; int cnt = 0;
for(int i = first[u]; i; i = nxt[i]){
int t = to[i]; if(!dfn[t]){
dfs(t), low[u] = min(low[u], low[t]);
if(low[t] >= dfn[u]){ ++cnt; if(u != rt || cnt > 1) cut[u] = 1;}
}
else low[u] = min(low[u], dfn[t]);
}
}
割边:
x
x
x 是割边,当且仅当:
l
o
w
[
s
o
n
]
>
d
f
n
[
x
]
low[son] > dfn[x]
low[son]>dfn[x]
然后不能回到父亲边,考虑重边的影响,需要记录来的边的编号
void dfs(int u, int from){
low[u] = dfn[u] = ++sign;
for(int i = first[u]; i; i = nxt[i]){
int t = to[i]; if(!dfn[t]){
dfs(t, i), low[u] = min(low[u], low[t]);
if(low[t] > dfn[u]) bridge[i >> 1] = true;
}
else if(i != (from ^ 1)) low[u] = min(low[u], dfn[t]);
}
}
边双联通分量:删去所有桥后每个联通块就是一个边双联通分量
边双的缩点:保留所有桥边,连接两端的边双联通
点双联通分量:
定义:在一个无向图中,若任意两点间至少存在两条 “点不重复” 的路径,则说这个图是点双连通的
性质:
- 任意两点间至少存在两条点不重复的路径等价于图中删去任意一个点都不会改变图的连通性,
即 BCC 中无割点 - 若BCC间有公共点,则公共点为原图的割点
- 无向连通图中割点一定属于至少两个BCC,非割点只属于一个BCC
求法:
- 当一个结点被第一次访问时,入栈
- 当
d
f
n
[
x
]
≤
l
o
w
[
s
o
n
]
dfn[x]\le low[son]
dfn[x]≤low[son] 时,无论
x
x
x是不是跟,都要:
弹栈直到 s o n son son 被弹出,弹出的所有点
弹出的点与 x x x 构成点双联通分量
void dfs(int u){
low[u] = dfn[u] = ++sign; sta[++top] = u;
for(int i = first[u]; i; i = nxt[i]){
int t = to[i];
if(!dfn[t]){
dfs(t); low[u] = min(low[u], low[t]);
if(low[t] >= dfn[u]){
++id; do{ v[id].push_back(sta[top]); } while(sta[top--] != t);
v[id].push_back(u);
}
} else low[u] = min(low[u], dfn[t]);
}
}
点双联通的缩点:
一个割点可能属于多个点双联通分量,缩完点后的点的个数等于割点数+点双连通分量个数
割点与包涵它的所有点双连边
连出来会是一棵树,可以求得无向图两点间的必经点:
树上两点路径上的割点
有向图的强连通分量:
记
l
o
w
[
x
]
low[x]
low[x] 为满足下列条件的最小时间戳:
- 在栈中
- 存在一条从 s u b t r e e ( x ) subtree(x) subtree(x) 出发的有向边以该点为终点
做法:
- 如果 y y y 没有访问过,那么 ( x , y ) (x,y) (x,y) 为树边,递归访问 y y y, 令 l o w [ x ] = m i n ( l o w [ x ] , l o w [ y ] ) low[x]=min(low[x],low[y]) low[x]=min(low[x],low[y])
- 如果 y y y 访问过,并且在栈中,那么令 l o w [ x ] = m i n ( l o w [ x ] , d f n [ y ] ) low[x]= min(low[x],dfn[y]) low[x]=min(low[x],dfn[y])
- 回溯之前判断 d f n [ x ] = l o w [ x ] dfn[x]=low[x] dfn[x]=low[x],如果成立,弹栈直到 x x x,这些点属于一个强连通分量
强制在栈中才更新 l o w [ x ] low[x] low[x] 是为了避免横叉边的影响
2-SAT:
重点讲方案输出的问题
法1:首先,在一个
S
C
C
SCC
SCC 中,只要确定了一个变量的赋值,其它的也就定了,这启发我们直接缩点
其次,如果我们选择一个有出点的边,它指向的点也要选,这样一来会比较繁琐
我们考虑先选择出度为 0 的点,然后反向做一次拓扑排序
法2:
t
a
r
j
a
n
tarjan
tarjan 算法的本质是一次
d
f
s
dfs
dfs,它回溯时会先取出有向图底部的
S
C
C
SCC
SCC 进行标记
所以说
t
a
r
j
a
n
tarjan
tarjan 算法的
S
C
C
SCC
SCC 编号本身就对应自底向上的拓扑序
对于两个对应点,我们只需要选择
S
C
C
SCC
SCC 编号较小的那一个即可
二分图匹配的扩展:
最小点覆盖:选择点集
S
S
S,使得任意一条边至少有一个顶点在
S
S
S中,求
m
i
n
(
∣
S
∣
)
min(|S|)
min(∣S∣)
最小点覆盖 = 最大匹配
证明 感性理解:如果不是最大匹配那么会有一条边两个点都没有被覆盖
最大独立集:最大独立集 = n - 最大匹配
证明:最大独立集
⇔
\Leftrightarrow
⇔ 在图中去掉最少的点,使得剩下的点没有边
在图中去掉最少的点,使得剩下的点没有边
⇔
\Leftrightarrow
⇔ 选最少的点覆盖所有的边
D
A
G
DAG
DAG 路径覆盖1:用不相交的链覆盖
D
A
G
DAG
DAG,求链的最少个数
首先令答案为
n
n
n,相邻的两条路径可以接在一起,也就是可以匹配
每次匹配路径数减去1,最终答案为
n
−
m
a
t
c
h
n-match
n−match,要让这个最小
也就是
m
a
t
c
h
match
match 最大,拆点求个最大匹配即可
D
A
G
DAG
DAG 路径覆盖2:用可以相交的链覆盖
D
A
G
DAG
DAG
若有两条路径
u
−
−
p
−
−
v
u--p--v
u−−p−−v,
x
−
−
p
−
−
y
x--p--y
x−−p−−y,
我们将
x
x
x 向
y
y
y 连边就可以避免重复走
p
p
p, 求出传递闭包后同上
法2:相交可以看做
x
x
x 越过
y
y
y 去匹配后面的点,那么网络流时
y
′
y'
y′ 向
y
y
y 连流量正无穷的边即可
无向图最小环问题:
f
l
o
y
e
d
floyed
floyed 外层循环执行到
k
k
k 时,
d
i
s
[
i
]
[
j
]
dis[i][j]
dis[i][j] 表示的是
i
i
i 到
j
j
j 经过不超过
k
−
1
k-1
k−1 的点的最短路
于是我们可以枚举
i
,
j
i,j
i,j,用
a
i
,
k
+
a
k
,
j
+
d
i
s
i
,
j
a_{i,k}+a_{k,j}+dis_{i,j}
ai,k+ak,j+disi,j 去更新答案
有向图最小环问题:
D
i
j
s
k
t
r
a
Dijsktra
Dijsktra, 枚举起点
s
s
s,更新完
s
s
s 的出点过后将
d
i
s
[
s
]
dis[s]
dis[s] 设成
i
n
f
inf
inf,然后
s
s
s 第二次从队列中取出时的
d
i
s
dis
dis 就是最小环
s
p
f
a
spfa
spfa 判负环:
用
c
n
t
[
x
]
cnt[x]
cnt[x] 记录 1 到
x
x
x 的最短路包涵的边数
当更新
d
i
s
[
y
]
=
d
i
s
[
x
]
+
z
dis[y]=dis[x]+z
dis[y]=dis[x]+z 时,同样更新
c
n
t
[
y
]
=
c
n
t
[
x
]
+
1
cnt[y]=cnt[x]+1
cnt[y]=cnt[x]+1
如果发现
c
n
t
[
x
]
≥
n
cnt[x]\ge n
cnt[x]≥n,那么出现了负环
bool spfa(){
memset(dis, 63, sizeof(dis));
memset(vis, 0, sizeof(vis));
memset(cnt, 0, sizeof(cnt));
queue<int> q; q.push(1); dis[1] = 0;
while(!q.empty()){
int x = q.front(); q.pop(); vis[x] = 0;
if(cnt[x] >= n) return true;
for(int i = first[x]; i; i = nxt[i]){
int t = to[i]; if(dis[t] > dis[x] + w[i]){
dis[t] = dis[x] + w[i]; cnt[t] = cnt[x] + 1;
if(!vis[t]) q.push(t), vis[t] = 1;
}
}
} return false;
}
差分约束:
给出
x
i
−
x
j
≤
k
x_i-x_j\le k
xi−xj≤k 的限制,求
m
a
x
(
x
n
−
x
0
)
max(x_n-x_0)
max(xn−x0)
考虑将所有不等式移项,加减,全部变成
x
n
−
x
0
≤
k
i
x_n-x_0 \le k_i
xn−x0≤ki
比如将
x
n
−
x
1
≤
k
1
x_n-x_1\le k_1
xn−x1≤k1,
x
1
−
x
2
≤
k
2
x_1-x_2\le k_2
x1−x2≤k2,
x
2
−
x
0
≤
k
3
x_2-x_0\le k_3
x2−x0≤k3, 变成
x
n
−
x
0
≤
k
1
+
k
2
+
k
3
x_n-x_0\le k_1+k_2+k_3
xn−x0≤k1+k2+k3
那么
m
a
x
(
x
n
−
x
0
)
=
m
i
n
(
k
i
)
max(x_n-x_0)=min(k_i)
max(xn−x0)=min(ki),
m
i
n
(
k
i
)
min(k_i)
min(ki) 恰为
0
0
0 到
n
n
n 的最短路
于是对于
x
i
−
x
j
≤
k
x_i-x_j \le k
xi−xj≤k 的限制,
j
j
j 向
i
i
i 连一条权值为
k
k
k 的边跑最短路即可
另一种理解是
x
i
−
x
j
≤
k
x_i-x_j \le k
xi−xj≤k即
x
i
≤
x
j
+
k
x_i\le x_j+k
xi≤xj+k 与
d
i
s
[
y
]
≤
d
i
s
[
x
]
+
c
[
i
]
dis[y] \le dis[x]+c[i]
dis[y]≤dis[x]+c[i] 类似
一般会对题目条件做一些等价变形
如果求
m
i
n
(
x
n
−
x
0
)
min(x_n-x_0)
min(xn−x0),需要把限制转换成
x
i
−
x
j
≥
k
x_i-x_j \ge k
xi−xj≥k 然后求最长路
欧拉回路:
无向图:存在欧拉回路当前仅当
d
e
g
[
x
]
deg[x]
deg[x] 全部为偶数
存在半欧拉回路(每条边经过一次,不用走回来),当且仅当每个点度数为偶数,只有两个点可以为奇数
这两个点就是起到和终点
有向图:存在欧拉回路当且仅当
i
n
[
x
]
=
o
u
t
[
x
]
in[x] = out[x]
in[x]=out[x]
方案:
d
f
s
dfs
dfs 搜索,不能再往下走便回溯,回溯时记录路径,将路径压入栈,最后将栈逆序输出就是一条欧拉回路,正确性看个图就会了 传送门
void dfs(int u){
for(int &i = first[u]; i; i = nxt[i]){
int t = to[i]; int j = i;
if(!vis[j]){ vis[j] = 1; dfs(t); ans[++cnt] = j;}
}
}