图论--强连通分量(tarjan)

一.DFS森林和强连通分量(SCC)
强连通:u->v,v->u,那么u和v就是强连通的,即u和v互相可达
强连通分量:一个集合内的所有点都互相可达

二.tarjan算法

#include<bits/stdc++.h>

#define x first
#define y second
#define endl '\n'
#define int long long
 
using namespace std;

const int N=1e6+10; 

typedef pair<int,int> PII;

vector<int> e[N]; 
int idx;
int dfn[N],low[N],ins[N],bel[N],cnt;
/*
dfn表示的是dfs序里面的时间戳,即第几个被访问到的 
low表示子树里能跳到的DFN最小的,且未被切掉的 
bel记录一个点归属于哪个scc 
*/ 
stack<int> stk;
vector<vector<int>>scc;

void dfs(int u){
	dfn[u]=low[u]=++idx;
	ins[u]=true;//u这个点有没有被切出去,即确定在一个强连通分量内,ins[u]=true,表示还未被切掉 
	stk.push(u);
	for(auto v:e[u]){
		if(!dfn[v]){
			dfs(v);
			low[u]=min(low[u],low[v]); 
		}else {
			if(ins[v])low[u]=min(low[u],dfn[v]);
		}
	} 
	if(dfn[u]==low[u]){
		vector<int> c;
		cnt++;  
		while(true){
			int v=stk.top();
			c.push_back(v);
			ins[v]=false;
			bel[v]=cnt;
			stk.pop();
			if(u==v) break;
		}
		sort(c.begin(),c.end());
		scc.push_back(c);
	}
}

void slove(){
	int n,m;cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v;cin>>u>>v;
		e[u].push_back(v);
	}
	
	for(int i=1;i<=n;i++)
	if(!dfn[i]) dfs(i);
	sort(scc.begin(),scc.end());
	for(auto c:scc){
		for(auto u:c){
			cout<<u<<" ";
		}
		cout<<endl;
	}
}

signed main(){
	ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
	int T=1;
//	cin>>T;
	while(T--) slove();
}

三.kosaraju算法
DFS一遍,得到出栈顺序(正常的dfs出栈顺序)
重要:
1.DAG出栈顺序是反图的拓扑序
2.有向图 SCC缩点--DAG 最后一个出栈--源点

#include<bits/stdc++.h>

#define x first
#define y second
#define endl '\n'
#define int long long
 
using namespace std;

const int N=1e6+10; 

typedef pair<int,int> PII;

vector<int> e[N],erev[N]; 
int vis[N],n,m;
stack<int> stk;
vector<vector<int>>scc;
vector<int> out,c; 

void dfs(int u){
	vis[u]=true;
	for(auto v:e[u]){
		if(!vis[v])dfs(v);
	}
	out.push_back(u);
}

void dfs2(int u){
	vis[u]=true;
	for(auto v:erev[u]){
		if(!vis[v]) dfs2(v);
	}
	c.push_back(u);
}

void slove(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v;cin>>u>>v;
		e[u].push_back(v);
		erev[v].push_back(u);
	}
	
	for(int i=1;i<=n;i++) if(!vis[i])dfs(i);
	
	reverse(out.begin(),out.end());
	memset(vis,0,sizeof vis);
	
	for(auto u:out)if(!vis[u]){
		c.clear();
		dfs2(u);
		sort(c.begin(),c.end());
		scc.push_back(c);
	}
	
	sort(scc.begin(),scc.end());
	for(auto c:scc){
		for(auto u:c){
			cout<<u<<" ";
		}
		cout<<endl;
	}
}

signed main(){
	ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
	int T=1;
//	cin>>T;
	while(T--) slove();
}

例题1:


有多少个点,所有点都可达
1.首先思考如果这个图是一个DAG的话,那么要满足什么条件?可以发现只有当这个DAG有唯一汇点的时候,才会存在一个点被所有的点喜欢,即DAG-->唯一汇点(没有出度的点)
2.然后就是把一般图给转化为DAG图,一般图->scc缩点->DAG

#include<bits/stdc++.h>

