强联通分量 和 双连通分量 (tarjan)

强连通分量:

P2272 最大半连通子图(强连通缩点建GAD+DP)

题目描述
一个有向图 G=(V,E) 称为半连通的 (Semi-Connected),如果满足:∀u,v∈V,满足 u→v 或 v→u,即对于图中任意两点 u,v,存在一条 u 到 v 的有向路径或者从 v 到 u 的有向路径。
若 G′=(V′,E′) 满足,E′ 是 E 中所有和 V′ 有关的边,则称 G′ 是 G 的一个导出子图。
若 G′ 是 G 的导出子图,且 G′ 半连通,则称 G′ 为 G 的半连通子图。
若 G′ 是 G 所有半连通子图中包含节点数最多的,则称 G′ 是 G 的最大半连通子图。
给定一个有向图 G,请求出 G 的最大半连通子图拥有的节点数 K,以及不同的最大半连通子图的数目 C。
由于 C 可能比较大,仅要求输出 C 对 X 的余数。

思路:对于一条拓扑链,满足题目描述的半联通子图的性质,所以最大半连通子图即是DAG中最长的一条链,所以通过 tarjan + 缩点 建立一个新的GAD;然后再递推求出答案;

#include<bits/stdc++.h>
#include<unordered_set>

using namespace std;
const int N = 1e5+7;
const int M = 2e6+7;
typedef long long LL;

int n,m,mod;
int h[N],hs[N],e[M],ne[M],idx;
int dfn[N],low[N],timestamp;  //时间戳
int stk[N],top,scc_cnt;		//递归栈 ,scc编号
bool in_stk[N];
int Size[N],id[N];	//scc大小,每个点所属scc
int f[N];  //以i结尾的点的最大节点数
int g[N];  //以i结尾的点的方案数

void add(int h[],int a,int b)
{
	e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}

