Tarjan

重学图论


前置芝士

强连通:对于点x,y,存在一条x到y,和y到x的路径

强连通图:图中每两个点都强联通

强联通分量:极大强联通子图

都是定义在有向图中的

应用:缩点

我们对一张图进行DFS,可以得出一颗搜索树

在这里插入图片描述

对于搜索树上的每一条边,我们都可以归为四类:

  • 树枝边(x,z):x是y的父节点

  • 前向边(x,y):x是y的祖先节点

  • 后向边(x,y):就是前向边反过来

  • 横叉边(y,a):连向其他树枝的边(需要注意的是,(y,b)并不是一条横叉边,因为b节点是第一次被访问到,因此就成了树枝边)

那么如何判断一个点是否处于强联通分量中呢?

有这两种情况:

  • 它可以回到某一个祖先节点(存在一条后向边,指向祖先节点)
  • 它存在一条横叉边,该横叉边走到了祖先节点

感性理解一下

于是,就有了:

Tarjan算法求强联通分量(SCC)

引入概念:时间戳

就是按照DFS序,给每个节点一个编号

这样的话我们得到了一些性质:

  • 树枝边/前向边的x小于y
  • 后箱变x大于y
  • 横叉边x大于y

我们对于每个点,定义两个时间戳:

dfn[u]表示遍历到u的时间

low[u]表示从u开始走,所能遍历到的最小时间戳

u是其所在强联通分量的最高点 ⟺ \Longleftrightarrow dfs[u]=low[u]

就是从u开始走的话,无论如何也走不到u前面的点

证明懒得找了…

感性理解

板子:

void tarjan(int u){
	dfn[u]=low[u]=++timestamp;
	stk[++top]=u,in_stk[u]=true;
	for(int i=hed[u];i;i=nxt[i]){
		int j=ver[i];
		if(!dfn[j]){
			tarjan(j);
			low[u]=min(low[u],low[j]);
		}
		else if(in_stk[j]) low[u]=min(low[u],dfn[j]);
	}
	if(dfn[u]==low[u]){
		int y;
		do{
			y=stk[top--];
			in_stk[y]=false;
			id[y]=scc_cnt;
		}while(y!=u);
	}
	return;
}

时间复杂度O(n+m);

值得注意的是,代码中栈存的点都不是强联通分量的顶点

当前还没有遍历完的强联通分量的所有点

缩点

for(int i=1;i<=n;i++)
    for(int i=hed[u];i;i=nxt[i])
        if(i和j不在同一SCC中)
            add(i,j);//加边

缩完点后,我们就得到了一个DAG

很多人会在缩点后进行一边拓扑排序

但是这可能没啥用,因为tar完之后连通分量标号递减的顺序一定是拓扑序

受欢迎的牛

首先我们进行缩点,因为在DAG上操作更舒服

一个比较显然的结论是:如果图中存在两个 出度为零的点,那么这两头牛一定互不欢迎

同理只存在一个出度为零的点,那么这个点受所有其他牛的欢迎

注意,这个点使我们缩点后的点,因此该点集中的点的个数就是答案

不过我们没有必要重新建出一个DAG其实

复杂度 O ( n ) O(n) O(n)

代码:

/*************************************************************************
    > File Name: p2341受欢迎的牛.cpp
    > Author: typedef
    > Mail: 1815979752@qq.com 
    > Created Time: 2020/12/16 22:15:12
 ************************************************************************/
