首页 > 其他分享 >tarjan-xue-xi-bi-ji

tarjan-xue-xi-bi-ji

时间:2023-03-10 17:58:54浏览次数:47  
标签:tarjan xi int cnt edge xue dfn maxn low

title: 'tarjan学习笔记'
date: 2022-12-30 15:00:56
tags: [学习笔记]
published: true
hideInList: false
feature: 
isTop: false

1.概念

  • 强连通分量:在有向图或无向图中的点集,保证集合中每两个点都可以互相到达。
  • 割点:一个点是割点当且仅当删除这个点之后连通块的数量会增加。
  • :一条边是桥当且仅当删除这条边后连同快的个数会增加。
  • 树边:遍历一个图时的dfs树上的边。
  • 非树边:图中除了树边以外的其他边。
  • dfn:时间戳,即dfs一个图时点是第几个被遍历到的。
  • low:子树内所有节点所能通过走非树边到的点的dfn的最小值。

2.算法

tarjan的算法本质其实就是在dfs图的时候求dfn和low这两个值,通过这两个值可以求出很多东西,详见应用。

对于dfn很好处理,在dfs时直接记录即可。对于 $low_x$,以及现在有一条 $(x,y)$ 的边,如果 $y$ 还没有遍历过,说明这是树边,$y$ 是 $x$ 子树内的节点,所以 $low_x=\min{low_x,low_y}$。如果 $y$ 已经遍历过了,则这条边为非树边(此时 $y$ 有可能为 $x$ 的父亲,因为可能有重边)或者树边(此时 $y$ 必定 $x$ 的父亲)。如果是非树边的话 $low_x=\min{low_x,dfn_y}$。

3.应用

3-1割点

如果一个点是割点,那么他的任意一个儿子的子树内的点都不会不经过这个点到达 $dfn$ 较小的点(即这个点的祖先),那么如果将这个点删去,这个点的某儿子的子树将会与自己的祖先断开。
模板

#include<bits/stdc++.h>
using namespace std;
const int maxn=20010;
int n,m,cnt=1,tim,rt,t[maxn],dfn[maxn],low[maxn],head[maxn];
vector<int>ans;
bool vis[maxn];
struct E{
    int to,next;
}edge[maxn*20];
void add(int u,int v){
    edge[++cnt].to=v;edge[cnt].next=head[u];head[u]=cnt;
}
void tarjan(int x,int re){
    dfn[x]=low[x]=++tim;
    int flag=0;
    for(int i=head[x];i;i=edge[i].next){
        int y=edge[i].to;
        if(!dfn[y]){
            tarjan(y,i);
            low[x]=min(low[x],low[y]);
            if(dfn[x]<=low[y]){
                if(vis[x])continue;
                if(x!=rt)ans.push_back(x),vis[x]=1;
                else if(flag>=1)ans.push_back(x),vis[x]=1;
                flag++;
            }
        }else low[x]=min(low[x],dfn[y]);
    }
    return;
}
int main(){
    ios::sync_with_stdio(false);
    std::cin.tie(0);std::cout.tie(0);
    cin>>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++){
        tim=0;rt=i;
        if(!dfn[i])tarjan(i,0);
    }
    for(int i=1;i<=n;i++)if(t[i]>=2)ans.push_back(i);
    cout<<ans.size()<<endl;
    sort(ans.begin(),ans.end());
    unique(ans.begin(),ans.end());
    for(int x:ans)cout<<x<<' ';
    return 0;
}

3-2桥

如果一条是桥,那么它肯定是树边。因为删掉任意一条非树边,点仍然可以通过树边相连,因此不会增加连通块的数量。而对于x指向y的树边 $(x,y)$,如果它是桥,那么y不能”绕开“ $(x,y)$ 这条边,所以y可以到的最小的点的dfn值必然会大于x。
模板

#include<bits/stdc++.h>
using namespace std;
const int maxn=20010;
int n,m,cnt=1,tim,rt,dfn[maxn],low[maxn],head[maxn];
vector<pair<int,int> >ans;
bool vis[maxn];
struct E{
    int from,to,next;
}edge[maxn*20];
void add(int u,int v){
    edge[++cnt].to=v;edge[cnt].from=u;
    edge[cnt].next=head[u];head[u]=cnt;
}
void tarjan(int x,int re){
    dfn[x]=low[x]=++tim;
    int flag=0;
    for(int i=head[x];i;i=edge[i].next){
        int y=edge[i].to;
        if(!dfn[y]){
            tarjan(y,i);
            low[x]=min(low[x],low[y]);
            if(dfn[x]<low[y]){
                pair<int,int>tmp=make_pair(edge[i].from,edge[i].to);
                if(tmp.first>tmp.second)swap(tmp.first,tmp.second);
                if(x!=rt||flag>=1)ans.push_back(tmp);
                flag++;
            }
        }else if(re!=(i^1))low[x]=min(low[x],dfn[y]);
    }
    return;
}
int main(){
    ios::sync_with_stdio(false);
    std::cin.tie(0);std::cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int u,v;cin>>u>>v;
        add(u,v);add(v,u);
    } 
    tarjan(1,0);
    sort(ans.begin(),ans.end());
    for(pair<int,int> x:ans)cout<<x.first<<' '<<x.second<<endl;
    return 0;
}

