图论--强连通分量(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();
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架