Tarjan——图的连通性
前置:图论
只需要一眼看透这玩意是个图即可
作为一个新专题,它与前面的LCA,最短路和最小生成树没有非常大的联系
可能最大的联系就是LCA的离线算法叫LCA的Tarjan算法吧
不过由于Tarjan算法是各种联通分量的缩点手法
因此Tarjan可以和最短路算法,拓扑排序,树型DP(DAG上DP)混用
不得不说树形DP真的是噩梦啊
概念:时间戳,搜索树,追溯值
1.时间戳
在无向图/流图的DFS遍历中,按每个节点第一次被访问的时间顺序,依次给n个节点打上1~n的标记,该标记称为时间戳
2.搜索树
在无向连通图上任选一个节点(流图的源点)进行DFS遍历,每个节点仅访问一次,所有发生递归的边(即从节点x以(x,y)这条边访问到节点y是对y的第一次访问)构成的树叫无向联通图(流图)的搜索树,不同无向联通块的搜索树构成整个图的搜索森林
3.追溯值:
设subtree(x)表示搜索树中以x为根的子树
追溯值low(x)定义为以下节点的时间戳的最小值:
1.subtree(x)中的节点
2.通过一条不在搜索树上的边可到达subtree(x)的节点
概念:割,连通
1.割点,割边
对于图G的节点x,若从图中删去x及所有与x关联的边后,G分裂为2个或两个以上不相连的子图,则x为G的割点
对于图G的边e,若从图中删去e后,G分为两个不相连的子图,则称e为G的割边
2.点双,边双,强联通
若一张无向联通图不存在割点,则称它为点双连通图
若一张无向联通图不存在割边,则称它为边双连通图
无向图的极大点双联通子图被称为点双连通分量,简记为v-DCC
无向图的极大边双连通子图被称为边双连通分量,简记为e-DCC
给定一张有向图,若对于图中任意两个节点x,y,满足既存在x到达y的路径,也存在y到达x的路径,则称该图为强联通图
有向图的极大强联通子图被称为强联通分量
算法类型
求割点,割边
割点
void tarjan(int x){
d[x]=l[x]=++sum;
int f=0;
for(int i=h[x];i;i=p[i].n){
int y=p[i].t;
if(!d[y]){
tarjan(y);
l[x]=min(l[x],l[y]);
if(l[y]>=d[x]){
f++;
if(x!=r||f>1){
c[x]=1;
}
}
}
else{
l[x]=min(l[x],d[y]);
}
}
}
割边
void tarjan(int x,int r){
d[x]=l[x]=++num;
for(int i=h[x];i;i=p[i].n){
int z=p[i].t;
if(!d[z]){
tarjan(z,x);
l[x]=min(l[x],l[z]);
if(l[z]>d[x]){
b[i]=b[i^1]=1;
}
}
else if(z!=r){
l[x]=min(l[x],d[z]);
}
}
}
求联通分量
点双
void tarjan(int x,int f){
d[x]=l[x]=++dp;
v[x]=1;
s[top++]=x;
for(int i=h[x];i!=-1;i=p[i].n){
int y=p[i].t;
if(y==f){
continue;
}
if(!d[y]){
tarjan(y,x);
l[x]=min(l[x],l[y]);
if(l[y]>d[x]){
p[i].c=1;
p[i^1].c=1;
}
}
else if(v[y]){
l[x]=min(l[x],d[y]);
}
}
if(d[x]==l[x]){
sum++;
int y;
do{
y=s[--top];
v[y]=0;
c[y]=sum;
}while(y!=x);
}
}
边双
void tarjan(int u,int fa){
dfn[u]=low[u]=++inr;
stack[++top]=u;
vis[u]=1;
int x=0;
for(int i=head[u];i!=-1;i=e[i].next){
int v=e[i].to;
if(v==fa){
continue;
}
if(!dfn[v]){
++x;
tarjan(v,u);
low[u]=min(low[u],low[v]);
if(dfn[u]<=low[v]){
cut[u]=1;
}
}
else if(vis[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(fa==-1&&x<=1){
cut[u]=0;
}
}
强联通
void tarjan(int x){
dfn[x]=++dex;
low[x]=dex;
vis[x]=1;
stack[++top]=x;
for(int i=st[x];i!=0;i=e[i].n){
int y=e[i].t;
if(!dfn[y]){
tarjan(y);
low[x]=min(low[x],low[y]);
}
else if(vis[y]){
low[x]=min(low[x],dfn[y]);
}
}
if(dfn[x]==low[x]){
vis[x]=0;
color[x]=++num;
while(stack[top]!=x){
color[stack[top]]=num;
vis[stack[top--]]=0;
}
top--;
}
}
缩点
就是重新建图的事,新建邻接表跑前向星即可
代码略了,前向星粘过来没啥意思
例题
备用交换机
一眼看到底的割点裸题
Code
备用交换机
#include <bits/stdc++.h>
using namespace std;
const int o=5e5+10;
int h[o],d[o],l[o],s[o];
struct path{
int t;
int n;
}p[o];
int cnt,n,m,r,sum;
bool c[o];
int read(){
int i=1,j=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-'){
i=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
j=(j<<1)+(j<<3)+ch-'0';
ch=getchar();
}
return i*j;
}
void add(int s,int t){
cnt++;
p[cnt].t=t;
p[cnt].n=h[s];
h[s]=cnt;
}
void tarjan(int x){
d[x]=l[x]=++sum;
int f=0;
for(int i=h[x];i;i=p[i].n){
int y=p[i].t;
if(!d[y]){
tarjan(y);
l[x]=min(l[x],l[y]);
if(l[y]>=d[x]){
f++;
if(x!=r||f>1){
c[x]=1;
}
}
}
else{
l[x]=min(l[x],d[y]);
}
}
}
void in(){
n=read();
int x,y;
while(scanf("%d%d",&x,&y)!=EOF){
if(x==y){
continue;
}
add(x,y);
add(y,x);
}
}
void work(){
for(int i=1;i<=n;i++){
if(!d[i]){
r=i;
tarjan(r);
}
}
}
void out(){
for(int i=1;i<=n;i++){
if(c[i]){
m++;
}
}
cout<<m<<endl;
for(int i=1;i<=n;i++){
if(c[i]){
cout<<i<<endl;
}
}
}
int main(){
in();
work();
out();
return 0;
}
旅游航道
一眼割边+计数,注意计数的位置
Code
旅游航道
#include <bits/stdc++.h>
using namespace std;
const int o=1e5+10;
int h[o],d[o],l[o];
int n,m,cnt,num,ans;
struct node {
int t;
int n;
}p[o];
int read(){
int i=1,j=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-'){
i=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
j=(j<<1)+(j<<3)+(ch&15);
ch=getchar();
}
return i*j;
}
void add(int s,int t){
cnt++;
p[cnt].t=t;
p[cnt].n=h[s];
h[s]=cnt;
}
void tarjan(int x,int r){
d[x]=l[x]=++num;
for(int i=h[x];i;i=p[i].n){
int z=p[i].t;
if(!d[z]){
tarjan(z,x);
l[x]=min(l[x],l[z]);
if(l[z]>d[x]){
ans++;
}
}
else if(z!=r){
l[x]=min(l[x],d[z]);
}
}
}
void in(){
int x,y;
for(int i=1;i<=m;i++){
x=read();
y=read();
add(x,y);
add(y,x);
}
num=0;
}
void work(){
tarjan(1,1);
}
void out(){
cout<<ans<<endl;
}
void back(){
memset(p,0,sizeof(p));
memset(h,0,sizeof(h));
memset(d,0,sizeof(d));
memset(l,0,sizeof(l));
n=m=cnt=num=ans=0;
}
int main(){
while(scanf("%d%d",&n,&m)!=EOF){
if(!n&&!m){
return 0;
}
in();
work();
out();
back();
}
return 0;
}
BLO
由于是删点,要考虑删点对于图的影响
题面中指示不联通的点对,所以要考虑删点对于连通性的影响
而连通性和点结合起来自然是一个割点问题
因此考虑是不是割点对于答案的影响
1.不是割点,删完边以后只剩下单点不与整个图上其它点联通
直接加法原理(n-1),考虑有序每个点乘2,加法原理就直接解出2*(n-1)
2.是割点
此时图被划分为若干联通块,并且每个联通块彼此不连通,个数设为k
运用加法原理和乘法原理即可
加法原理k选2
乘法原理从2个里面任意挑点即可
Code
BLO
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int o=5e5+10;
struct node{
int t;
int n;
}p[o*2];
int h[o],d[o],l[o],s[o];
ll ans[o];
bool c[o];
int cnt,n,m,num;
int read(){
int i=1,j=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-'){
i=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
j=(j<<1)+(j<<3)+ch-'0';
ch=getchar();
}
return i*j;
}
void add(int s,int t){
cnt++;
p[cnt].t=t;
p[cnt].n=h[s];
h[s]=cnt;
}
void tarjan(int x){
d[x]=l[x]=++num;
s[x]=1;
int f=0,sum=0;
for(int i=h[x];i;i=p[i].n){
int y=p[i].t;
if(!d[y]){
tarjan(y);
s[x]+=s[y];
l[x]=min(l[x],l[y]);
if(l[y]>=d[x]){
f++;
ans[x]+=(ll)s[y]*(n-s[y]);
sum+=s[y];
if(x!=1||f>1){
c[x]=1;
}
}
}
else{
l[x]=min(l[x],d[y]);
}
}
if(c[x]){
ans[x]+=(ll)(n-sum-1)*(sum+1)+(n-1);
}
else{
ans[x]=2*(n-1);
}
}
void in(){
n=read();
m=read();
cnt=1;
int x,y;
for(int i=1;i<=m;i++){
x=read();
y=read();
if(x==y){
continue;
}
add(x,y);
add(y,x);
}
}
void out(){
for(int i=1;i<=n;i++){
printf("%lld\n",ans[i]);
}
}
int main(){
in();
tarjan(1);
out();
return 0;
}
矿场搭建
其实这题还是一个割点
塌掉某个点实际上等价于删点,然后找出联通块讨论联通块中有几个割点即可
注意只删掉一个点
1.联通块中无割点:
如果一个安全出口正好塌了,就需要修另一个
所以这时修两个
2.联通块有一个割点:
防塌割点修一个即可
3.联通块有两个以上:
塌一个割点没事可以跑路直接溜球
一个都不用
第二问就是个计数问题:
乘法原理即可
矿场搭建
#include <cctype>
#include <cstdio>
#include <cstring>
using namespace std;
#define il inline
#define rg register
#define ll long long
const int o=1e5+10;
int m,n,top,cnt,inr,opt,id;
int dfn[o],low[o],stack[o],cut[o],belong[o],siz[o],c[o];
bool vis[o];
ll ans;
struct node{
int to;
int next;
node(){}
node (int to,int next):to(to),next(next){};
}e[o];
int head[o],tot;
il void read(int &x){
int f=1;
rg char ch=getchar();
for(x=0;!isdigit(ch);ch=='-'&&(f=-1),ch=getchar());
for(;isdigit(ch);x=x*10+ch-48,ch=getchar());
x=x*f;
}
il void clear(){
tot=0;
top=0;
inr=0;
id=0;
cnt=0;
n=0;
ans=1;
memset(c,0,sizeof(c));
memset(dfn,0,sizeof(dfn));
memset(vis,0,sizeof(vis));
memset(low,0,sizeof(low));
memset(cut,0,sizeof(cut));
memset(siz,0,sizeof(siz));
memset(head,-1,sizeof(head));
memset(belong,0,sizeof(belong));
}
int min(int a,int b){
return a<b?a:b;
}
void add(int x,int y){
e[++tot]=node(y,head[x]);
head[x]=tot;
e[++tot]=node(x,head[y]);
head[y]=tot;
}
void tarjan(int u,int fa){
dfn[u]=low[u]=++inr;
stack[++top]=u;
vis[u]=1;
int x=0;
for(int i=head[u];i!=-1;i=e[i].next){
int v=e[i].to;
if(v==fa){
continue;
}
if(!dfn[v]){
++x;
tarjan(v,u);
low[u]=min(low[u],low[v]);
if(dfn[u]<=low[v]){
cut[u]=1;
}
}
else if(vis[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(fa==-1&&x<=1){
cut[u]=0;
}
}
void dfs(int u){
vis[u]=1;
++siz[id];
for(int i=head[u];i!=-1;i=e[i].next){
int v=e[i].to;
if(vis[v]){
continue;
}
if(!cut[v]){
dfs(v);
}
else if(belong[v]!=id){
belong[v]=id;
++c[id];
}
}
}
int main(){
while(1){
read(m);
if(!m){
break;
}
clear();
for(int x,y;m--;){
read(x);
read(y);
add(x,y);
if(x>n){
n=x;
}
if(y>n){
n=y;
}
}
for(int i=1;i<=n;++i){
if(!dfn[i]){
tarjan(i,-1);
}
}
memset(vis,0,sizeof(vis));
for(int i=1;i<=n;++i){
if(!cut[i]&&!vis[i]){
++id;
dfs(i);
}
}
if(id==1){
cnt=2;
ans=(ll)n*(n-1)/2;
}
else{
for(int i=1;i<=id;++i){
if(c[i]==1){
++cnt;
ans=(ll)ans*siz[i];
}
}
}
printf("Case %d: %d %lld\n",++opt,cnt,ans);
}
return 0;
}
受欢迎的牛
如果说受欢迎可传递的话,那一个强连通分量中的所有节点都受到指向这之中任何一个节点的边的起点的欢迎。
于是考虑缩点,缩点完成重新建边
显然如果出现“南北对峙”或者“三足鼎立”或者“五胡十六国”等局面是不可能出现公认受欢迎的牛的
因此只有出度为0的点只有一个的情况下才有可能出现
此时由于已经缩点,对应的个数就是强连通分量的节点个数
Code
受欢迎的牛
#include <bits/stdc++.h>
using namespace std;
const int o=1e6;
int d[o],l[o],v[o],s[o],c[o],t[o],f[o];
int n,m,top,sum,dp,tmp,ans;
vector <int> g[o];
int read(){
int i=1,j=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-'){
i=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
j=(j<<1)+(j<<3)+ch-'0';
ch=getchar();
}
return i*j;
}
void tarjan(int x){
d[x]=l[x]=++dp;
v[x]=1;
s[++top]=x;
int a=g[x].size();
for(int i=0;i<a;i++){
int y=g[x][i];
if(!d[y]){
tarjan(y);
l[x]=min(l[x],l[y]);
}
else{
if(v[y]){
l[x]=min(l[x],l[y]);
}
}
}
if(d[x]==l[x]){
c[x]=++sum;
v[x]=0;
while(s[top]!=x){
c[s[top]]=sum;
v[s[top--]]=0;
}
top--;
}
}
void in(){
n=read();
m=read();
int x,y;
for(int i=1;i<=m;i++){
x=read();
y=read();
g[x].push_back(y);
}
}
void work(){
for(int i=1;i<=n;i++){
if(!d[i]){
tarjan(i);
}
}
for(int i=1;i<=n;i++){
int a=g[i].size();
for(int j=0;j<a;j++){
int b=g[i][j];
if(c[b]!=c[i]){
t[c[i]]++;
}
}
f[c[i]]++;
}
for(int i=1;i<=sum;i++){
if(t[i]==0){
tmp++;
ans=f[i];
}
}
}
void out(){
if(tmp==1){
printf("%d",ans);
}
else{
printf("0");
}
}
int main(){
in();
work();
out();
return 0;
}
分离的路径
首先如果走点双肯定是不会有重复路径的
如果说走的边一定重复那肯定是个割边
但是本题并不是个求割边的题,而是要说加边
那么对于不重要的环就直接缩掉就行了
缩完以后整张图只剩下一棵树了,所有的边就肯定是割边了
但是我们还要把割边变成环
就相当于把每个叶子节点联通就行了
(有个图示就简明易懂了)
加的边数就是(度为0的节点数+1)/2
Code
分离的路径
#include <bits/stdc++.h>
using namespace std;
const int o=1e5;
int d[o],l[o],v[o],s[o],c[o],h[o],f[o];
int n,m,top,sum,dp,ans,cnt;
struct node {
int t;
int n;
bool c;
}p[o*10];
void add(int s,int t){
p[cnt].t=t;
p[cnt].c=0;
p[cnt].n=h[s];
h[s]=cnt++;
}
int read(){
int i=1,j=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-'){
i=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
j=(j<<1)+(j<<3)+ch-'0';
ch=getchar();
}
return i*j;
}
void tarjan(int x,int f){
d[x]=l[x]=++dp;
v[x]=1;
s[top++]=x;
for(int i=h[x];i!=-1;i=p[i].n){
int y=p[i].t;
if(y==f){
continue;
}
if(!d[y]){
tarjan(y,x);
l[x]=min(l[x],l[y]);
if(l[y]>d[x]){
p[i].c=1;
p[i^1].c=1;
}
}
else if(v[y]){
l[x]=min(l[x],d[y]);
}
}
if(d[x]==l[x]){
sum++;
int y;
do{
y=s[--top];
v[y]=0;
c[y]=sum;
}while(y!=x);
}
}
void in(){
memset(h,-1,sizeof(h));
n=read();
m=read();
int x,y;
for(int i=1;i<=m;i++){
x=read();
y=read();
add(x,y);
add(y,x);
}
}
void work(){
tarjan(1,1);
for(int x=1;x<=n;x++){
for(int i=h[x];i!=-1;i=p[i].n){
if(p[i].c){
f[c[x]]++;
}
}
}
ans=0;
for(int i=1;i<=sum;i++){
if(f[i]==1){
ans++;
}
}
}
void out(){
cout<<(ans+1)/2;
}
int main(){
in();
work();
out();
return 0;
}
ATM
问最多价值
树形DP或者最长路
由于树形DP并不是我的长处,就打了SPFA
由于每个点只会算一次,所以还要缩点
强连通分量里每个点都可以到达强连通分量的其他节点,
因此如果有一个节点可以被当成终点那么整个强连通分量都可以是终点
强连通分量的点权就直接是每个节点的加和
这样以后再跑SPFA就行了
起点固定终止点固定在满足终止点的最大值位置就行了
Code
ATM
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
const int o=5e5+10;
struct path{
int t;
int n;
}e[o],map[o];
int st[o],head[o],cnt;
int atm[o],money[o];
int d[o],q[o];
int stack[o],top;
int dfn[o],low[o],vis[o],color[o],num,dex;
int n,m,a,b,s,p,ans,z;
int read(){
int i=1,j=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-'){
i=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
j=(j<<1)+(j<<3)+ch-'0';
ch=getchar();
}
return i*j;
}
void build(int s,int t){
e[++cnt].t=t;
e[cnt].n=st[s];
st[s]=cnt;
}
void tarjan(int x){
dfn[x]=++dex;
low[x]=dex;
vis[x]=1;
stack[++top]=x;
for(int i=st[x];i!=0;i=e[i].n){
int y=e[i].t;
if(!dfn[y]){
tarjan(y);
low[x]=min(low[x],low[y]);
}
else if(vis[y]){
low[x]=min(low[x],dfn[y]);
}
}
if(dfn[x]==low[x]){
vis[x]=0;
color[x]=++num;
while(stack[top]!=x){
color[stack[top]]=num;
vis[stack[top--]]=0;
}
top--;
}
}
void add(){
cnt=0;
for(int i=1;i<=n;i++){
for(int j=st[i];j;j=e[j].n){
int y=e[j].t;
if(color[i]!=color[y]){
map[++cnt].t=color[y];
map[cnt].n=head[color[i]];
head[color[i]]=cnt;
}
}
}
}
void spfa(int x){
memset(vis,0,sizeof(vis));
int l=1,r=1;
q[l]=x;
vis[x]=1;
d[x]=money[x];
while(l<=r){
int u=q[l++];
for(int i=head[u];i!=0;i=map[i].n){
int v=map[i].t;
if(d[v]<d[u]+money[v]){
d[v]=d[u]+money[v];
if(vis[v]){
continue;
}
q[++r]=v;
vis[v]=1;
}
}
vis[u]=0;
}
}
void in(){
n=read();
m=read();
for(int i=1;i<=m;i++){
a=read();
b=read();
build(a,b);
}
}
void pre(){
for(int i=1;i<=n;i++){
if(!dfn[i]){
tarjan(i);
}
}
add();
for(int i=1;i<=n;i++){
atm[i]=read();
money[color[i]]+=atm[i];
}
}
void work(){
s=read();
p=read();
spfa(color[s]);
for(int i=1;i<=p;i++){
z=read();
ans=max(ans,d[color[z]]);
}
}
void out(){
printf("%d",ans);
}
int main(){
in();
pre();
work();
out();
return 0;
}
Trick or Treat
差不多和ATM一个题吧。。。
只不过终点任意
Code
Trick or Treat
#include<map>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define MAXN 100010
using namespace std;
map<int,int>ma[MAXN];
int n,tot,tot1,tim,top,num,sumcol;
int to[MAXN],net[MAXN],head[MAXN];
int to1[MAXN],net1[MAXN],head1[MAXN];
int stack[MAXN],visstack[MAXN],ans[MAXN];
int low[MAXN],dfn[MAXN],vis[MAXN],col[MAXN],sum[MAXN];
void add(int u,int v){
to[++tot]=v;net[tot]=head[u];head[u]=tot;
}
void add1(int u,int v){
to1[++tot1]=v;net1[tot1]=head1[u];head1[u]=tot1;
}
void tarjin(int now){
low[now]=dfn[now]=++tim;
vis[now]=1;visstack[now]=1;
stack[++top]=now;
for(int i=head[now];i;i=net[i])
if(visstack[to[i]])
low[now]=min(low[now],dfn[to[i]]);
else if(!vis[to[i]]){
tarjin(to[i]);
low[now]=min(low[now],low[to[i]]);
}
if(low[now]==dfn[now]){
col[now]=++sumcol;
while(stack[top]!=now){
col[stack[top]]=sumcol;
visstack[stack[top]]=0;
top--;sum[sumcol]++;
}
visstack[now]=0;top--;sum[sumcol]++;
}
}
void dfs(int now){
if(ans[now]) return ;
ans[now]+=sum[now];
for(int i=head1[now];i;i=net1[i]){
dfs(to1[i]);
ans[now]+=ans[to1[i]];
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
int x;scanf("%d",&x);
add(i,x);
}
for(int i=1;i<=n;i++)
if(!vis[i]) tarjin(i);
for(int i=1;i<=n;i++)
for(int j=head[i];j;j=net[j])
if(col[i]!=col[to[j]])
if(ma[col[i]].find(col[to[j]])==ma[col[i]].end()){
ma[col[i]][col[to[j]]]=1;
add1(col[i],col[to[j]]);
}
for(int i=1;i<=n;i++) dfs(col[i]);
for(int i=1;i<=n;i++) cout<<ans[col[i]]<<endl;
}
炸弹
打过题解了,直接放链接
炸弹
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具