模板
缩点
一些定义
-
强连通:如果两个顶点可以相互通达,则称两个顶点强连通。
-
如果有向图(G)的每两个顶点都 强连通,称(G)是一个强连通图。
-
非强连通图有向图的极大强连通子图,称为强连通分量。
算法思路
定义两个数组,(dfn[ ])和(low[])
(dfn[u])表示(u)节点第一次被(bfs)到的时间戳
(low[u])表示(u)节点能"回溯"到的最早时间
如果一个节点(u)的儿子(v)的(low[])小于自己的(low[]),说明该节点可以"回溯"到(u)之前,会产生一个强连通分量,记录这些点,将其合并成一个新点即可
算法流程
-
每(dfs)一个点(u)时,先把该点的(low[u])和(dfn[u])初始化成该点的时间戳,同时把这个点压入一个栈中
-
遍历其所有儿子,如果一个儿子没有被访问过,继续向下(dfs),同时更新节点(u)的(low[u])
-
反之,说明该儿子出现在(u)之前,无需再次(dfs),直接更新节点(u)的(low[u])即可
-
最后,如果一个节点的(dfn[])跟(low)相等,说明该点是一个强连通分量中的起点,将栈中的所有该节点前的值全部取出,合并成一个新的节点
代码:
void tarjan(int u){
dfn[u] = low[u] = ++Time;//初始化
s[++top] = u;//压入栈
vis[u] = 1;//被访问过
for(int i=head[u];i;i=edge[i].next){//遍历所有儿子
int v = edge[i].to;
if(!dfn[v]){
tarjan(v);//向下遍历
low[u] = min(low[v] , low[u]);//更新low
}
else if(vis[v]){//节点被访问过,直接更新
low[u] = min(low[v] , low[u]);
}
}
if(low[u]==dfn[u]){//如果是一个强连通分量的起点
int v;
while(v = s[top--]){
vis[v] = 0;
if(v==u) break;//将u前面的值全部取出
w[u]+=w[v];//合并点权
}
}
}
割点
一些定义
- 割点:将一个无向图中的一个节点及与其相连的所有的边都删去后,整个图不再联通,则成该点为该图的一个割点
算法思路
跟缩点大同小异的思路。
既然去除该点可以让整个图不再联通,说明其儿子与其上面的节点都无连接,也就是无法"回溯"到该点之前的点
直接用跟缩点一样的思路去写即可。
要注意的是 (:) 这里的(low[u])不再是直接更新成(low[v]),而是更新为(dfn[v]),在无向图中,每个点都有一条"返祖"边,这时把子节点的(low[])值赋为父节点的(low[]),就可能导致其(low[]==)其父节点(low[]<)其父节点(dfn[])
算法流程
void tarjan(int u,int fa){
dfn[u] = low[u] = ++Time;//初始化
int child = 0;//根节点的儿子数
for(int i=head[u];i;i=e[i].next){
int v = e[i].to;
if(!dfn[v]){
tarjan(v,fa);
low[u] = min(low[u] , low[v]);//更新
if(low[v]>=dfn[u]&&u!=fa){//有一个子节点满足条件即可
cut[u]=1;
}
if(u==fa) child++;//如果是根节点,增加儿子数
}
low[u] = min(low[u],dfn[v]);//更新
}
if(u==fa&&child>=2) cut[u]=1;//如果根节点数量大于2,说明根节点可以成为割点
}
缩点练习
P2341 [USACO03FALL][HAOI2006]受欢迎的牛 G
P2863 [USACO06JAN]The Cow Prom S
P2746 [USACO5.3]校园网Network of Schools
P2341 [USACO03FALL]受欢迎的牛 G
(tarjan)经典题。
首先是缩点,将每个强联通分量都分成一个组,并记录这个组的出度和大小,如果只有一个组的出度为(0),则说明该组中的所有奶牛都可以成为神犇(如果有两个组的出度均为零的话则说明%无法完全传递)
(code:)
void tarjan(int u){
dfn[u] = low[u] = ++Time;
vis[u] = 1,s[++top] = u;
for(int i=head[u];i;i=e[i].next){
int v = e[i].to;
if(!dfn[v]){
tarjan(v);
low[u] = min(low[u],low[v]);
}
else if(vis[v]){
low[u] = min(low[u],low[v]);
}
}
if(dfn[u]==low[u]){
int v;
scc[u] = ++k;//记录分组
while(v=s[top--]){
vis[v] = 0;
scc[v] = k;//记录分组
size[k]++;//记录大小
if(v==u) break;
}
}
}
[P2863 [USACO06JAN]The Cow Prom S]
简单的(tarjan)模板题。
实际上比模板题还要简单
判断栈头是否为(u),若不为(u),则说明有两个及以上的节点,若是,说明只有(u)一个节点
(code:)
void tarjan(int u){
dfn[u] = low[u] = ++Time;
vis[u] = 1,s[++top] = u;
for(int i=head[u];i;i=e[i].next){
int v = e[i].to;
if(!dfn[v]){
tarjan(v);
low[u] = min(low[u],low[v]);
}
else if(vis[v]){
low[u] = min(low[u],low[v]);
}
}
if(dfn[u]==low[u]){
if(s[top]!=u) ans++;//特判
int v;
while(v=s[top--]){
vis[v] = 0;
if(v==u) break;
}
}
}
[P2746 [USACO5.3]校园网Network of Schools]
给一张图,求:
-
图中入度为零的点的个数
-
把整张图变成强连通图的最少添边值
第一问很简单,直接统计一下即可,对于第二问,由于强连通图中每个点的入度和出度都不为零,感性理解一下,可以先将那些出度为0的点和入度为0的点互补,由于出度为0的点的数量和入度为0的点数量可能不一致,因此最后还要将那些剩余的点和其他任意一个点相连
最后的答案即为出度为零的点的数量和入读为零的点的数量中的最大值。
(code:)
for(int i=1;i<=n;i++){
for(int j=head[i];j;j=e[j].next){
int v = e[j].to;
if(scc[i]!=scc[v]){
in[scc[v]]++;//记录入度
out[scc[i]]++;//记录出度
}
}
}
int ansin = 0,ansout = 0;//出度数和入度数
for(int i=1;i<=k;i++){
if(in[i]==0){//记录入度
ansin++;
}
if(out[i]==0){//记录出度
ansout++;
}
}
if(k==1){//特判,如果该图已经是一个强连通图了,直接输出1和0即可
cout<<1<<endl<<0<<endl;
return 0;
}
cout<<ansin<<endl<<max(ansin,ansout);
return 0;
}
P1726 上白泽慧音
车万题面好评
也是一道挺模板的题。
大致题意就是说求最大且字典序最小的那个强联通分量。
直接跑一遍(tarjan)记录每个点所在的强连通分量和该强联通分量的大小,然后一遍(for)循环求出最大的那个组,再将在那个组中的节点输出即可
(code:)
void tarjan(int u){
dfn[u] = low[u] = ++Time;
vis[u] = 1,s[++top] = u;
for(int i=head[u];i;i=e[i].next){
int v = e[i].to;
if(!dfn[v]){
tarjan(v);
low[u] = min(low[u],low[v]);
}
else if(vis[v]){
low[u] = min(low[u],low[v]);
}
}
if(dfn[u]==low[u]){
int v;
int tot = 0;
scc[u] = ++k;//新分组
while(v=s[top--]){
vis[v] = 0;
scc[v] = k;//记录每个点所在的组
size[k]++;//记录该组的大小
if(v==u) break;
}
}
}
P1262 间谍网络
直接跑一遍缩点,由于强连通分量中的每个点都是互相联通的,一个强联通分量中的最少花费资金也就是那个环里罪犯所需资金最小的
最后检查一下有无未访问的点,若有,直接输出(NO),(return),若无,那就接着记录每个点的入度,将入度为0的组的权值全部加起来即可
(code:)
void tarjan(int u){
dfn[u] = low[u] = ++Time;
s[++top] = u;
vis[u] = 1;
for(int i=head[u];i;i=edge[i].next){
int v = edge[i].to;
if(!dfn[v]){
tarjan(v);
low[u] = min(low[v] , low[u]);
}
else if(vis[v]){
low[u] = min(low[v] , low[u]);
}
}
if(low[u]==dfn[u]){
int v;
tot++;
while(v = s[top--]){
vis[v] = 0;
c[v] = tot;
sum[tot] = min(sum[tot],w[v]);//取min
if(v==u) break;
}
}
}
P5676 [GZOI2017]小z玩游戏
对于这道题,首先想到的肯定是直接(n^2)建边
但会发现数据太大,复杂度接受不了
所以要考虑更优秀的建边方法
-
建一个由有趣程度到游戏的边
-
建一个由游戏到兴趣程度的边
-
建立一个兴趣程度整数倍的边
(code:)
for(int i=1;i<=n;i++){//从有趣程度到该游戏
int u;
scanf("%d",&u);
add(n+u,i);
maxn = max(maxn , u);
}
for(int i=1;i<=n;i++){//从该游戏到兴趣程度
int u;
scanf("%d",&u);
add(i,n+u);
}
for(int i=1;i<=maxn;i++){//兴趣程度的整数倍
for(int j=2;j*i<=maxn;j++){
add(n+i,n+j*i);
}
}
对于那些存在兴趣程度整数倍的游戏,相当于是连上了两个游戏
而对于那些不存在的游戏,相当于是连了一个虚点,(rt)
割点练习
P5058 [ZJOI2004]嗅探器
要满足同时能收到(A)和(B)的信息的话要满足两个条件:
-
(u)是割点
-
(A)或(B)在(v)子树内(包括(v))且(B)或(A)不在(v)子树内(这样才能把(A),(B)两个点的信号都收集到)
因此,如果要成为满足条件的点,必须要满足:
- (dfn[v]<=dfn[a])&&(dfn[v]>dfn[b]) ((A)在子树内,(B)不在)
或
- (dfn[v]<=dfn[b])&&dfn([v]>dfn[a])((B)在子树内,(A)不在)
(code:)
void tarjan(int u,int fa){
dfn[u] = low[u] = ++Time;
for(int i=head[u];i;i=edge[i].next){
int v = edge[i].to;
if(!dfn[v]){
tarjan(v,u);
low[u] = min(low[v] , low[u]);
if(low[v]>=dfn[u]&&u!=a&&u!=b&&dfn[v]<=dfn[a]&&dfn[v]>dfn[b]){//第一种情况
ans = min(ans , u);//取最小的点
}
if(low[v]>=dfn[u]&&u!=a&&u!=b&&dfn[v]<=dfn[b]&&dfn[v]>dfn[a]){//第二种情况
ans = min(ans , u);//取最小的点
}
}
else if(v!=fa) low[u] = min(low[u] , dfn[v]);
}
}