#define x first
#define y second
#define endl '\n'
#define int long long
 
using namespace std;

const int N=1e6+10; 

typedef pair<int,int> PII;

vector<int> e[N]; 
int idx;
int dfn[N],low[N],ins[N],bel[N],cnt,sz[N];
int outd[N]; 
int n,m; 
/*
dfn表示的是dfs序里面的时间戳,即第几个被访问到的 
low表示子树里能跳到的DFN最小的,且未被切掉的 
bel记录一个点归属于哪个scc 
*/ 
stack<int> stk;
vector<vector<int>>scc;

void dfs(int u){
	dfn[u]=low[u]=++idx;
	ins[u]=true;//u这个点有没有被切出去,即确定在一个强连通分量内,ins[u]=true,表示还未被切掉 
	stk.push(u);
	for(auto v:e[u]){
		if(!dfn[v]){
			dfs(v);
			low[u]=min(low[u],low[v]); 
		}else {
			if(ins[v])low[u]=min(low[u],dfn[v]);
		}
	} 
	if(dfn[u]==low[u]){//找到了一个强连通分量 
		cnt++;  
		while(true){
			int v=stk.top();
			ins[v]=false;
			bel[v]=cnt;
			sz[cnt]++;
			stk.pop();
			if(u==v) break;
		}
	}
}

void slove(){
	int n,m;cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v;cin>>u>>v;
		e[u].push_back(v);
	}
	
	for(int i=1;i<=n;i++)
	if(!dfn[i]) dfs(i);
	
	for(int u=1;u<=n;u++)
	 for(auto v:e[u]){
	 	if(bel[u]!=bel[v]) outd[bel[u]]++;//记录每个强连通分量的出度 
	 }
	
	int cnts=0,cntv=0;
//	cout<<cnt<<endl;
	for(int i=1;i<=cnt;i++){
		if(outd[i]==0){
			cnts++;
			cntv+=sz[i];
		}
	}
	if(cnts>=2){//大于等于两个汇点的话,那么没有一个牛是被所有点欢迎的
		cout<<0<<endl;
	}else cout<<cntv<<endl; 
}

signed main(){
	ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
	int T=1;
//	cin>>T;
	while(T--) slove();
}

四.一般图缩点->DAG图上DP结合

题意:求最大半连通子图的节点数K已经不同的最大半连通子图

思考:如果是一个DAG的话,并且要求这个DAG是半连通子图,那么需要满足什么条件?

可以发现只有当这个DAG是一条路径的时候,即是一条链的情况,才能满足这个DAG是半连通子图

一般图:

一般图的话,先用scc缩点,得到一个DAG图,转化过后就是需要这些强连通分量形成一条链

每个强连通分量都是带权的,权值就是该强连通分量内部点的数量,要求最大数量的点的子图,即为求长路

版本1:先缩点,再DP

重要:DAG图,DFS一遍后(按照tarjan算法的出栈顺序),出栈顺序就是反图的拓扑序,所以Tarjan的scc编号是反序拓扑序

所以在DP的时候,不需要做额外的排序,只需要根据编号从前往后做即可

#include<bits/stdc++.h>

#define x first
#define y second
#define endl '\n'
#define int long long
 
using namespace std;

const int N=1e6+10; 

typedef pair<int,int> PII;

vector<int> e[N]; 
int idx,mod,n,m,cnt;
int dfn[N],low[N],ins[N],bel[N];
vector<int> vec[N];//存储每个强连通分量的点 
stack<int> stk;

void dfs(int u){
	dfn[u]=low[u]=++idx;
	ins[u]=true;//u这个点有没有被切出去,即确定在一个强连通分量内,ins[u]=true,表示还未被切掉 
	stk.push(u);
	for(auto v:e[u]){
		if(!dfn[v]){
			dfs(v);
			low[u]=min(low[u],low[v]); 
		}else {
			if(ins[v])low[u]=min(low[u],dfn[v]);
		}
	} 
	if(dfn[u]==low[u]){//找到了一个强连通分量 
		cnt++;  
		while(true){
			int v=stk.top();
			vec[cnt].push_back(v);
			ins[v]=false;
			bel[v]=cnt;
			stk.pop();
			if(u==v) break;
		}
	}
}

