图论初步-Tarjan算法及其应用
暑假刷了一堆Tarjan题到头来还是忘得差不多。
这篇博客权当复习吧。
一些定义
无向图
割顶与桥 (划重点)
图G是连通图,删除一个点表示删除此点以及所有与其相连的边。
若删除某点u后G不再连通,那么u是G的一个割顶(割点)。
若删除某边e后G不再连通,那么e是G的一个桥。
双连通
一个图为双连通,意思是说任一点对(u,v),从u到v都有两条路径。
广义双连通有两种:点双连通(狭义的双连通)、边双连通。
- 点双连通:就是这两条路径除了起点和终点外无重复点。
- 边双连通:就是这两条路径无重复边。
例如,两个简单环共一个点的图是边双连通的,但不是点双连通的。
双连通图的性质
点双连通图删除任意一个点仍然连通。
边双连通图删除任意一条边仍然连通。
双连通分量
双连通分量也分为两种。
图G的双连通分量也是G的子图,且保证添加任意其他点、边后都不再是双连通的。
一个顶点可以属于多个点双连通分量,此时这个顶点必定是割顶。
显然,点双连通分量必定是边双连通分量。
哈密顿回路和欧拉回路
哈密顿路是经过所有顶点一次的路径。若起点和终点相同则称哈密顿回路。
欧拉路是经过所有边一次的路径。
有向图
有向图的连通性
- 强连通:任意点对(u,v),存在从u到v的路径和从v到u的路径。
- 单连通:任意点对(u,v),存在从u到v的路径或从v到u的路径。亦称单向连通。
- 弱联通:忽略边的方向得到的无向图是连通的。此无向图称为该有向图的底图。
单连通图一定是弱连通图。反之则不然。
强连通分量(SCC)(划重点)
亦如上面的定义。有向图G的一个强连通分量是G的一个子图,且任意添加其他点、边都不能再满足强连通的要求。
Tarjan算法
Tarjan求割顶
首先从根节点开始dfs遍历全图,对于每个点我们定义两个数组:\(dfn[],low[]\),其中\(dfn[]\)是这个点的时间戳,用来记录首次访问这个点的时刻;而\(low[]\)则是用来记录从这个点出发所能到达点的最小时间戳。可以配合注释食用。
void tarjan(int u,int fath){
dfn[u]=low[u]=++tmp;
int child=0;
for (int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if (!dfn[v]){//如果在此之前点v没有被访问过
tarjan(v,u);
low[u]=Min(low[u],low[v]);//更新最小时间戳
child++;
if (!fath&&child>=2) rec[v]=1;//如果u为根节点且u下有两棵子树那么u一定是割顶
else (fath&&(low[v]>=dfn[u])) rec[v]=1;//如果u有子树的最小时间戳不小于u的时间戳,那么u一定是割顶
}else{
if (u!=fath) low[u]=Min(low[u],dfn[v]);//v的时间戳可能比u小,并且回边不能算
}
}
}
Tarjan求桥
和求割顶非常类似
void tarjan(int u,int fath){
dfn[u]=low[u]=++tmp;
int child=0;
for (int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if (!dfn[v]){
tarjan(v,u);
low[u]=Min(low[u],low[v]);
child++;
if (dfn[u]<low[v]){e[++cnt].u=u;e[cnt].v=v;}
}else {if (u!=fath) low[u]=Min(low[u],dfn[v]);}
}
}
Tarjan求强连通分量
在Tarjan求强连通分量的过程中,low的定义有所改变,并且引入一个栈,low不是能到达的最前的dfn值,而是能到达的仍在栈中的dfn的最前值。
void tarjan(int u){
dfn[u]=low[u]=++timer;
st.push(u); instack[u]=true;
for (int i=0;i<e[u].size();i++) {
int v=e[u][i];
if (dfn[v]==0) {//如果访问过v
tarjan(v);
low[u]=min(low[u],low[v]);//更新最小时间戳
} else if (instack[v]) {//如果在栈内(在同一个强连通分量内)
low[u]=min(low[u],dfn[v]);
/*同low[u]=min(low[u],low[v]);
LXQ:求scc应该都可以-.-因为你一旦找到了上一个scc那它们就都出栈了-.-所以后面更新判断是否在栈中就保证了更新的值都是可行的
求scc不存在割顶的问题所以应该都可以
Upd.其实就是说已经走过的v在栈内,但是由于递归没有回溯,所以此时dfn[v]==low[v]*/
}
}
if (dfn[u]==low[u]) {//这就是一个强连通分量的根节点
int m;
printf("SCC: ");
do {
m=st.top();
st.pop();/*使栈顶出栈,把该强连通分量内除根节点外的
所有点弹出*/
instack[m]=false;
printf("%d ",m);
} while (m!=u);
printf("\n");
}
}
缩点
一般,缩点是针对有向图而言的。
找出了一个有向图的强连通分量后我们可以把每个强连通分量缩为一个点。
如此一来我们便得到一个有向无环图(DAG)。
有了有向无环图,我们就可以在上面跑拓扑。能跑拓朴就能跑dp,因为状态转移的无后效性已经在DAG建立的同时保证了。
例题:
- Luogu_3627_[APIO2009]抢掠计划
一句话题解:对于题目中的起点S的处理,在跑Tarjan的时候记录访问过的点,缩点的时候把没访问过的点排除即可。
练习:
Tarjan缩点+DAG+dp模板题:
Luogu_3387_【模板】缩点
Luogu_4742_[Wind Festival]Running In The Sky