3-3缩点

对于一个有向图,每个强连通分量可以缩为1个点,使得原图变为一个有向无环图。

先叙述过程在解释原理:我们维护一个栈,每dfs到一个点就入栈,如果我们dfs到了 $x$,回溯时如果 $dfn_x=low_x$ 那么就将 $x$ 及 $x$ 的上方的点出栈,这些点构成一个强连通分量。

原理:那么在每个点回溯时,每个点及其栈中上方的所有点构成了这个点的子树,如果 $dfn_x=low_x$ 那么意味着子树内每个点都无法通过非树边到达 $dfn$ 更小的点。我们设栈中 $x$ 上方的点构成集合 $S$。对于 $y \in S$ 我们有结论:

  • $low_y=x$ :因为 $low_x==dfn_x$ 所以根据定义 $low_y \ge dfn_x$。但是如果 $low_y>dfn_x$ 的话,$dfn=low_y$ 的点回溯时就将 $y$ 从栈中弹出了,因此 $low_y=dfn_x$。

既然 $low_y=dfn_x$ 那么 $y$ 肯定能通过非树边走到 $x$,也就是子树内出现了环。所以 $y$ 与 $x$ 在同一强连通分量中,且 $dfn_x$ 为该强连通分量中最小的。

具体地,我们再举个例子。

在该图中,$(1,2) , (2,5) , (5,6) , (2,3)$ 是树边,而 $4$ 号点与其他点不连通。

我们从1号点开始dfs,我们会dfs到2,然后dfs到5,再dfs到6。

此时6无法走到任何地方,于是开始回溯,因为 $low_6=dfn_6$ 且6在栈顶,所以6单独为1个强连通分量。

接下来我们回溯到5,此时 $low_5=\min{low_5,low_6}=5=dfn_5$ 且5在栈顶,所以5单独为一个强联通分量。

接下来我们回到2,继续dfs到3,这时,3有一条到1的非树边,所以 $low_3=\min{low_3,dfn_1}=1$,然后回溯到2。$low_2=\min{low_2,low_3}=1$,$low_2 \ne dfn_2$ 所以继续回溯。

回溯到1,因为 $low_1=dfn_1$ 所以1,2,3为一个强连通分量。
模板
只需要记录每个强连通分量的大小即可。

4.练习

受欢迎的牛
缩点后每个点的奶牛必然互相爱慕,所以缩点后的点如果有仅有一个出度为0的点,那么这个点的所有牛都是明星牛。所以我们再tarjan的时候需要记录强连通分量的大小。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
using namespace std;
const int maxn=100010,maxm=200010;
int head[maxn],low[maxn],dfn[maxn],stack[maxn],size[maxn];
int cd[maxn],id[maxn];
int top,tot,cnt,n,m,col;
bool vis[maxn];
struct E{
	int to,next,from;
}edge[maxm];
vector<int>g[maxn];
void add(int u,int v){
	edge[++cnt].to=v;
	edge[cnt].next=head[u];
	head[u]=cnt;edge[cnt].from=u;
}
void tarjan(int x){
	dfn[x]=low[x]=++tot;
	stack[++top]=x;
	vis[x]=1;
	for(int i=head[x];i;i=edge[i].next){
		int y=edge[i].to;
		if(!dfn[y]){
			tarjan(y);
			low[x]=min(low[x],low[y]);
		}else if(vis[y])low[x]=min(low[x],dfn[y]);
	}
	if(low[x]==dfn[x]){
		int y;++col;id[x]=col;
		while(y=stack[top--]){
			vis[y]=0;
			size[col]++;
			id[y]=col;
			if(x==y)break;
		}
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		add(x,y);
	}
	for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i);
    for(int i=1;i<=m;i++){
		if(id[edge[i].from]!=id[edge[i].to]){
			cd[id[edge[i].from]]++;
		}
	}
	int sum=0,ans=0;
	for(int i=1;i<=col;i++)if(!cd[i])sum++,ans=size[i];
	printf("%d\n",sum==1?ans:0);
	return 0;
}

校园网
对于缩完点的图。

  • 任务A:入度为0的点没有点给他软件,所以需要自己得到软件副本,所以答案为入度为0的点的数量。
  • 任务B:我们直接将出度为0的点连给入度为0的点,因此答案就是出度为0的点和入度为0的点数的更大值。