int dp[N],way[N];
bool vis[N];

void slove(){
	int n,m;cin>>n>>m>>mod;
	for(int i=1;i<=m;i++){
		int u,v;cin>>u>>v;
		e[u].push_back(v);
	}
	
	for(int i=1;i<=n;i++)
	if(!dfn[i]) dfs(i);
	
	int w=0,ans=0;
	for(int i=1;i<=cnt;i++){
		way[i]=1;
		dp[i]=0;
		for(int u:vec[i]){
			for(int v:e[u]){
				if(!vis[bel[v]]&&bel[v]!=i){//两个点不能在一个强连通分量内,并且一个强连通分量只能被考虑一次 
					vis[bel[v]]=true;
					if(dp[bel[v]]>dp[i]) dp[i]=dp[bel[v]],way[i]=0;
					if(dp[bel[v]]==dp[i]) way[i]=(way[i]+way[bel[v]])%mod;
				}
			}
		}
		dp[i]+=vec[i].size();
		if(dp[i]>ans)ans=dp[i],w=0;
		if(dp[i]==ans)w=(w+way[i])%mod;
		for(auto u:vec[i]) for(auto v:e[u]) vis[bel[v]]=false;
	}
	cout<<ans<<" "<<w<<endl;
}

signed main(){
	ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
	int T=1;
//	cin>>T;
	while(T--) slove();
}

版本2:边缩点边DP(代码量更短)

#include<bits/stdc++.h>

#define x first
#define y second
#define endl '\n'
#define int long long
 
using namespace std;

const int N=1e6+10; 

typedef pair<int,int> PII;

vector<int> e[N]; 
int idx,mod,n,m,cnt;
int dfn[N],low[N],ins[N],bel[N];
stack<int> stk;
int dp[N],way[N],vis[N],T,ans,w;

void dfs(int u){
	dfn[u]=low[u]=++idx;
	ins[u]=true;//u这个点有没有被切出去,即确定在一个强连通分量内,ins[u]=true,表示还未被切掉 
	stk.push(u);
	for(auto v:e[u]){
		if(!dfn[v]){
			dfs(v);
			low[u]=min(low[u],low[v]); 
		}else {
			if(ins[v])low[u]=min(low[u],dfn[v]);
		}
	} 
	if(dfn[u]==low[u]){//找到了一个强连通分量 
		cnt++;  
		int sz=0;
		dp[cnt]=0;
		way[cnt]=1;
		++T;
		vis[cnt]=T;
		while(true){
			int v=stk.top();
			ins[v]=false;
			bel[v]=cnt;
			sz++;
			for(int w:e[v]){
				if(vis[bel[w]]!=T&&bel[w]!=0){//两个点不能在一个强连通分量内,并且一个强连通分量只能被考虑一次,并且不能与还没被赋值scc的编号的点进行更新
					vis[bel[w]]=T;//这样就可以保证每次都不需要清空vis数组,从而简化代码 
					if(dp[bel[w]]>dp[cnt]) dp[cnt]=dp[bel[w]],way[cnt]=0;
					if(dp[bel[w]]==dp[cnt]) way[cnt]=(way[cnt]+way[bel[w]])%mod;
				}
			}
			stk.pop();
			if(u==v) break;
		}
		dp[cnt]+=sz;
		if(dp[cnt]>ans)ans=dp[cnt],w=0;
		if(dp[cnt]==ans)w=(w+way[cnt])%mod;
	}
}

void slove(){
	int n,m;cin>>n>>m>>mod;
	for(int i=1;i<=m;i++){
		int u,v;cin>>u>>v;
		e[u].push_back(v);
	}
	
	for(int i=1;i<=n;i++)
	if(!dfn[i]) dfs(i);
	
	cout<<ans<<endl<<w<<endl;
}

signed main(){
	ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
	int T=1;
//	cin>>T;
	while(T--) slove();
}
posted @   MENDAXZ  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
点击右上角即可分享
微信分享提示