#include<bits/stdc++.h>
using namespace std;
const int N=10010,M=50010;
int n,m;
int hed[N],ver[M],nxt[M];
int tot=0;
int dfn[N],low[N],timestamp;
int stk[N],top;
bool in_stk[N];
int id[N],scc_cnt,size[N];
int dout[N];
void add(int x,int y){
	ver[++tot]=y;
	nxt[tot]=hed[x];
	hed[x]=tot;
	return;
}
void tarjan(int u){
	dfn[u]=low[u]=++timestamp;
	stk[++top]=u,in_stk[u]=true;
	for(int i=hed[u];i;i=nxt[i]){
		int j=ver[i];
		if(!dfn[j]){
			tarjan(j);
			low[u]=min(low[u],low[j]);
		}
		else if(in_stk[j]) low[u]=min(low[u],dfn[j]);
	}
	if(dfn[u]==low[u]){
		++scc_cnt;
		int y;
		do{
			y=stk[top--];
			in_stk[y]=false;
			id[y]=scc_cnt;
			size[scc_cnt]++;
		}while(y!=u);
	}
	return;
}
int main(){
	scanf("%d%d",&n,&m);
	while(m--){
		int a,b;
		scanf("%d%d",&a,&b);
		add(a,b);
	}
	for(int i=1;i<=n;i++)
		if(!dfn[i])
			tarjan(i);
	for(int i=1;i<=n;i++)
		for(int j=hed[i];j;j=nxt[j]){
			int k=ver[j];
			int a=id[i],b=id[k];
			if(a!=b) dout[a]++;
		}
	int zeros=0,sum=0;
	for(int i=1;i<=scc_cnt;i++){
		if(!dout[i]){
			zeros++;
			sum+=size[i];
			if(zeros>1){
				sum=0;
				break;
			}
		}
	}
	printf("%d\n",sum);
	return 0;
}

学校网络

首先缩点,把它艹成有向无环图

这样我们变得到了 p 个起点, q 个终点

因此我们至少给这 p 个点发信息,才能传到所有点,这便是第一问

那么第二问,我们只需要加 Max{p,q} 条边了

为什么呢?

我们不妨设 p 小于等于 q

边界情况: p 中有一个点

那么我们只需加 q 条边

若 p 大于1,那么 q,p 大于等于 2

我们至少可以找到两组点

p1 能走到 q1

p2 能走到 q2

反证法:如果找不到这样的两组点

那么必然意味着所有的出发点p都能走到同一个终点

由于终点至少有两个,那么另一个终点就走不回来了

与假设矛盾

因此我们仅需把q1连到p2

这样按照我们的定义,p和q的个数都将会减一

因此我们只需要连 |P| -1次边

然后再连|Q|-(|P|-1)次;

总的次数就是 Q 次

/*************************************************************************
    > File Name: p2746校园网.cpp
    > Author: typedef
    > Mail: 1815979752@qq.com 
    > Created Time: 2020/12/17 21:38:26
 ************************************************************************/
#include<bits/stdc++.h>
using namespace std;
const int N=110,M=N*N;
int n;
int hed[N],ver[M],nxt[M];
int tot=0;
int dfn[N],low[N],timestamp;
int stk[N],top;
bool in_stk[N];
int id[N],scc_cnt,size[N];
int dout[N],din[N];
void add(int x,int y){
	ver[++tot]=y;
	nxt[tot]=hed[x];
	hed[x]=tot;
	return;
}
void tarjan(int u){
	dfn[u]=low[u]=++timestamp;
	stk[++top]=u,in_stk[u]=true;
	for(int i=hed[u];i;i=nxt[i]){
		int j=ver[i];
		if(!dfn[j]){
			tarjan(j);
			low[u]=min(low[u],low[j]);
		}
		else if(in_stk[j]) low[u]=min(low[u],dfn[j]);
	}
	if(dfn[u]==low[u]){
		++scc_cnt;
		int y;
		do{
			y=stk[top--];
			in_stk[y]=false;
			id[y]=scc_cnt;
		}while(y!=u);
	}
	return;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		int t;
		while(cin>>t,t) add(i,t);
	}
	for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
	for(int i=1;i<=n;i++){
		for(int j=hed[i];j;j=nxt[j]){
			int k=ver[j];
			int a=id[i],b=id[k];
			if(a!=b){
				dout[a]++;
				din[b]++;
			}
		}
	}
	int a=0,b=0;
	for(int i=1;i<=scc_cnt;i++){
		if(!din[i]) a++;
		if(!dout[i]) b++;
	}
	printf("%d\n",a);
	if(scc_cnt==1) puts("0");
	else printf("%d\n",max(a,b));
	system("pause");
	return 0;
}

