tarjan求有向图强连通分量(scc)
【一. 常见概念理解】
- 强连通分量:任意两个节点相互可达的强连通图中最大的一个。
- tarjan:一个类似dfs的过程,用于寻找有向图中的强连通分量。
- 缩点:对于具有传导性的关系,把有向图中的"环"缩成点,形成有向无环图。
求强连通分量:先把有向图看成一棵有向树,进行dfs(tarjan)。
当有节点的连边连向dfn比它小的地方时,就形成了环,需要判断连通分量了。
【二. 常见数组及其使用】
(1)、dfn[i],表示时间戳,即这个点在dfs时是第几个被搜到的。
(2)、low[i],表示[这个点以及其子孙节点]连的所有点中[dfn最小的值],
即:能匹配到的最上层的祖先的位置。
(3)、stack[i],表示当前过程中可能构成强连通分量的所有点。
(4)、vis[i],表示一个点是否在stack[i]数组中。
【三. tarjan算法的实现过程】
从点u开始遍历(某个dfn为0的节点):
(1)首先初始化:dfn[u]=low[u]=第几个被dfs到。
(2)将u存入stack[]中,并将vis[u]设为true。
--- stack[ ]的意义是:如果u在stack中,在u被回溯时、
u和栈中所有在它之后的点、都可以构成强连通分量。
(3)遍历u的所有连向点v,如果dfn[v]为0,即未访问过,就对点v进行dfs。
在遍历子节点的连向边的过程中,如果遍历到dfn比它小的地方,
判断该连向节点是否在stack[ ]里,如果在,更新 low[v]。
low[u]=min{low[u],low[v]};
--- low[ u ]的意义是:记录该点能连通到的最大的祖先节点(或者自己),
即能连通到的dfn最小的值,且该值在栈内(保证是u的祖先)。
(4)如果dfn[u]=low[u],说明 [ u点及u点之下的所有子节点 ] 没有边会指向u的祖先,
那么u就是强连通分量的顶端,记录此次搜索到的强连通分量。
把所有的u点及以后压入栈中的所有点弹出,vis[ ]改为false(或打上相同标记)。
【四. 常见例题分析】
【例题1】【p2341】受欢迎的牛
- 告诉你有n头牛,m个崇拜关系,并且崇拜具有传递性,
- 如果a崇拜b,b崇拜c,则a崇拜c,求最后有几头牛被所有牛崇拜。
【标签】有向图缩点 + 连通性判断 + 入度出度的判断
将整张图(上的强连通分量)进行缩点,剩下的点都不强连通,整张图变成了有向无环图。
有向无环图上必然有>=1个的点出度为0,如果>1个,那么图不相连、答案为0。
如果只有一个点出度为0,判断它是不是强连通分量缩成的点,输出包含的点数。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;
/*【p2341】受欢迎的牛
告诉你有n头牛,m个崇拜关系,并且崇拜具有传递性,
如果a崇拜b,b崇拜c,则a崇拜c,求最后有几头牛被所有牛崇拜。*/
//将整张图(上的强连通分量)进行缩点,剩下的点都不强连通,整张图变成了有向无环图。
//有向无环图上必然有>=1个的点出度为0,如果>1个,那么图不相连、答案为0。
//如果只有一个点出度为0,判断它是不是强连通分量缩成的点,输出包含的点数。
const int N=500019;
ll n,m,x,y,tot=0,head[N*2],tmp=0,ans=0;
ll dfss=0,num=0,sum=0,col[N],times[N],du[N];
ll dfn[N],low[N],stack[N],vis[N]; //tarjan算法中的四个数组
struct node{ ll ver,nextt; }e[N*2];
void add(ll u,ll v){ e[++tot].ver=v,e[tot].nextt=head[u],head[u]=tot; }
void tarjan(ll u){ //dfss记录当前dfs序到达的数字
dfn[u]=low[u]=++dfss,vis[u]=1,stack[++num]=u; //步骤一:初始化
for(int i=head[u];i;i=e[i].nextt){ //步骤二:枚举连向点,递归更新
if(!dfn[e[i].ver]) tarjan(e[i].ver),low[u]=min(low[u],low[e[i].ver]);
else if(vis[e[i].ver]) low[u]=min(low[u],dfn[e[i].ver]); //这里写dfn或low都可以
} //↑↑步骤三:已经到达过,判断是否在当前栈内(栈内都是当前情况下能相连的点)
if(dfn[u]==low[u]){
col[u]=++sum; vis[u]=0;
while(stack[num]!=u){ //u上方的节点是可以保留的
col[stack[num]]=sum;
vis[stack[num]]=0,num--;
} num--; //col数组记录每个点所在连通块的编号
}
}
int main(){
while(scanf("%lld%lld",&n,&m)!=EOF){
memset(vis,false,sizeof(vis));
memset(dfn,0,sizeof(dfn));
dfss=0,num=0,tot=0,sum=0,ans=0,tmp=0;
memset(head,0,sizeof(head));
for(ll i=1;i<=m;i++){
scanf("%lld%lld",&x,&y),add(x,y);
} for(ll i=1;i<=n;i++)
if(!dfn[i]) tarjan(i);
for(ll u=1;u<=n;u++){
for(ll i=head[u];i;i=e[i].nextt)
if(col[e[i].ver]!=col[u]) du[col[u]]++;
times[col[u]]++; //记录强连通分量大小
} for(ll i=1;i<=sum;i++) //对每种颜色(缩点得到的点)
if(du[i]==0) tmp++,ans=times[i];
if(tmp==0||tmp>1) cout<<0<<endl;
else cout<<ans<<endl;
}
}
【例题2】【p2194】HXY烧情侣
- n个点,m条有向边,每个点都有点权w[i]。
- 如果能从一个点出发并返回这个点,只需要出发点的价值。
- 希望遍历所有的点,询问最少价值和最少价值的方案数。
【标签】有向图缩点 + 乘法原理求方案数
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;
/*【p2194】HXY烧情侣
n个点,m条有向边,每个点都有点权w[i]。
如果能从一个点出发并返回这个点,只需要出发点的价值。
遍历所有的点,询问最少价值和最少价值的方案数。*/
//寻找强连通分量 + 乘法原理求方案数
const int N=500019,mod=1000000007;
ll n,m,ans1,ans2=1,tot=0,head[N*2],w[N];
ll dfn[N],low[N],stack[N],vis[N],dfss=0,num=0;
//↑↑tarjan算法中的 四个数组 和 两个变量
struct node{ ll ver,nextt; }e[N*2];
void add(ll u,ll v){ e[++tot].ver=v,e[tot].nextt=head[u],head[u]=tot; }
void tarjan(ll u){ //dfss记录当前dfs序到达的数字
dfn[u]=low[u]=++dfss,vis[u]=1,stack[++num]=u; //步骤一:初始化
for(int i=head[u];i;i=e[i].nextt){ //步骤二:枚举连向点,递归更新
if(!dfn[e[i].ver]) tarjan(e[i].ver),low[u]=min(low[u],low[e[i].ver]);
else if(vis[e[i].ver]) low[u]=min(low[u],dfn[e[i].ver]); //这里写dfn或low都可以
} //↑↑步骤三:已经到达过,判断是否在当前栈内(栈内都是当前情况下能相连的点)
if(dfn[u]==low[u]){ ll minn=(ll)1e15,cnt=0;
do{ vis[stack[num]]=0; //以后这里的判断全部用do while...
int now=w[stack[num]]; num--; //此点的价值
if(now==minn) cnt++; //此连通分量的最小价值的方案数
if(now<minn) minn=now,cnt=1; //有更小的,cnt还原为1
}while(stack[num+1]!=u); //更新答案一定要包含节点u啊...
ans1+=minn; ans2=(ans2*cnt)%mod; //用乘法原理求方案数
//↑↑记录此连通分量(包含只有一个节点的情况)的最小权值 和 方案数
}
}
int main(/*hs_love_wjy*/){
ll x,y; cin>>n; for(ll i=1;i<=n;i++) cin>>w[i];
cin>>m; for(ll i=1;i<=m;i++) cin>>x>>y,add(x,y);
for(ll i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
cout<<ans1<<" "<<ans2<<endl; //最小代价和方案数
}
【例题3】【p2416】泡芙
- 输入位置st、ed,询问路径上是否经过权值为1的边。
【标签】无向图缩点 + 无向无环图性质(就是树) + lca
先放一份自己的代码...觉得并没有什么问题但是TLE了...
果然还是我太水了不过这种题了解写法了解思维就够了吧qwq
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;
/*【p2416】泡芙
输入位置st、ed,输出路径上是否经过权值为1的边。*/
//无向图缩点 + 无向无环图性质(就是树) + lca
void reads(int &x){ //读入优化(正负整数)
int fa=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')fa=-1;s=getchar();}
while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
x*=fa; //正负号
}
const int N=800019;
int n,m,q,tot=0,head[N*2],tot1=0,head1[N*2];
int dep[N],dist[N],fa[N][20],col[N],sizes[N],circle[N];
int dfn[N],low[N],stack_[N],dfss=0,num=0,sum=0;
//↑↑tarjan算法中的 四个数组 和 三个变量
struct node{ int ver,nextt,w; }e[N*2],e1[N*2];
void add(int u,int v,int w){ e[++tot].ver=v,e[tot].nextt=head[u],e[tot].w=w,head[u]=tot; }
void add1(int u,int v,int w){ e1[++tot1].ver=v,e1[tot1].nextt=head1[u],e1[tot1].w=w,head1[u]=tot1; }
void tarjan(int u,int u_fa){ //dfss记录当前dfs序到达的数字
dfn[u]=low[u]=++dfss,stack_[++num]=u; //步骤一:初始化
for(int i=head[u];i;i=e[i].nextt){ //步骤二:枚举连向点,递归更新
if(e[i].ver==u_fa) continue; //【无向图缩点】
if(!dfn[e[i].ver]) tarjan(e[i].ver,u),low[u]=min(low[u],low[e[i].ver]);
else if(!col[e[i].ver]) low[u]=min(low[u],dfn[e[i].ver]); //这里写dfn或low都可以
} //↑↑步骤三:已经到达过,判断是否在当前栈内(栈内都是当前情况下能相连的点)
if(dfn[u]==low[u]){
col[u]=++sum,sizes[sum]=1;
while(stack_[num]!=u){
col[stack_[num]]=sum;
sizes[sum]++,num--;
} num--; //记录所有的连通分量和它们的大小
}
}
void sum_circle(){ //预处理每个环的边权sum,并重新建图
for(int x=1;x<=n;x++){
for(int i=head[x];i;i=e[i].nextt){
if(col[x]!=col[e[i].ver])
add1(col[x],col[e[i].ver],e[i].w),add1(col[e[i].ver],col[x],e[i].w);
else circle[col[x]]+=e[i].w;
} //↑↑建立新图,每个连通分量的边权记录在编号上
}
}
void pre_dfs(int u,int u_fa){ //缩点之后变成了无向无环图,也就是树
for(int i=0;i<20;i++) fa[u][i+1]=fa[fa[u][i]][i];
for(int i=head1[u];i;i=e1[i].nextt){
int v=e1[i].ver; //找到下一条相连的边
if(v==u_fa) continue;
dep[v]=dep[u]+1; //深度
dist[v]=dist[u]+e1[i].w+circle[v]; //距离
fa[v][0]=u; pre_dfs(v,u); //记录father,递归
}
}
int lca_(int x,int y){ //找lca的主程序
if(dep[x]<dep[y]) swap(x,y); //保证dep[x]>dep[y]
for(int i=19;i>=0;i--){ //注意:这里的20和上面的19都是log2n的近似取值
if(dep[fa[x][i]]>=dep[y]) x=fa[x][i];
//↑↑↑i的2^k辈祖先的结点仍比y深,令x=fa[x,i],继续向上跳
if(x==y) return x; //若x=y,则已经找到了lca
}
for(int i=19;i>=0;i--) //↓↓↓未找到lca时,一起倍增
if(fa[x][i]!=fa[y][i]){ x=fa[x][i]; y=fa[y][i]; }
return fa[x][0]; //最终的答案为fa[x][0]
}
int main(/*hs_love_wjy*/){
int x,y,z,ans; cin>>n>>m;
for(int i=1;i<=m;i++){
reads(x),reads(y),reads(z);
add(x,y,z); add(y,x,z); //无向图啊...
} for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0);
sum_circle(); //预处理每个环的边权sum,环上任意点是相互可达的
dist[col[1]]=circle[col[1]]; pre_dfs(col[1],0);
cin>>q; for(int i=1;i<=q;i++){
reads(x),reads(y); x=col[x],y=col[y];
int lca=lca_(x,y); ans=dist[x]+dist[y]-2*dist[lca]+circle[lca];
if(ans>=1) cout<<"YES"<<endl; else cout<<"NO"<<endl;
}
}
然后是AC代码...思路一样的(受教qwq)...
#include <iostream>
#include <cstdio>
using namespace std;
const int wx=800017;
inline int read(){
int sum=0,f=1; char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1; ch=getchar();}
while(ch>='0'&&ch<='9'){sum=(sum<<1)+(sum<<3)+ch-'0'; ch=getchar();}
return sum*f;
}
int a[wx],belong[wx],dfn[wx],low[wx],size[wx];
int head[wx],h[wx],st[wx],dep[wx],dis[wx];
int f[wx][23];
int n,m,q;
int top,tot,num,Num,col;
struct e{
int nxt,to,dis;
}edge[wx*2];
struct ee{
int nxt,to,dis;
}e[wx*2];
void Add(int from,int to,int dis){
e[++Num].nxt=h[from];
e[Num].to=to;
e[Num].dis=dis;
h[from]=Num;
}
void add(int from,int to,int dis){
edge[++num].nxt=head[from];
edge[num].to=to;
edge[num].dis=dis;
head[from]=num;
}
void Tarjan(int u,int fa){
dfn[u]=low[u]=++tot;
st[++top]=u;
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa)continue;
if(!dfn[v]) Tarjan(v,u), low[u]=min(low[u],low[v]);
else if(!belong[v]) low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u]){
belong[u]=++col;
size[col]=1;
while(st[top]!=u){
belong[st[top]]=col;
size[col]++;
top--;
}
top--;
}
}
void CQ(){
for(int u=1;u<=n;u++){
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].to;
if(belong[u]!=belong[v])
add(belong[u],belong[v],e[i].dis);
else a[belong[u]]+=e[i].dis;
}
}
}
void dfs(int u,int fa){
dep[u]=dep[fa]+1;
for(int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v==fa)continue;
f[v][0]=u; dis[v]=dis[u]+edge[i].dis+a[v];
dfs(v,u);
}
}
void pre(){
for(int j=1;j<=20;j++)
for(int i=1;i<=n;i++)
f[i][j]=f[f[i][j-1]][j-1];
}
int LCA(int x,int y){
if(dep[x]<dep[y])swap(x,y);
for(int i=20;i>=0;i--)
if(dep[f[x][i]]>=dep[y])x=f[x][i];
if(x==y)return x;
for(int i=20;i>=0;i--)
if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
return f[x][0];
}
int main(){
n=read(); m=read();
for(int i=1;i<=m;i++){
int x,y,z;
x=read(); y=read(); z=read();
Add(x,y,z); Add(y,x,z);
}
for(int i=1;i<=n;i++)if(!dfn[i])Tarjan(i,0);
CQ();
dis[belong[1]]=a[belong[1]];
dfs(belong[1],0); pre();
q=read();
for(int i=1;i<=q;i++){
int x,y;
x=read(); y=read();
x=belong[x]; y=belong[y];
int lca=LCA(x,y);
if(dis[x]+dis[y]-2*dis[lca]+a[lca]>0)puts("YES");
else puts("NO");
}
return 0;
}
【例题4】【p2002】消息扩散
- 有向图。n个城市,m条单行道路。道路可能重复或者自环。
- 至少需要在几个城市发布消息才能让这所有n个城市都得到消息。
【标签】思维 + 有向图强连通分量 + 特判自环情况
题意可以转化为:统计入度为0的强连通分量的个数。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;
/*【p2002】消息扩散
有向图。n个城市,m条单行道路。道路可能重复或者自环。
至少需要在几个城市发布消息才能让这所有n个城市都得到消息。*/
//统计入度为0的强连通分量的个数
void reads(int &x){ //读入优化(正负整数)
int fa=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')fa=-1;s=getchar();}
while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
x*=fa; //正负号
}
const int N=800019;
int n,m,q,tot=0,head[N*2],col[N],rd[N];
int dfn[N],low[N],vis[N],stack_[N],dfss=0,num=0,sum=0;
//↑↑tarjan算法中的 四个数组 和 三个变量
struct node{ int ver,nextt; }e[N*2];
void add(int u,int v){ e[++tot].ver=v,e[tot].nextt=head[u],head[u]=tot; }
void tarjan(int u){ //dfss记录当前dfs序到达的数字
dfn[u]=low[u]=++dfss,vis[u]=true,stack_[++num]=u; //步骤一:初始化
for(int i=head[u];i;i=e[i].nextt){ //步骤二:枚举连向点,递归更新
if(!dfn[e[i].ver]) tarjan(e[i].ver),low[u]=min(low[u],low[e[i].ver]);
else if(vis[e[i].ver]) low[u]=min(low[u],dfn[e[i].ver]); //这里写dfn或low都可以
} //↑↑步骤三:已经到达过,判断是否在当前栈内(栈内都是当前情况下能相连的点)
if(dfn[u]==low[u]){
col[u]=++sum; vis[u]=0;
while(stack_[num]!=u){
col[stack_[num]]=sum;
vis[stack_[num]]=0,num--;
} num--; //记录所有的连通分量
}
}
int main(/*hs_love_wjy*/){
int x,y,ans=0; cin>>n>>m;
for(int i=1;i<=m;i++){ //↓↓特判自环的情况
reads(x),reads(y); if(x!=y) add(x,y);
} for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
for(int x=1;x<=n;x++)
for(int i=head[x];i;i=e[i].nextt)
if(col[x]!=col[e[i].ver]) rd[col[e[i].ver]]++;
for(int i=1;i<=sum;i++) if(!rd[i]) ans++;
cout<<ans<<endl; return 0;
}
【例题5】【p2863】牛的舞会
- 给你n个点,m条边,求图中所有大小大于1的强连通分量的个数。
【标签】强连通分量(模板题) + 计数
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;
//【p2863】给你n个点,m条边,求图中所有大小大于1的强连通分量的个数
void reads(ll &x){
int fx=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+(s-'0');s=getchar();}
x=x*fx; //正负号
}
const int N=500019;
ll n,m,tot=0,head[N*2],tmp[N],ans=0,dfss=0,num=0; //tmp和ans都用于记录答案
ll dfn[N],low[N],stack[N],vis[N]; //tarjan算法中的四个数组
struct node{ ll ver,nextt; }e[N*2];
void add(ll u,ll v){ e[++tot].ver=v,e[tot].nextt=head[u],head[u]=tot; }
void tarjan(ll u){ //dfss记录当前dfs序到达的数字
dfn[u]=low[u]=++dfss,vis[u]=1,stack[++num]=u; //步骤一:初始化
for(int i=head[u];i;i=e[i].nextt){ //步骤二:枚举连向点,递归更新
if(!dfn[e[i].ver]) tarjan(e[i].ver),low[u]=min(low[u],low[e[i].ver]);
else if(vis[e[i].ver]) low[u]=min(low[u],dfn[e[i].ver]);
} //↑↑步骤三:已经到达过,判断是否在当前栈内(栈内都是当前情况下能相连的点)
if(dfn[u]==low[u]){
ll cnt=0; //此次构成的连通分量的大小
while(stack[num]!=u){ //u上方的节点是可以保留的
cnt++,vis[stack[num]]=0,num--;
} cnt++,vis[u]=0,num--;
if(cnt>1) ans++;
}
}
int main(){
ll x,y; reads(n),reads(m);
for(ll i=1;i<=m;i++){
reads(x),reads(y),add(x,y);
} for(ll i=1;i<=n;i++) //注意,dfn为0时,
if(!dfn[i]) tarjan(i); //才需要重新调用tarjan函数
printf("%lld\n",ans); //强连通分量的个数
}
【例题6】【p1262】间谍网络
- 每个人掌握一些情报,对于情报中提及的每个人,能获得他有的情报。
- 有一些人可以被收买。希望获得所有人的情报,求min代价。
【标签】思维 + 分情况讨论 + 有向图缩点
【分析】因为关系有传递性-->有向图-->1.不连通,无解;
2.连通,无环-->代价为rd=0的点的权值,有环-->代价为缩点后rd=0的点的权值。
可以直接把三种情况都归到有环的情况中,先缩点,再计算入度、得到ans。
P.S.本蒟蒻一开始想要分情况讨论,写了一个have数组表示是否被传递到了。
但是我没有dfs,直接用连向边瞎搞的...结果特判失败被打回原形2333...
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;
/*【p1262】间谍网络
每个人掌握一些情报,对于情报中的每个人,能获得他的情报。
希望获得所有人的情报,求min代价。*/
//【标签】思维 + 分情况讨论 + 有向图缩点
/*【分析】因为关系有传递性-->有向图-->1.不连通,无解;
2.连通,无环-->代价为rd=0的点的权值,有环-->代价为缩点后rd=0的点的权值。
可以直接把三种情况都归到有环的情况中,先缩点,再计算入度、得到ans。*/
void reads(int &x){ //读入优化(正负整数)
int fa=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')fa=-1;s=getchar();}
while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
x*=fa; //正负号
}
const int N=500019;
int n,p,m,tot=0,head[N*2],col[N],rd[N],a[N],circle[N];
int dfn[N],low[N],vis[N],stack_[N],dfss=0,num=0,sum=0;
//↑↑tarjan算法中的 四个数组 和 三个变量
struct node{ int ver,nextt; }e[N*2];
void add(int u,int v){ e[++tot].ver=v,e[tot].nextt=head[u],head[u]=tot; }
void tarjan(int u){ //dfss记录当前dfs序到达的数字
dfn[u]=low[u]=++dfss,vis[u]=true,stack_[++num]=u; //步骤一:初始化
for(int i=head[u];i;i=e[i].nextt){ //步骤二:枚举连向点,递归更新
if(!dfn[e[i].ver]) tarjan(e[i].ver),low[u]=min(low[u],low[e[i].ver]);
else if(vis[e[i].ver]) low[u]=min(low[u],dfn[e[i].ver]); //这里写dfn或low都可以
} //↑↑步骤三:已经到达过,判断是否在当前栈内(栈内都是当前情况下能相连的点)
if(dfn[u]==low[u]){
col[u]=++sum; vis[u]=0; circle[sum]=a[u];
while(stack_[num]!=u){
circle[sum]=min(circle[sum],a[stack_[num]]),
col[stack_[num]]=sum,vis[stack_[num]]=0,num--;
} num--; //记录所有的连通分量
}
}
int main(/*hs_love_wjy*/){
int x,y,ans=0; cin>>n>>p;
for(int i=1;i<=n;i++) a[i]=1e9;
for(int i=1;i<=p;i++) reads(x),reads(y),a[x]=y;
cin>>m; for(int i=1;i<=m;i++) reads(x),reads(y),add(x,y);
for(int i=1;i<=n;i++) //需要缩点的情况
if(!dfn[i]&&a[i]!=1e9) tarjan(i);
for(int i=1;i<=n;i++) if(dfn[i]==0) //无法完全连通
{ cout<<"NO"<<endl<<i<<endl; return 0;}
for(int x=1;x<=n;x++) //统计记录经过缩点之后,
for(int i=head[x];i;i=e[i].nextt) //每个强连通分量的入度
if(col[x]!=col[e[i].ver]) rd[col[e[i].ver]]++;
for(int i=1;i<=sum;i++) if(!rd[i]) ans+=circle[i];
cout<<"YES"<<endl<<ans<<endl; return 0;
}
【例题7】【p3043】牛联盟
- 给出n个点m条边的图,现把点和边分组,
- 每条边只能和相邻两点之一分在一组,问分组方案数。
【标签】结论(无向图连通块只可能是环或树) + 分类讨论 + 无向图缩点
【分析】因为是无向图,所以寻找到的连通块只可能是环或树(可以使用并查集,dfs,tarjan...)
如果是环,只有两种方案(顺时针+逆时针);如果不是环,相当于是n个点,n-1条边,有n种方案。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<set>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;
/*【p3043】牛联盟
给出n个点m条边的图,现把点和边分组,每条边只能和相邻两点之一分在一组,问分组方案数。*/
/*【分析】因为是无向图,所以寻找到的连通块只可能是环或树(可以使用并查集,dfs,tarjan...)
如果是环,只有两种方案(顺时针+逆时针);如果不是环,相当于是n个点,n-1条边,有n种方案。*/
void reads(ll &x){ //读入优化(正负整数)
int fa=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')fa=-1;s=getchar();}
while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
x*=fa; //正负号
}
const ll N=500019,mod=1000000007;
ll n,p,m,tot=0,head[N*2],col[N],x[N],y[N],circle[N],circle2[N];
ll dfn[N],low[N],vis[N],stack_[N],dfss=0,num=0,sum=0;
//↑↑tarjan算法中的 四个数组 和 三个变量
struct node{ ll ver,nextt; }e[N*2];
void add(ll u,ll v){ e[++tot].ver=v,e[tot].nextt=head[u],head[u]=tot; }
void tarjan(ll u){ //dfss记录当前dfs序到达的数字
dfn[u]=low[u]=++dfss,vis[u]=true,stack_[++num]=u; //步骤一:初始化
for(ll i=head[u];i;i=e[i].nextt){ //步骤二:枚举连向点,递归更新
if(!dfn[e[i].ver]) tarjan(e[i].ver),low[u]=min(low[u],low[e[i].ver]);
else if(vis[e[i].ver]) low[u]=min(low[u],dfn[e[i].ver]); //这里写dfn或low都可以
} //↑↑步骤三:已经到达过,判断是否在当前栈内(栈内都是当前情况下能相连的点)
if(dfn[u]==low[u]){
col[u]=++sum; vis[u]=0; circle[sum]=1;
while(stack_[num]!=u){
circle[sum]++,col[stack_[num]]=sum,
vis[stack_[num]]=0,num--;
} num--; //记录所有的连通分量 和 大小
}
}
int main(/*hs_love_wjy*/){
ll ans=1; cin>>n>>m;
for(ll i=1;i<=m;i++) reads(x[i]),reads(y[i]),
add(x[i],y[i]),add(y[i],x[i]);
for(ll i=1;i<=n;i++)
if(!dfn[i]) tarjan(i);
for(ll i=1;i<=m;i++) //求无向图的连通块是树还是环
if(col[x[i]]==col[y[i]]) circle2[col[x[i]]]++;
for(ll i=1;i<=sum;i++){
if(circle[i]==circle2[i]) circle[i]=2; //如果是环,只有两种方案
ans=(ans*(circle[i]%mod))%mod; //如果不是环,相当于是n个点,n-1条边,有n种方案
} printf("%lld\n",ans); return 0;
}
——时间划过风的轨迹,那个少年,还在等你