【强连通分量算法】
Kosaraju 算法/Tarjan 算法
【强连通分量实现】
//Kosaraju
// g 是原图,g2 是反图
void dfs1(int u) {
vis[u] = true;
for (int v : g[u])
if (!vis[v]) dfs1(v);
s.push_back(u);
}
void dfs2(int u) {
color[u] = sccCnt;
for (int v : g2[u])
if (!color[v]) dfs2(v);
}
void kosaraju() {
sccCnt = 0;
for (int i = 1; i <= n; ++i)
if (!vis[i]) dfs1(i);
for (int i = n; i >= 1; --i)
if (!color[s[i]]) {
++sccCnt;
dfs2(s[i]);
}
}
// tarjan
int dfn[N], low[N], dfncnt, s[N], in_stack[N], tp;
int scc[N], sc; // 结点 i 所在 SCC 的编号
int sz[N]; // 强连通 i 的大小
void tarjan(int u) {
low[u] = dfn[u] = ++dfncnt, s[++tp] = u, in_stack[u] = 1;
for (int i = h[u]; i; i = e[i].nex) {
const int &v = e[i].t;
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
} else if (in_stack[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u]) {
++sc;
while (s[tp] != u) {
scc[s[tp]] = sc;
sz[sc]++;
in_stack[s[tp]] = 0;
--tp;
}
scc[s[tp]] = sc;
sz[sc]++;
in_stack[s[tp]] = 0;
--tp;
}
}
【割点】
1.定义:
删除某个点后,这个图就不再联通,称这个点为割点
2.求法
显然,如果根节点有多于两个儿子就一定是割点,其他点如果至少存在一个儿子的dfn[x]<=low[son],那么这个点就是割点
【割点实现】
void tarjan(int u,int fa)
{
low[u]=dfn[u]=++times;
int child=0;
for(int i=head[u];i;i=e[i].nxt)
{
int to=e[i].to;
if(!dfn[to])
{
tarjan(to,fa);
low[u]=min(low[to],low[u]);
if(low[to]>=dfn[u] && u!=fa && !cut[u]) cut[u]=1,cnt++;
if(u==fa) child++;
}
else low[u]=min(low[u],dfn[to]);
}
if(child>=2 && u==fa)
{
cut[u]=1;
cnt++;
}
}
【割边】
1.定义:
在无向图中删去这条边,图不再联通,就成这条边为割边
2.求法
low[v]>num[u]即可表示u-v之间的边为割边
【割边实现】
void tarjan(int u, int fa) {
father[u] = fa;
low[u] = dfn[u] = ++dfs_time;
for (int i = 0; i < G[u].size(); i++) {
int v = G[u][i];
if (!dfn[v]) {
tarjan(v, u);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u]) {
isbridge[v] = true;
++cnt_bridge;
}
} else if (dfn[v] < dfn[u] && v != fa) {
low[u] = min(low[u], dfn[v]);
}
}
}
【双联通分量】
在一张连通的无向图中,对于两个点u和v,如果无论删去哪条边(只能删去一条)都不能使它们不连通,我们就说u和v边双连通。
在一张连通的无向图中,对于两个点u和v,如果无论删去哪个点(只能删去一个,且不能删u和v自己)都不能使它们不连通,我们就说u和v点双连通。
其中,边双有传递性,点双没有传递性,下图为反例
在一个无向连通图中,如果没有割点,那么它是点双连通图。一个无向图的极大点双连通子图就是点双连通分量。
在一个无向连通图中,如果没有割边,那么它是边双连通图。一个无向图的极大边双连通子图就是边双连通分量。
【双联通分量实现】
//点双
void tarjan(int x,int in_edge){
low[x]=dfn[x]=++cnt;sta[++t]=x;
for(int i=head[x];i+1;i=a[i].next){
int y=a[i].y;
if(!dfn[y]){
tarjan(y,i);
low[x]=min(low[x],low[y]);
if(low[y]>=dfn[x]){
int u;
dcc++;size=0;
vdcc.clear();
do{
u=sta[t--];
block[u]=dcc;
size++;
vdcc.push_back(u);
}while(u!=y);
block[x]=dcc;
vdcc.push_back(x);
size++;
}
}
else low[x]=min(low[x],dfn[y]);
}
}
int low[maxn],dfn[maxn],cnt=0;
bool bridge[maxm<<1];
void tarjan(int x,int in_edge){
dfn[x]=low[x]=++cnt;
for(int i=head[x];i+1;i=a[i].next){
int y=a[i].y;
if(!dfn[y]){
tarjan(y,i);
low[x]=min(low[x],low[y]);
if(low[y]>low[x])bridge[i]=bridge[i^1]=true;
}
else if(i!=(in_edge^1))
low[x]=min(low[x],dfn[y]);
}
}
int block[maxn],dcc=0;
void dfs(int x,int color){
block[x]=color;
for(int i=head[x];i+1;i=a[i].next){
int y=a[i].y;
if(bridge[i])continue;
if(!block[y])dfs(y,color);
}
}
void get_edccs(){
for(int i=1;i<=n;i++)
if(!block[i])
dfs(i,++dcc);
}
【题型总结】
1.tarjan缩点
1.度数相关
P2341 [USACO03FALL][HAOI2006]受欢迎的牛 G
缩点后,如果出度为0的点大于1个,那么不存在受欢迎的牛,否则那个出度为0的点集为受欢迎的牛的集合 代码
缩点后求入度为0的点个数
代码
[USACO5.3]校园网Network of Schools
分别为入度为0个数和max{入度为0个数,出度为0个数},注意特判初始联通的情况 代码
缩点后,求所有度数为0集合中最小值的和,如果跑完tarjan仍有点未经过,那么一定无法全部收买 代码
2.DAG上dp(最长路)
题目转化为,缩点后求DAG上的最长链及方案数,dp即可 代码
因为可以重复经过边/点,所以强连通分量内的点一定都可以走到,所以我们的问题就转换为了缩点后求最长路 代码
这道题目的主要在于判断两个咒语机是否有升级关系,其实用dfs跑一边,两个机器同时跑,保证当前时刻的字符串相同,如果此时A有输出,B无输出,那么B就不是A的升级,在建图后,直接缩点最长路 代码
首先建立差分约束系统,然后答案转换为最长路+1,floyd会超时,考虑负环一定不可行,全是1的正环也不可行,但由于+1-1是正反挨着建的图,所以直接判断有没有负环即可判读是否有解。一个强连通分量中的相对关系是确定的,贡献为最长路长度,强连通分量之间的边一定是0的边,所以它们的差值随意,故最终答案为所有强连通分量的最长路+强连通分量个数 代码
3.特殊建图
夫妻之间:girl→boy 情人之间:boy→girl,此时如果对夫妻在同一个强连通分量中就危险了
2.割点
割点模板 代码
我们要断开一个点,使得A.B分开,那么这个点一定是割点,同时我们要判断这个割点是否把A和B割到了两个部分,也就是dfn[割点]在dfn[A]和dfn[B]之间 代码
求出每个点双中的割点数量,没有割点就需要建两个,有一个割点就需要建一个,有两个割点就不需要建了 代码
3.割边
求所有割边中最小值,细节较多比如重边不能是割边,而且开始就不联通答案为0,边权为0也要派一个人去 代码
4.广义圆方树
对于无向图上的简单路径问题,我们可以考虑用圆方树解决
我们找到所有点双,对于每个点双连通分量,新建立一个方节点,将其向点双的每个圆点连边,这样就得到了一棵树
性质:1.只有圆圆边和圆方边 2.一个方点对应一个点双 3.方点的度数为点双大小
两圆点在圆方树上的路径,与路径上经过的方点相邻的圆点的集合,就等于原图中两点简单路径上的点集。回到题目,考虑固定s和f,求合法的c的数量,显然有合法c的数量等于s和f之间简单路径的并集的点数减2(去掉s和f本身)。那么,对原图建出圆方树后,两点之间简单路径的点数,就和它们在圆方树上路径经过的方点(点双)和圆点的个数有关。
接下来是圆方树的一个常用技巧:路径统计时,点赋上合适的权值。本题中,每个方点的权值为对应点双的大小,而每个圆点权值为 -1。那么这样赋权后则有两圆点间圆方树上路径点权和,恰好等于原图中简单路径并集大小减2。
至此,问题转化为求任意两圆点间路径权值和。
这里又有一个常见tips:考虑转化为点的贡献,即权值乘以经过它的路径条数,利用树形(换根)dp求解即可 代码