#include<bits/stdc++.h>
using namespace std;
const int maxn=5010;
int n,m,cnt,tim,ans,res;
int f[maxn],rd[maxn],cd[maxn],dfn[maxn],low[maxn],head[maxn];
bool vis[maxn];
stack<int>s;
queue<int>q;
struct E{
    int from,to,next;
}edge[maxn];
void add(int u,int v){
    edge[++cnt].to=v;edge[cnt].from=u;edge[cnt].next=head[u];head[u]=cnt;
}
int find(int x){
    if(x==f[x])return x;
    else return f[x]=find(f[x]);
}
void tarjan(int x){
    dfn[x]=low[x]=++tim;
    s.push(x);vis[x]=1;
    for(int i=head[x];i;i=edge[i].next){
        int y=edge[i].to;
        if(!dfn[y]){
            tarjan(y);
            low[x]=min(low[x],low[y]);
        }else if(vis[y])low[x]=min(low[x],dfn[y]);
    }
    if(dfn[x]==low[x]){
        while(s.top()!=x){
            f[s.top()]=x;
            vis[s.top()]=0;
            s.pop();
        }vis[x]=0;s.pop();
    }
    return;
}
int main(){
    ios::sync_with_stdio(false);
    std::cin.tie(0);std::cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++){
        int y;cin>>y;f[i]=i;
        while(y!=0){
            add(i,y);
            cin>>y;
        }
    }
    for(int i=1;i<=n;i++){
        if(!dfn[i])tarjan(i);
    }
    for(int i=1;i<=cnt;i++){
        int u=edge[i].from,v=edge[i].to;
        int fx=find(u),fy=find(v);
        if(fx==fy)continue;
        rd[fy]++;cd[fx]++;
    }
    for(int i=1;i<=n;i++)if(f[i]==i&&rd[i]==0)ans++;
    cout<<ans<<endl;
    for(int i=1;i<=n;i++)if(f[i]==i&&cd[i]==0)res++;
    int tmp=0;
    for(int i=1;i<=n;i++)tmp+=f[i]==i;
    if(tmp==1)cout<<0;
    else cout<<max(ans,res);
    return 0;
}

标签:tarjan,xi,int,cnt,edge,xue,dfn,maxn,low
From: https://www.cnblogs.com/ybaggio/p/17204290.html

相关文章

  • shu-lian-pou-fen-xue-xi-bi-ji
    title:'树链剖分学习笔记'date:2023-01-1408:52:28tags:[学习笔记]published:truehideInList:falsefeature:isTop:false前言我们知道线段树可以处理区间......
  • Axios
    Axios是什么?Axios是一个基于 promise 网络请求库,作用于node.js 和浏览器中。在服务端它使用原生node.js http 模块,而在客户端(浏览端)则使用XMLHttpReques......
  • [vp记录] 2021 Summer Petrozavodsk Camp, Day 3: IQ test (XXII Open Cup, Grand Pri
    2021SummerPetrozavodskCamp,Day3:IQtest(XXIIOpenCup,GrandPrixofIMO)A(性质,转化)发现如果存在\(b\)中存在\(0\),那么直接构造\(b_1,0,b_2,0,\dots......
  • kaldi在linux上编译,Ubuntu 12.04下编译安装Kaldi https://blog.csdn.net/we
    因为同事工作需要kaldi,所以安装过程有点麻烦。在此记录一下折腾的过程。OS:Ubuntu 12.04(amd64)kaldi的下载地址 http://svn.code.sf.net/p/kaldi/code/ 我这里下......
  • ESXI下硬盘的两种直通方式
    文章来自:https://rmbz.net/archives/vmware-esxi-passthrough最近再搞ESXI,把原来的“黑群晖”改成ESXI;因为群晖里有数据,为了不想迁移数据所以需要对硬盘做直通0x01RDM......
  • 浅谈 Axios 和 Fetch 的区别
    1.简单区分   2.请求方式axios传一个对象,里面包含请求url和请求方法,参数。fetch传两个参数,第一个是请求url,第二个是请求的一些参数。//axios请求:constoptio......
  • esxi 安装记录
    (17条消息)win10安装VMwarePowerCLI_powercli离线安装_dnpao的博客-CSDN博客 先安装vmwarepoweshellcli (17条消息)ESXI驱动注入_vistaup的博客-CSDN博客 ......
  • vue生命周期以及如何将axios挂载到vue的原型链上
    生命周期组件的生命周期是指一个组件从创建->运行->销毁的整个阶段,强调的是一个时间段生命周期函数:是由vue框架提供的内置函数,会伴随着组件的生命周期,自动按次序执行 ......
  • vCenter 添加ESXI6.0主机失败处理过程
     报错 无法访问指定的主机(x.x.x.x)。此主机在网络上不间用,网络配置有问题或主机上的管理服务无响应。在集群下添加ESXI6.0主机的时候,输入ESXI用户名和密码后发现认证可......
  • 670. Maximum Swap
    670.MaximumSwap标签(空格分隔):leetcodemedium题目Givenanon-negativeinteger,youcouldswaptwodigitsatmostoncetogetthemaximumvaluednumb......