void tarjan(int u)		//tarjan模板
{
	dfn[u] = low[u] = ++timestamp;
	stk[++top] = u,in_stk[u] = 1;
	for(int i=h[u];~i;i=ne[i]){
		int j = e[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 x;
		++scc_cnt;
		do{
			x = stk[top--];
			in_stk[x] = 0;
			id[x] = scc_cnt;
			Size[scc_cnt] ++;
		}while(x!=u);
	}
}

int main()
{
	memset(h,-1,sizeof h);
	memset(hs,-1,sizeof hs);
	scanf("%d%d%d",&n,&m,&mod);
	while(m--){
		int x,y;
		scanf("%d%d",&x,&y);
		add(h,x,y);
	}
	for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
	
	unordered_set<LL> hash;		//去重边,避免重复计算
	for(int i=1;i<=n;i++){
		for(int j=h[i];~j;j=ne[j]){
			int k = e[j];
			int a = id[i],b = id[k];	//建新图
			LL Hash = a*1000000ll + b;
			if(a!=b&&!hash.count(Hash)) {
				add(hs,a,b);
				hash.insert(Hash);
			}
		}
	}
	for(int i=scc_cnt;i>0;i--){		//scc编号的递减顺序即为拓扑序
		if(!f[i]){  //入度为零的点
			f[i ] = Size[i];
			g[i] = 1;
		}
		for(int j=hs[i];~j;j=ne[j]){	//递推求出f[i], g[i]
			int k = e[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,ans=0;
	for(int i=1;i<=scc_cnt;i++){	  //求最长路
		if(maxf<f[i]) maxf = f[i],ans = g[i];
		else if(maxf==f[i]) ans = (ans+g[i])%mod;
	}
	printf("%d\n%d\n",maxf,ans);
	return 0;
}

E. Chain Email (强连通缩点建图+dfs)

A chain email is an email that people receive and then forward to all of their friends. This sort of email is very common amongst elderly people, who have notably bad memories. Elderly people’s memories are so bad that if they ever receive a chain email they will forward it to all of their contacts. This can become very problematic when elderly people continually send the same email to each other. For instance, if two people have each other in their contacts and if either of them receive a chain email they will continually send the email to each other back and forth forever. Email companies are worried that this will result in a massive amount of storage loss on their servers and have asked you to determine if a specific person were to start a chain email, who would receive that email forever.
The Problem:
Given each elderly person’s contacts and which elderly person will be starting a chain email, determine who will be indefinitely receiving emails.
The Input:
The first line of the input is a positive integer, n, indicating the number of scenarios that your program will have to analyze. Following this will be the description of each scenario. The first line of each scenario will have two single-space-separated integers, p (1 ≤ p ≤ 50), indicating the number of people who use the email service and, s (1 ≤ s ≤ p), indicating the source of the chain email, where each person is labeled from 1 to p. Following this will be a single line with the names of all of the people, from person 1 to person p, who use the email service, each separated by exactly one space. All names will contain alphabetic characters only and be between 1 and 19 characters (inclusive) in length. Following this will be p lines. The ith line will describe the contact list of the ith person. This description will consist of an integer, m (0 ≤ m < p), indicating the number of contacts this person has, followed by the 1-based index of each of the contacts, each separated by exactly one space. It’s guaranteed that no one will contain themselves as a contact.
The Output:
The first line of the output for each scenario should be “Chain Email #d:”, where d is the scenario number, starting with 1. Following this should be a line containing the names of all of the people who will infinitely receive chain emails, assuming that everyone continually forwards the email to all of their contacts. Each name should be followed by a space. List these contacts in the order that they appear in the input. If no one will infinitely receive chain emails, then print “Safe chain email!” instead. Leave a blank line after the output for each data set. Follow the format illustrated in Sample
The Output:
The first line of the output for each scenario should be “Chain Email #d:”, where d is the scenario number, starting with 1. Following this should be a line containing the names of all of the people who will infinitely receive chain emails, assuming that everyone continually forwards the email to all of their contacts. Each name should be followed by a space. List these contacts in the order that they appear in the input. If no one will infinitely receive chain emails, then print “Safe chain email!” instead. Leave a blank line after the output for each data set. Follow the format illustrated in Sample Output.

样例输入

3
3 1
James Sarah John 
2 2 3       
2 1 3       
2 1 2 
3 1 
James Sarah John 
2 2 3 
0 
0 
6 3 
Ali Matt Glenn Sumon Arup Chris 
2 3 5 
0 
1 4 
1 1 
1 2 
2 5 4 

样例输出

Chain Email #1: 
James Sarah John 

Chain Email #2: 
Safe chain email! 

Chain Email #3: 
Ali Matt Glenn Sumon Arup 

题意:给n个人编号1~n,组成一个关系有向图,从起点发出一封邮件,问那些人会一直收到同一封邮件;
思路:tarjan缩点建图,显然在一个SCC节点个数大于1的子图内是可以无限循环的,所以在新建成的GAD中有一条初始消息链,在这条链中假设SCC节点个数大于1的节点为 vi ,那么从 vi 能到达的节点也能无限收到消息 , 若 vi 不存在则没有消息循环;

#include<bits/stdc++.h>
using namespace std;
const int N = 100,M = N*N;
 
int n,s;
int h[N],hs[N],e[M],ne[M],idx;
int dfn[N],low[N],timestamp;   //时间戳
int stk[N],top;   //联通栈
bool is_stk[N];   //标记是否在栈中
int id[N],scc_cnt;  //每个点所属SCC
vector<int> dd[N];  //记录每个SCC内的点
vector<int> scc_vis;  //初始链上size大于1的节点 
string name[N];
bool ans[N];  //存答案
 
void add(int h[],int a,int b){
	e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}

void init(){
	for(int i=0;i<=n;i++) dd[i].clear(),is_stk[i]=0,id[i]=0;
	scc_cnt=0;timestamp=0;top=0;scc_vis.clear(); 
	memset(dfn,0,sizeof dfn);
	memset(low,0,sizeof low);
	memset(stk,0,sizeof stk); 
}
 
void tarjan(int u)
{
	dfn[u] = low[u] = ++timestamp;
	stk[++top] = u , is_stk[u] = 1;
	for(int i=h[u];~i;i=ne[i]){
		int j = e[i];
		if(!dfn[j]){
			tarjan(j);
			low[u] = min(low[u],low[j]);
		}
		else if(is_stk[j]) low[u] = min(low[u],dfn[j]);
	}
	if(dfn[u]==low[u]){
		int x;
		++scc_cnt;
		do{
			x = stk[top--];
			is_stk[x] = 0;
			id[x] = scc_cnt;
			dd[scc_cnt].push_back(x);
		}while(x!=u);
	}
}
void dfs1(int x)
{
	for(int i = hs[x];~i;i=ne[i]){
		int j = e[i];
		if(dd[j].size()>1) scc_vis.push_back(j);
		dfs1(j);
	}
}
void dfs2(int v)
{
	for(auto u:dd[v]) ans[u] = 1;
	for(int i=hs[v];~i;i=ne[i]){
		int j = e[i];
		dfs2(j);
	}
}

int main()
{
	int T,k=1;
	cin>>T;
	while(T--){
		scanf("%d%d",&n,&s);
		for(int i=1;i<=n;i++) cin>>name[i];
		memset(h,-1,sizeof h),idx=0;
		memset(hs,-1,sizeof hs);
		for(int i=1;i<=n;i++){
			int x,y;
			cin>>x;
			while(x--){
				scanf("%d",&y);
				add(h,i,y);
			}
		}
		for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
		
		for(int i=1;i<=n;i++){		 //新建 GAD
			for(int j = h[i];~j;j=ne[j]){
				int k = e[j];
				int a = id[i],b = id[k];
				if(a!=b) add(hs,a,b);
			}
		}
		for(int i=1;i<=n;i++) ans[i]=0;
		
		if(dd[id[s]].size()>1) scc_vis.push_back(id[s]);	//寻找scc_size大于1的节点
		dfs1(id[s]);
		for(auto x:scc_vis) dfs2(x);
		
		printf("Chain Email #%d:\n",k++);
		int num=0;
		for(int i=1;i<=n;i++) if(ans[i]) cout<<name[i]<<" ",num++;
		if(num==0) cout<<"Safe chain email!";
		printf("\n\n");
		init();
	}
	return 0;
}

边双连通分量:

思想:利用 tarjan 算法寻找桥并标记,然后可以利用栈和dfn==low缩点,也可以把桥删去然后使用 bfs 或 dfs 或 并查集缩点,成为一棵树。

P2860 冗余路径 (边双缩点)

思路:首先将所有边-双连通分量缩成点,将整个图转变为一个树,然后在这个规模更小的图上加最少的边数,使得所有缩成的点都在环上。这个连通分量上的广义叶子节点(度数为1)除以2向上取整即为所需要加的边数。

#include<bits/stdc++.h>
using namespace std;
#define F first
#define S second
#define IO ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
typedef long long LL;
typedef unsigned long long ULL;
typedef long double LD;
typedef pair<LL,int> PII;
const LL LINF = 0x3f3f3f3f3f3f3f3f; 
const int INF = 0x3f3f3f3f;
const double eps = 1e-6;
const int mod = 1e9+7;
const ULL P = 131,p1 = 2333,p2=233333;
const int N = 5e3+7;
const int M = 1e4+7;

int h[N],e[M*2],ne[M*2],idx;
int dfn[N],low[N],timestamp;
int stk[N],top;
int id[N],dcc_cnt;
bool is_bridge[N];
int d[N];

void add(int a ,int b){
	e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}

void tarjan(int u,int f)   //找桥 + 缩点
{
	dfn[u] = low[u] = ++timestamp;
	stk[++top] = u;
	for(int i=h[u];~i;i=ne[i]){
		int j = e[i];
		if(!dfn[j]){
			tarjan(j,i);
			low[u] = min(low[u],low[j]);
			if(low[j]>dfn[u]) 
				is_bridge[i] = is_bridge[i^1] = true;   //此边为桥
		}else if(i!=(f^1)) low[u] = min(low[u],dfn[j]);
	}
	if(low[u]==dfn[u]){  //缩点
		++dcc_cnt;
		int y;
		do{
			y = stk[top--];
			id[y] = dcc_cnt;
		}while(y!=u);
	}
}

int main()
{
	IO;
	memset(h,-1,sizeof h);
	int n,m;
	cin>>n>>m;
	for(int i=0,x,y;i<m;i++){
		cin>>x>>y;
		add(x,y),add(y,x);
	}
	tarjan(1,-1);
	for(int i=0;i<idx;i++){
		if(is_bridge[i]) d[id[e[i]]]++;   //缩点后每个点的度数情况
	}
	int cnt=0;
	for(int i=1;i<=dcc_cnt;i++) 
		if(d[i]==1) cnt++;
	
	cout<<(cnt+1)/2<<"\n";    //度数为1的点的个数加1除以2即为答案
	return 0;
}

点双连通分量:

利用 tarjan 寻找割点,然后建图缩点,成为一棵树。

P3388 【模板】割点(割顶) (输出割点)

输出图中的割点(不一定联通);

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+7;
const int M = 1e5+7;

int h[N],e[M*2],ne[M*2],idx;
int dfn[N],low[N],timestamp;
int root;
vector<int> ans;

void add(int a,int b){
	e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}

void tarjan(int u)
{
	dfn[u] = low[u] = ++timestamp;
	int cnt=0;
	for(int i=h[u];~i;i=ne[i]){
		int j = e[i];
		if(!dfn[j]){
			tarjan(j);
			low[u] = min(low[u],low[j]);
			if(low[j]>=dfn[u]){
				cnt++;
				if(u!=root||cnt>1) ans.push_back(u);  //会保存有重复点
			}
		}else low[u] = min(low[u],dfn[j]);
	}
	//if(u==root&&cnt>=2) ans.push_back(u);  //当u是根时,孩子中必须要有一个以上的边双连通分量,此时u才是割点;
	//else if(u!=root&&cnt) ans.push_back(u);
}

int main()
{
	int n,m;
	cin>>n>>m;
	memset(h,-1,sizeof h);
	for(int i=0,x,y;i<m;i++){
		cin>>x>>y;
		add(x,y);add(y,x);
	}
	for(int i=1;i<=n;i++)
		if(!dfn[i]) root=i,tarjan(i);

	sort(ans.begin(),ans.end()); 
	ans.erase(unique(ans.begin(),ans.end()),ans.end());  //此时需要去重
	
	cout<<ans.size()<<"\n";
	for(int i=0;i<ans.size();i++){
		cout<<ans[i];
		if(i!=ans.size()-1) cout<<" ";
	}
	return 0;
}

P3225 [HNOI2012]矿场搭建 (点双缩点)

用Tarjan跑出割点,然后利用递归栈缩点
分类讨论:

1、如果没有割点,至少需要建立两个出口,从任意非割点的地方选择两个点建立

2、如果这个分组只有一个割点,只需要在分组内设立一个出口,可以设立在任意一个非割点的地方

3、如果有两个及以上个割点,则无需建立,可以直接到达其他联通块

#include<bits/stdc++.h>
using namespace std;
#define F first
#define S second
#define IO ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
typedef long long LL;
typedef unsigned long long ULL;
typedef long double LD;
typedef pair<LL,int> PII;
const LL LINF = 0x3f3f3f3f3f3f3f3f; 
const int INF = 0x3f3f3f3f;
const double eps = 1e-6;
const int mod = 1e9+7;
const ULL P = 131,p1 = 2333,p2=233333;
const int N = 1e4+7;
const int M = 1e4+7;

int h[N],e[M],ne[M],idx;
int dfn[N],low[N],timestamp;
int stk[N],top;
int dcc_cnt;
bool cut[N];

vector<int> dcc[N];
int root;

void add(int a,int b){
	e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}

void tarjan(int u)
{
	dfn[u] = low[u] = ++timestamp;
	stk[++top] = u;

	if(u==root&&h[u]==-1){    //单点情况
		++dcc_cnt;
		dcc[dcc_cnt].push_back(u);
		return ;
	}

	int cnt=0;   //每个节点(割点)所连点双个数
	for(int i=h[u];~i;i=ne[i]){
		int j = e[i];
		if(!dfn[j]){
			tarjan(j);
			low[u] = min(low[u],low[j]);
			if(low[j]>=dfn[u]){
				cnt++;
				if(u!=root||cnt>1) cut[u] = true;   //此点是割点
				int y;
				++dcc_cnt;
				do{
					y = stk[top--];
					dcc[dcc_cnt].push_back(y);
				}while(y!=j);  //割点不弹出
				dcc[dcc_cnt].push_back(u);    //割点属于此点双连通分量
			}
		} else low[u] = min(low[u],dfn[j]);
	}
}

int main()
{
	//点双节点大小要开2倍;
	int T=1;
	int m;
	while(cin>>m,m){

		for(int i=0;i<=dcc_cnt;i++) dcc[i].clear();   //多组清空
		memset(h,-1,sizeof h);
		idx = timestamp = top = dcc_cnt = 0;
		memset(dfn,0,sizeof dfn);
		memset(cut,0,sizeof cut);

		int n=0;
		for(int i=0,x,y;i<m;i++){
			cin>>x>>y;
			add(x,y),add(y,x);
			n = max(n,x);n = max(n,y);
		}
		for(root=1;root<=n;root++)
			if(!dfn[root]) tarjan(root);  //每个连通块选取一个根

		int res=0;
		ULL ans=1;
		for(int i=1;i<=dcc_cnt;i++){
			int num=0;
			for(int j=0;j<dcc[i].size();j++)
				if(cut[dcc[i][j]]) num++;  //每个点双里有多少割点
			
			int t = dcc[i].size();
			if(num==0) {	
				if(t>1) res+=2,ans *= t*(t-1)/2;  //没有割点并且点双大小大于1,设置两个出口;
				else res++;
			}else if(num==1) res++, ans *= t-1;  //只有一个割点,设置一个出口;
				// 割点个数大于等于2时,不设置出口。
		}
		printf("Case %d: %d %llu\n",T++,res,ans);
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值