【模板】Tarjan
\(\color{black}{Tarjan}\)是在\(dfs\)的过程中维护了一个栈,以及\(low\)(回溯值)
,\(dfn\)(时间戳)
两个数组。
\(low\)和\(dfn\)的计算
\(dfn\)就在\(dfs\)的过程中打就可以了,而\(low\)的计算方式稍稍复杂一些:
1.如果\(edge_{x,y}\)为树枝边(\(y\)未被访问),则\(low_x = \min(low_x,low_y)\)
2.如果\(edge_{x,y}\)为返祖边(\(y\)在栈中),则\(low_x = \min(low_x,dfn_y)\)
3.除此之外,啥都不干(\(edge_{x,y}\)是横叉边)
有向图的连通性
强连通分量
在有向图\(G\)中,如果两个顶点间至少存在一条路径,称两个顶点强连通。
如果有向图\(G\)的每两个顶点都强连通,称\(G\)是一个强连通图。
非强连通图有向图的极大强连通子图,称为强连通分量。
则通过手模易得:\(\text{当一个点}x\text{经过}dfs\text{后},dfn_x = low_x,\text{则栈中}x\text{以上的点构成一个强连通分量}\)
代码:
bool instk[z];
int ti, dfn[z], low[z], belong[z];
stack<int> stk;
void tarjan(int u) {
dfn[u] = low[u] = ++ti;
stk.push(u);
instk[u] = true;
for(int i = head[u];i;i = edge[i].next) {
if(!dfn[edge[i].t]) {
tarjan(edge[i].t);
low[u] = min(low[u],low[edge[i].t]);
} else if(instk[edge[i].t]) {
low[u] = min(low[u],dfn[edge[i].t]);
}
}
if(low[u] == dfn[u]) {
int tmp;
++belong[0];
do {
tmp = stk.top();
stk.pop();
instk[tmp] = false;
belong[tmp] = belong[0];
} while(tmp != u);
}
}
无向图的连通性
割点
若删掉某点后,原连通图分裂为多个子图,则称该点为割点。
则易得:\(\text{若一点}x\text{的}low_x \ge dfn_x,\text{则}x\text{为割点}\)
代码:
bool cutPoint[z];
int ti, root, dfn[z], low[z];
void tarjan(int u) {
dfn[u] = low[u] = ++ti;
int son = 0;
for(int i = head[u];i;i = edge[i].next) {
if(!dfn[edge[i].t]) {
++son;
tarjan(edge[i].t);
low[u] = min(low[u],low[edge[i].t]);
if(dfn[u] <= low[edge[i].t])
if(u != root||son > 1) cutPoint[u] = true;
//注意如果u为起点,至少有两棵子树才算割点;
} else if(instk[edge[i].t]) {
low[u] = min(low[u],dfn[edge[i].t]);
}
}
}
割边(桥)
删掉某边之后,图会分裂为两个或两个以上的子图,则该边为桥。
则易得:\(\text{若某边}x-y\text{有}low_x > dfn_y,\text{则该边为割边}\)
进一步,则有
1.\(low_x = dfn_x\)
int dfn[z], low[z], time;
bool vis[z], cute[z<<2];
void tarjan(int u,int p) {
vis[u] = true;
dfn[u] = low[u] = ++time;
for(int i = head[u];i;i = edge[i].next) {
int v = edge[i].t;
if(!dfn[v]) {
tarjan(v,i);
low[u] = min(low[u],low[v]);
} else if(p != (i^1)) {
low[u] = min(low[u],dfn[v]);
}
}
if(p&&low[u] == dfn[u])
cute[p] = cute[p^1] = true;
}
2.\(dfn_x < low_y\)
int dfn[z], low[z], time;
bool vis[z], cute[z<<2];
void tarjan(int u,int p) {
dfn[u] = low[u] = ++time;
bool first = true;
for(int i = head[u];i;i = edge[i].next) {
int v = edge[i].t;
if(first&&v == p) {
first = false;
continue;
}
if(!dfn[v]) {
tarjan(v,u);
low[u] = min(low[u],low[v]);
if(dfn[u] < low[v]) {
cute[i] = cute[i^1] = true;
}
} else {
low[u] = min(low[u],dfn[v]);
}
}
}
3.\(\text{前两种综合,}\)代码如下:
bool cutEdge[z];
int ti, dfn[z], low[z], cut, pEdge[z];
void tarjan(int u) {
dfn[u] = low[u] = ++ti;
for(int i = head[u];i;i = edge[i].next) {
if(i == (pEdge[u]^1)) continue;
if(!dfn[edge[i].t]) {
pEdge[edge[i].t] = i;
tarjan(edge[i].t);
low[u] = min(low[u],low[edge[i].t]);
if(dfn[u] > low[edge[i].t]) {
cut++;
cutEdge[i] = cutEdge[i^1] = true;
}
} else {
low[u] = min(low[u],dfn[edge[i].t]);
}
}
}
点、边双连通分量
若一个无向图中的去掉任意一个节点(一条边)都不会改变此图的连通性,即不存在割点(桥),则称作点(边)双连通图。
一个无向图中的每一个极大点(边)双连通子图称作此无向图的点(边)双连通分量。求双连通分量可用\(Tarjan\)算法。
1.点双联通分量
对于点双连通分支,实际上在求割点的过程中就能顺便把每个点双连通分支求出。建立一个栈,存储当前双连通分支,访问每个点时把这个点入栈。
如果某时满足\(dfn_u\le low_v\),说明u是一个割点,那么把点从栈顶一个个取出,直到遇到了点\(u\)(\(u\)不取出),取出的这些点和\(u\)组成一个点双连通分支。
割点可以属于多个点双连通分支,其余点和每条边只属于且属于一个点双连通分支。
代码:
bool cutPoint[z];
int ti, dfn[z], low[z], cut, root;
stack<int> stk;
vector<int> pbcc[z], belong[z];
void tarjan(int u,int p) {
dfn[u] = low[u] = ++ti;
stk.push(u);
bool first = true;
int sub = 0;
for(int i = head[u];i;i = edge[i].next) {
if(first&&edge[i].t == p) {
first = false;
continue;
}
if(!dfn[edge[i].t]) {
++sub;
tarjan(edge[i].t,u);
low[u] = min(low[u],low[edge[i].t]);
if(dfn[u] <= low[edge[i].t]) {
cutPoint[u] = true;
cut++;
pbcc[cut].push_back(u);
int tmp;
do {
tmp = stk.top();
stk.pop();
pbcc[cut].push_back(tmp);
belong[tmp].push_back(cut);
} while(tmp != edge[i].t);
}
} else {
low[u] = min(low[u],dfn[edge[i].t]);
}
}
if(u == root&&sub == 1)
cutPoint[u] = false;
}
2.边双连通分量
对于边双连通分支,求法更为简单。只需在求出所有的桥以后,把桥边删除,原图变成了多个连通块,则每个连通块就是一个边双连通分支。
桥不属于任何一个边双连通分支,其余的边和每个顶点都属于且只属于一个边双连通分支。
代码:
stack<int> stk;//栈;
vector<int> ebc[z];//存每一个边双联通分量;
bool cut[z];//边是否为桥;
int dfn[z], low[z], ti;
int belong[z];//判断某点在哪一个分量里;
void tarjan(int &u,int &pid) {
dfn[u] = low[u] = ++ti;
stk.push(u);
for(int i = head[u];i;i = edge[i].next) {
if(i == (pid^1)) continue;//WARNING!加括号!
if(!dfn[edge[i].t]) {
tarjan(edge[i].t,i);//顺便这里判的一直是边的id;
low[u] = min(low[u],low[edge[i].t]);
} else {
low[u] = min(low[u],dfn[edge[i].t]);
}
}
if(dfn[u] == low[u]) {//发现有桥,开始找分量;
cut[pid] = true;
++belong[0];
int tmp;
do {
tmp = stk.top();
stk.pop();
belong[tmp] = belong[0];
ebc[belong[0]].push_back(tmp);
} while(tmp != u);
}
return;
}
3.构造边双连通分量
若一个有桥的连通图的叶子结点数量为\(n\),则至少要加\(\left\lfloor\dfrac{n+1}{2}\right\rfloor\)条边就能使该图变为边双连通图。
具体方法为,首先在两个最近公共祖先最远的两个叶节点之间连接一条边,把这两个点到祖先的路径上所有点收缩到一起,再继续祖先最远的两个叶节点,以此类推。
LCA
我们先读入所有的询问并对这些询问构建一个邻接表。
在遍历到\(u\)时,先\(tarjan\)遍历完\(u\)的子树,则\(u\)和\(u\)的子树中的节点的最近公共祖先就是\(u\),并且u
和u的兄弟节点及其子树
的最近公共祖先就是\(u\)的父亲。
用一个\(color\)数组,正在访问的节点标记为\(1\),未访问的标记为\(0\),已经访问到的即在u的子树中的
及u的已访问的兄弟节点及其子树中的
标记为\(2\)。
再维护一个并查集,访问完节点\(u\)的一个子树时,就把这个子树的根节点的\(fa\)改为\(u\)。访问完u的所有子树后,考虑所有与\(u\)相关的询问\(lca(u,v)\),那么\(lca(u,v)\)就是\(v\)所在并查集的根。
这是一个离线算法,时间复杂度为\(\operatorname{O}(N\operatorname{\alpha}(N))\),约为\(\operatorname{O}(N)\)。
#include <stdio.h>
const int z = 1024;
template<typename Tec>
Tec min(Tec &__x,Tec &__y) {
return __x < __y ? __x : __y;
}
struct EDGE {
int t, next;
} edge[z<<2];
int tot = 1, head[z];
void add_edge(int f,int t) {
edge[++tot].next = head[f];
edge[tot].t = t;
head[f] = tot;
}
struct QUERY {
int t, next;
} query[z<<2];
int qyt = 1, qtop[z];
void add_query(int u,int v) {
query[++qyt].next = qtop[u];
query[qyt].t = v;
qtop[u] = qyt;
}
int p[z], col[z], ans[z];
int get(int x) {
if(p[x] != x)
return get(p[x]);
else
return x;
}
void tarjan(int u) {
p[u] = u, col[u] = 1;
for(int i = head[u];i;i = edge[i].next) {
int v = edge[i].t;
if(!col[v]) {
tarjan(v);
p[v] = u;
}
}
for(int i = qtop[u];i;i = query[i].next) {
int v = query[i].t;
if(col[v] == 2)
ans[i>>1] = get(v);
}
col[u] = 2;
}
int n, m;
signed main() {
scanf("%d %d",&n,&m);
for(int i = 2, a;i <= n;++i) {
scanf("%d",&a);
add_edge(a,i);
}
for(int i = 1, a, b;i <= m;++i) {
scanf("%d %d",&a,&b);
add_query(a,b);
add_query(b,a);
}
tarjan(1);
for(int i = 1;i <= m;++i)
printf("%d\n",ans[i]);
}
2-SAT
keys
问题:从点\(b\)到点\(e\)的必经点?
还是割点,多判一个条件\(dfn_v \le dfn_e\)罢了。
code
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <bits/stl_algobase.h>
using namespace std;
char _tp;
template<typename Tec>
void get_int(Tec &_aim) {
Tec _sg = 1;
_aim = 0, _tp = getchar();
for(;!isdigit(_tp);_tp = getchar()) {
if(_tp == '-')
_sg = -1;
}
for(;isdigit(_tp);_tp = getchar())
_aim = (_aim<<3)+(_aim<<1)+_tp-'0';
_aim *= _sg;
return;
}
const int z = 2e5+10;
struct EDGE {
int t, next;
} edge[z<<2];
int head[z], edge_tot;
void add_edge(int f,int t) {
edge[++edge_tot].next = head[f];
edge[edge_tot].t = t;
head[f] = edge_tot;
}
int T, n, m;
int ans;
int bg, ed;
bool cutPoint[z];
int time, dfn[z], low[z];
void tarjan(int u) {
dfn[u] = low[u] = ++time;
for(int i = head[u];i;i = edge[i].next) {
int v = edge[i].t;
if(!dfn[v]) {
tarjan(v);
low[u] = min(low[u],low[v]);
if(dfn[u] <= low[v]&&dfn[v] <= dfn[ed]&&u != 1&&u != ed) {
ans++;
cutPoint[u] = true;
}
} else low[u] = min(low[u],dfn[v]);
}
}
void init() {
get_int(n);
get_int(m);
get_int(bg);
get_int(ed);
ans = edge_tot = time = 0;
memset(dfn,0,(n+1)*sizeof(int));
memset(head,0,(n+1)*sizeof(int));
memset(cutPoint,false,(n+1)*sizeof(bool));
}
signed main() {
get_int(T);
for(int outs = 1;outs <= T;++outs) {
init();
for(int i = 1, a, b;i <= m;++i) {
get_int(a);
get_int(b);
if(a == b)
continue;
add_edge(a,b);
add_edge(b,a);
}
tarjan(bg);
printf("%d\n",ans);
for(int i = 2;i < n;++i)
if(cutPoint[i])
printf("%d ",i);
putchar(10);
}
}