Tarjan算法
Tarjan算法是用於求圖上的強連通分量(環)的算法。
應用:
- 有向圖求強連通分量/縮點
- 無向圖求割點
- 無向圖找環
求強連通分量/縮點
強連通是有向圖才有的概念。如果有向圖G的每兩個頂點都強連通,稱G是一個強連通圖。有向圖的極大強連通子圖,稱為強連通分量。求有向圖的強連通分量是Tarjan最基本的應用。
算法原理:
Tarjan算法的復雜度是O(v+e)的,因為只需要進行一次dfs就能處理出一個塊中所有的強連通分量。首先建立dfn[]數組和low[]數組,前者記錄節點的dfs序(第幾個被dfs到),確定后就不會改變,后者記錄該節點屬於的強連通分量(能遍歷到的點)中dfs序最小的節點(的dfs序),是會不斷更新的。申明一個棧,用於存儲可能成為強連通分量中的點。
然后,依次對每個節點進行dfs(因為從一個節點不一定能遍歷到其他所有的節點),初始化low[]為dfs序(low[u]=dfn[u]=dfsid),將每次dfs的點u加入棧中,遍歷這個點能到達的相鄰的點v,如果這個點在棧中,說明構成了一個環,也就是強連通分量,就用v點的dfs序更新(取min)low[u],當遞歸回溯時,所有強連通分量中的點low[]值都會被更新為dfn[v]。當一個點u的low[]值與它的dfs序相同時,說明這個點是一個強連通分量的根(也可能這個分量只有這一個點),將棧中的點逐個彈出,直至u(也彈出)為止,將這些點染成同個顏色。
縮點:對於有些有向圖上的問題,可以將一個強連通分量用一個點來等效替代,這個時候染成的一種顏色就是新圖上的一個點。
int vis[maxn];//記錄棧中的元素
int cnt[maxn];//記錄一個強連通分量中的結點個數
int color[maxn];//染色,將同一個強連通分量中的點染成同個顏色
int dfn[maxn],low[maxn],dfsid=0,id=0;
stack<int>st;
void tarjan(int u){
low[u]=dfn[u]=++dfsid;
vis[u]=1;
st.push(u);
for(int i=head[u];i>0;i=E[i].next){
int v=E[i].to;
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(vis[v])
low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u]){//u是一個強連通分量的根
id++;
int k;
do{
k=st.top();st.pop();
vis[k]=0;
color[k]=id;
cnt[id]++;
}while(k!=u);
}
}
int main(){
for(int i=1;i<=n;i++)
if(!dfn[i])tarjan(i);
}
無向圖求割點
割點是無向圖才有的概念。如果有一個頂點集合,刪除這個頂點集合以及這個集合中所有頂點相關聯的邊以后,圖的連通分量增多,就稱這個點集為割集。若一個割集只有一個點,則稱這個點為割點。
算法原理:
設u為dfs中的非根結點,如果點u的子結點v(后遍歷到的點)能遍歷到的最小 dfs序>=dfn[u],那么刪除u后v將和u之前的點斷開,說明u是割點 。雖然是無向圖,但這里的遍歷是有方向的,一個點不能沿着已經訪問過的點繼續往前(根)遍歷,但是low[]值能被所有相鄰點更新。最后得到的low[]並不是正確的(能訪問到的最小dfs序),因為后續結點被限制在只能遍歷到已訪問過的點就停止了,因此判斷的時候用low[v]==dfn[u]其實也是可以的。
若u是dfs中的根結點,那么如果u有2個及以上的兒子(除去u之外的每個連通塊都是一個兒子),u是割點。
#include <bits/stdc++.h>
using namespace std;
const int maxm=1e6+5;
const int maxn=1e5+5;
struct edge{
int v,next;
}E[maxm];
int head[maxn],tot=0;
void addedge(int u,int v){
E[++tot].v=v;
E[tot].next=head[u];
head[u]=tot;
}
set<int>res;//一定要用set,因為兩種判割點方式可能會重復記錄一個點
int low[maxn],dfn[maxn],dfsid;
void tarjan(int u,int rt){
low[u]=dfn[u]=++dfsid;
int child=0;
for(int i=head[u];i>0;i=E[i].next){
int v=E[i].v;
if(!dfn[v]){
child++;
tarjan(v,rt);
low[u]=min(low[u],low[v]);
if(u!=rt&&low[v]>=dfn[u]){
res.insert(u);
}
}
low[u]=min(low[u],dfn[v]);
}
if(u==rt&&child>1)
res.insert(u);
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
addedge(u,v);
addedge(v,u);
}
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i,i);
printf("%d\n",res.size());
for(auto v:res){
printf("%d ",v);
}
printf("\n");
}
仙人掌圖判斷&判環
定義(有向圖和無向圖略有不同)
有向圖:
- 是一個強連通圖
- 每條有向邊都屬於且僅屬於一個環(強連通圖所有邊都必須在環上)
無向圖:
- 是一個連通圖
- 每條無向邊至多屬於一個環(即有些無向邊可以不屬於任何一個環)
無向圖仙人掌的判定和環計數:
首先判斷一個圖是否是連通的,dfs一次判斷點的個數即可,之后每找到一個環,就將環上除了根(最先遍歷到的點)之外的點度數都+1,如果一個點度數為2,說明這個點的前向邊被兩個環所共有,即該圖不是仙人掌圖。
//洛谷P4129,ans需要改成大數
#include <bits/stdc++.h>
using namespace std;
const int maxm=1e6+5;
const int maxn=1e5+5;
struct edge{
int v,next;
}E[maxm];
int head[maxn],tot=0;
void addedge(int u,int v){
E[++tot].v=v;
E[tot].next=head[u];
head[u]=tot;
}
int dfn[maxn],dep[maxn],fa[maxn],dfsid=0;
int cnt[maxn];//記錄一條邊屬於環的個數
long long ans=1;
bool cal(int u,int rt){
ans=ans*(dep[u]-dep[rt]+2);//dep[u]-dep[rt]+1為環的大小
for(u;u!=rt;u=fa[u]){//這里用點來代替dfs樹邊
if(++cnt[u]==2)return 0;//如果一個邊被兩個環共用,那么不是仙人掌圖
}
return 1;
}
int siz=0;
int ok=1;
void tarjan(int u){
siz++;
dfn[u]=++dfsid;
for(int i=head[u];i;i=E[i].next){
int v=E[i].v;
if(v==fa[u])continue;
if(!dfn[v]){
dep[v]=dep[u]+1;
fa[v]=u;
tarjan(v);
}
}
for(int i=head[u];i;i=E[i].next){
int v=E[i].v;
if(fa[v]!=u&&dfn[u]<dfn[v]){
if(!cal(v,u))
ok=0;
}
}
}
int a[maxn];
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=m;i++){
int ki;
scanf("%d",&ki);
for(int i=1;i<=ki;i++){
scanf("%d",&a[i]);
}
for(int i=1;i<=ki-1;i++){
addedge(a[i],a[i+1]);
addedge(a[i+1],a[i]);
}
}
tarjan(1);
if(ok==0||siz!=n){//邊被多個環共用||非連通圖
printf("0\n");
}
else{
cout<<ans<<endl;//所有環大小+1的乘積
}
}
有向圖仙人掌的判定:
先用tarjan判斷這個圖是否強連通,之后同無向圖一樣,找是否有度數為2的點。
#include <bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
const int maxm=1e6+5;
struct edge{
int v,next;
}E[maxm];
int head[maxn],tot=0;
void addedge(int u,int v){
E[++tot].v=v;
E[tot].next=head[u];
head[u]=tot;
}
int vis[maxn];//記錄棧中的元素
int dfn[maxn],low[maxn],fa[maxn],dfsid=0,id=0;
stack<int>st;
int siz=0;
void tarjan(int u){
siz++;
low[u]=dfn[u]=++dfsid;
vis[u]=1;
st.push(u);
for(int i=head[u];i>0;i=E[i].next){
int v=E[i].v;
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(vis[v])
low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u]){//u是一個強連通分量的根
id++;
int k;
do{
k=st.top();st.pop();
vis[k]=0;
}while(k!=u);
}
}
int du[maxn];
bool cal(int u,int rt){
for(u;u!=rt;u=fa[u]){
if(++du[u]==2){
return 0;
}
}
return 1;
}
int ok=1;
void dfs(int u){
dfn[u]=++id;
for(int i=head[u];i;i=E[i].next){
int v=E[i].v;
if(!dfn[v]){
fa[v]=u;
dfs(v);
}
else{
if(!cal(u,v)){
ok=0;
}
}
}
}
void init(int n){
dfsid=0;id=0;
fill(dfn,dfn+1+n,0);
fill(low,low+1+n,0);
fill(fa,fa+1+n,0);
fill(du,du+1+n,0);
fill(vis,vis+1+n,0);
siz=0;ok=1;
}
int main(){
int T;
cin>>T;
while(T--){
int n;
scanf("%d",&n);
init(n);
fill(head,head+1+n,0);
tot=0;
int u,v;
while(scanf("%d%d",&u,&v)){
if(u==0&&v==0) break;
u++;v++;
addedge(u,v);
}
tarjan(1);
if(id>1||siz!=n){
printf("NO\n");
continue;
}
init(n);
dfs(1);
if(!ok){
printf("NO\n");
}
else{
printf("YES\n");
}
}
}
例題
Bomb(縮點/拓撲)
題意:
一個平面上有n個炸彈,每個炸彈i有一個圓形的爆炸范圍ri和二維坐標xi,yi,以及手動引爆這個炸彈的費用ci。如果引爆了一個炸彈,其爆炸范圍內(包括邊界)的所有其他炸彈都會被引爆(會發生連鎖反應),問如何以最小的費用引爆所有的炸彈?
思路:
顯然一個炸彈能引爆其他炸彈,但是其他炸彈不一定能引爆這個炸彈,這是一個有向圖。但是有的炸彈可能會循環引爆構成一個環,用tarjan將圖上的環縮掉就可以變成一個森林圖,顯然只要把圖中所有入度為0節點引爆,所有點都會被引爆。
縮點后,計算入度前要判斷該邊連接的兩個點u,v是否為同一顏色,同色就不計度數,不同顏色就將v所在顏色的入度+1。同樣地,計算一個大點的權值也是用小點對該顏色的權值取min。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e3+5;
const int maxm=2e6+5;
struct edge{
int v,next;
}E[maxm];
int tot=0,head[maxn];
void addedge(int u,int v){
E[++tot].v=v;
E[tot].next=head[u];
head[u]=tot;
}
int vis[maxn];//記錄棧中的元素
int cnt[maxn];//記錄一個強連通分量中的結點個數
int color[maxn];//染色,將同一個強連通分量中的點染成同個顏色
int dfn[maxn],low[maxn],dfsid=0,id=0;
int n;
stack<int>st;
void tarjan(int u){
low[u]=dfn[u]=++dfsid;
vis[u]=1;
st.push(u);
for(int i=head[u];i>0;i=E[i].next){
int v=E[i].v;
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(vis[v])
low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u]){//u是一個強連通分量的根
id++;
int k;
do{
k=st.top();st.pop();
vis[k]=0;
color[k]=id;
cnt[id]++;
}while(k!=u);
}
}
int x[maxn],y[maxn],r[maxn],c[maxn];
bool f(ll x1,ll y1,ll r1,ll x2,ll y2){
if((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)<=r1*r1)
return 1;
else return 0;
}
int du[maxn];//入度
int cost[maxn];
void init(){
memset(cost,0,sizeof(cost));
memset(du,0,sizeof(du));
memset(head,0,sizeof(head));
memset(dfn,0,sizeof(dfn));
memset(low,0,sizeof(low));
memset(vis,0,sizeof(vis));
memset(cnt,0,sizeof(cnt));
dfsid=0,id=0;
tot=0;
while(st.size())st.pop();
}
int main(){
int t;
cin>>t;
for(int kase=1;kase<=t;kase++){
scanf("%d",&n);
init();
for(int i=1;i<=n;i++){
scanf("%d%d%d%d",&x[i],&y[i],&r[i],&c[i]);
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(j==i)continue;
if(f(x[i],y[i],r[i],x[j],y[j])){
addedge(i,j);
}
}
}
for(int i=1;i<=n;i++)
if(!dfn[i])tarjan(i);
for(int u=1;u<=n;u++){
for(int i=head[u];i;i=E[i].next){
int v=E[i].v;
if(color[v]!=color[u]){
du[color[v]]++;//記錄縮成的大點的入度
}
}
}
for(int i=1;i<=n;i++){
if(cost[color[i]]==0){
cost[color[i]]=c[i];
}
else
cost[color[i]]=min(cost[color[i]],c[i]);
}
int ans=0;
for(int i=1;i<=id;i++){
if(du[i]==0)
ans+=cost[i];
}
printf("Case #%d: %d\n",kase,ans);
}
}