注意特判QwQ

最大半连通子图

前置定义:

  • 半联通:所谓半联通就是值可以满足从u走到v或从v走到u其中之一

  • 导出子图:我们先从这张图中选出一些点,再加上相关的边(就是如果一条边连接的两个点属于这些点,那这条边就是相关的)

  • 如果导出的子图是半联通的,那他就是半联通子图

  • 最大半连通子图就是节点数最多的半联通子图

我们要做的是求出最大半联通子图的节点数量,以及不同最大半联通子图的个数

强联通分量显然是半联通的

所以我们先求出所有的强联通分量

在缩点,艹成拓扑图

我们要做的就是选一条最长(节点数量最大)链(不能分叉)

这条链就是所求

第二问呢?

我们就用递推好了

f[i]表示以第i个点为终点的最长链的节点数

如何求?

我们枚举i的前驱

搞一个g(i),记录方案数

如果能够更新f(i)=f(j)+s(i),g(i)=g(j);

如果恰好相等,我们就把方案数相加

有点像背包求方案数

总结:

  • tarjan
  • 缩点,建图,给边判重
  • 按拓扑序递推

优化常数:哈希表判重

/*************************************************************************
    > File Name: p2272最大半联通子图.cpp
    > Author: typedef
    > Mail: 1815979752@qq.com 
    > Created Time: 2020/12/18 20:11:13
 ************************************************************************/
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+7,M=2e6+7;
int n,m,mod;
int hed[N],heds[N],ver[M],nxt[M],tot;
int dfn[N],low[N],stk[N];
int timestamp,top;
bool in_stk[N];
int id[N],scc_cnt,size[N];
int f[N],g[N];
void add(int h[],int x,int y){
	ver[++tot]=y;
	nxt[tot]=h[x];
	h[x]=tot;
	return;
}
void tarjan(int u){
	dfn[u]=low[u]=++timestamp;
	stk[++top]=u,in_stk[u]=true;
	for(int i=hed[u];i;i=nxt[i]){
		int j=ver[i];
		if(!dfn[j]){
			tarjan(j);
			low[u]=min(low[u],low[j]);
		}
		else if(in_stk[j]) low[u]=min(low[u],dfn[j]);
	}
	if(dfn[u]==low[u]){
		++scc_cnt;
		int y;
		do{
			y=stk[top--];
			in_stk[y]=false;
			id[y]=scc_cnt;
			size[scc_cnt]++;
		}while(y!=u);
	}
	return;
}
int main(){
	scanf("%d%d%d",&n,&m,&mod);
	while(m--){
		int a,b;
		scanf("%d%d",&a,&b);
		add(hed,a,b);
	}
	for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
	unordered_set<ll> S;
	for(int i=1;i<=n;i++)
		for(int j=hed[i];i;i=nxt[j]){
			int k=ver[j];
			int a=id[i],b=id[k];
			ll hash=a*1000000ll+b;
			if(a!=b&&!S.count(hash)){
				add(heds,a,b);
				S.insert(hash);
			}
		}
	for(int i=scc_cnt;i>=1;i--){
		if(!f[i]) {
			f[i]=size[i];
			g[i]=1;
		}
		for(int j=heds[i];j;j=nxt[j]){
			int k=ver[j];
			if(f[k]<f[i]+size[k]){
				f[k]=f[i]+size[k];
				g[k]=g[i];
			}
			else if(f[k]==f[i]+size[k]) g[k]=(g[k]+g[i])%mod;
		}
	}
	int maxf=0,sum=0;
	for(int i=1;i<=scc_cnt;i++)
		if(f[i]>maxf){
			maxf=f[i];
			sum=g[i];
		}
		else if(f[i]==maxf) sum=(sum+g[i])%mod;
	printf("%d\n%d\n",maxf,sum);
	return 0;
}

大概就是这样…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值