图论(连通性问题)
割点
割点针对于连通图(无向图)。在一张连通图中,如果删去一个点会导致图不连通,那么这个点就是割点。
一般求割点都会使用tarjan算法,这个算法最重要的两个数组分别是 $ dfn $ 和 $ low $ 。
$dfn$ 代表时间戳数组,而$low$数组代表从某点出发回溯到最早的时间戳,也就是走不通过父亲的道路能到达的最小的$dfn$ 。
想要求割点的话只需要考虑两种情况。
第一种:是显而易见的,如果一个节点为搜索树(注意不是图)的根节点,并且有至少2个的子树,那么它就是割点。
证明:因为是搜索树,所以两个子树就代表着它们只能靠根节点来连通,如果删去它,就无法连通了。
第二种:假设一个节点$u$不是根节点,它的儿子为$v$,如果 $ low_v ≥ dfn_u $ ,就说明$u$为割点。
证明:因为 $ low_v ≥ dfn_u $,说明了儿子节点$v$通过回溯无法到达$u$节点之前的已遍历节点。如果删去节点$u$,那么$v$就无法与那些已遍历节点连通。
另外就是tarjan算法的求解问题了,$dfn$数组很容易得出。只需要一个编号赋值就可以了。$low$数组求解的本质是通过dfs来求的,初始时赋值为时间戳,也就是最开始只能回溯到本身。
然后从根节点开始,不断递归,如果递归到未遍历的节点,就用它的$low$取最小值$low[u]=min(low[u],low[v])$
如果遇到一条返祖边,也就是连向已遍历节点的边,取最小值就和前面不太一样了,是$low[u]=min(low[u],dfn[v])$。
因为$u$的$low$数组如果不是本身的时间戳,就一定是其他的儿子节点更新的,假设$u$为割点的话,那么儿子$v$的$low$值肯定不能从其他子树节点的值来更新,因为割点$u$删除就不连通了。自己画图模拟一下就能理解了。
$code:$
#include<bits/stdc++.h>
using namespace std;
const int N = 6e6+5;
int n,m,cnt=1,dfn[N],low[N],id;
int to[N],nxt[N],head[N],ans,root;
bool b[N];
void add(int u,int v){
to[++cnt] = v;
nxt[cnt] = head[u];
head[u] = cnt;
}
void tarjan(int now,int in){
dfn[now] = low[now] = ++id;
int son = 0;
for(int i = head[now] ; i ; i = nxt[i]){
if(i == (in ^ 1)) continue;
int g = to[i];
if(!dfn[g]){
tarjan(g,i); son++;
low[now] = min(low[now],low[g]);
if(low[g] >= dfn[now] && now != root){
b[now] = 1;
}
}
else if(i != (in ^ 1))
low[now] = min(low[now] , dfn[g]);
}
if(now == root && son >= 2){
b[now] = 1;
}
}
signed main(){
scanf("%d%d",&n,&m);
for(int i = 1 ; i <= m ; ++i ){
int x,y;
scanf("%d%d",&x,&y);
add(x,y);add(y,x);
}
for(int i = 1 ; i <= n ; ++i )
if(!dfn[i]) root = i,tarjan(i,0);
for(int i = 1 ; i <= n ; ++i )
if(b[i]) ans++;
printf("%d\n",ans);
for(int i = 1 ; i <= n ; ++i )
if(b[i]) printf("%d ",i);
}
点双连通分量
定义:
点双连通图:一张无向图,任意两个点都可以有两条点不相交的路径到达。也就是一张连通图中没有割点
点双连通分量:选择一个点集使之是一个点双连通图
注意,点双连通分量不满足传递性(强连通分量和边双连通分量是满足的)
因为假设a和b之间没有割点, b和c之间没有割点,那么a和c之间是不一定没有割点的,因为b本身可能就是割点

求解同样使用tarjan算法,过程与割点大部分是一样的。两个点双的公共点只有一个,那就是割点。因为两个点双如果合并为1个就存在割点了。如果是不是割点的话,与之相连的两个点双就要合并为一个。
不是割点的节点只能出现在一个点双里。在dfs的过程中,不断将搜索到的点压入栈中。
如果找到一个割点或者树根,就将栈中找到割点的子树中的节点弹出。如果一颗子树没有找到割点,那就先存在栈里,直到向上找到另外的割点再弹出,注意割点是不能弹出的,因为这个割点还可以算入其他点双中。
树根也要算入点双,如果根节点只有一个子树,那么它肯定要算入儿子节点的点双中。如果有多个子树,那么它也是一个割点。
上述过程实在是不清晰,下面用图具体说明一下

对于这张连通图,他的搜索树其实也是这个样。那么 先看3号节点,他的第一个子树显然无法得出3是割点,那就先存入栈中。回溯到1号节点时,和3号以及8号一并归入1号节点的点双中,而7号节点已经因为找到割点而被弹出了。
$ code:$
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5, M = 4e6 + 5;
int cnt = 1, head[N], nxt[M], to[M] , root;
int s[M], top, tot, low[N], dfn[N], id, n, m;
vector<int> ans[N];
void tarjan(int u, int in) {
int son = 0;
low[u] = dfn[u] = ++id;
s[++top] = u;
for(int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if(!dfn[v]) {
if(i == (in^1)) continue;
son++;
tarjan(v, i);
low[u] = min(low[u], low[v]);
if(low[v] >= dfn[u]) {
tot++;
while(s[top + 1] != v) ans[tot].push_back(s[top--]);
ans[tot].push_back(u);
}
} else if(i != (in ^ 1)) low[u] = min(low[u], dfn[v]);
}
if(root == u && son == 0) ans[++tot].push_back(u);
}
void add(int u, int v) {
to[++cnt] = v;
nxt[cnt] = head[u];
head[u] = cnt;
}
signed main() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
add(u, v);add(v, u);
}
for(int i = 1; i <= n; i++) {
if(!dfn[i]){
top = 0;root = i;
tarjan(i, 0);
}
}
printf("%d\n", tot);
for(int i = 1; i <= tot; i++) {
cout<<ans[i].size()<<" ";
for(int j = 0 ; j < ans[i].size() ; ++j )
printf("%d ", ans[i][j]);
printf("\n");
}
}
缩点
故名思意,就是将一个环缩为一个点。仍然是用tarjan
给定一个 n 个点 m 条边有向图,每个点有一个权值,求一条路径,
使路径经过的点权值之和最大。你只需要求出这个权值和。
模板题:给定一个 n 个点 m 条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。求出这个权值和。
允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。
如果图没有环的存在,可以直接使用dp来求解。先来考虑如何用dp求解,设计一个状态很容易,$f[i]$ 就是路径终点为$i$时最大权值和。还要考虑无后效性问题,因为如果按常规遍历的话,求到的值肯定是错误的。
可以使用拓扑排序先更新入度为0的,再更新连接的节点入度,再重复上述过程进行dp。
而tarjan的写法