Tarjan 總結及各類題型拓展(縮點篇)
【Tarjan演算法的作用】:
- 求強連通分量;
- 縮點(將一個環縮成一個點);
- 割點(這裡不談)……
【Tarjan演算法的過程】:
- 初始化陣列:dfn[u](時間戳:該節點是第幾個被首次訪問到的),low[u](low[u]表示u或u的子樹所能回溯到的棧中的最早的節點的dfn值)
- 堆疊:將u壓入棧頂
- 更新low[u]
- 對於邊(u,v),如果v不在棧中,即v是第一次被訪問,滿足dfn[v]==0;則繼續向下找,然後low[u]=min(low[u],low[v])
如果v在棧中,即v已被訪問,滿足dfn[v]!=0;如果v未被染色,代表v在棧中(dfn[v]!=0表示v進過棧,在棧中的點染色後被彈出,未被染色即未被彈出還在 棧中),則low[u]=min(low[u],dfn[v])
5.如果完成上述操作後 low[u]==dfn[u],則將u和在u之後入棧的所有節點彈出,被彈出的所有結點構成一個強連通分量
6.繼續搜尋(有向圖不一定連通),直到所有點都被遍歷
【圖解】:
【程式碼實現】(部分):
struct node{ int ver,next; }r[]; //鄰接表 inline void tarjan(int u){ dfn[u]=++num; //num計數 low[u]=num; sta[++top]=u; //手寫棧,入棧 for(int i=h[u];i;i=r[i].next){ int v=r[i].ver; if(!dfn[v]){ tarjan(v); //向下找,dfs的思想 low[u]=min(low[u],low[v]); } else if(!c[v]) //如果結點v還在棧中,則v不屬於任何強連通分量 low[u]=min(low[u],dfn[v]); } if(dfn[u]==low[u]){ c[u]=++col; //染色 while(sta[top]!=u){ c[sta[top]]=col; --top; } --top; //將u彈出(退棧) } }
【時間複雜度】:O(n+m)
【基礎題型】:
1.https://www.luogu.com.cn/problem/P2863
【題目大意】:
有一個n個點,m條邊的有向圖,請求出這個圖點數大於1的強聯通分量個數。
【題目分析】:
裸題,跳過,直接上程式碼
注意:求點數大於1的強聯通分量個數
【程式碼】:
#include<bits/stdc++.h> using namespace std; int n,m,cnt,tot,num,top,ans; int dfn[10005],low[10005],sta[10005],take[10005],head[10005],color[10005]; struct node{ int ver,next; }r[200005]; inline void add(int x,int y){ r[++cnt].ver=y; r[cnt].next=head[x]; head[x]=cnt; } inline void tarjan(int x){ dfn[x]=++tot; low[x]=tot; sta[++top]=x; for(int i=head[x];i;i=r[i].next){ int y=r[i].ver; if(!dfn[y]){ tarjan(y); low[x]=min(low[x],low[y]); } else if(!color[y]) low[x]=min(low[x],dfn[y]); } if(low[x]==dfn[x]){ color[x]=++num; while(sta[top]!=x){ color[sta[top]]=num; --top; } --top; } } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=m;i++){ int u,v; scanf("%d%d",&u,&v); add(u,v); } for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i); for(int i=1;i<=n;i++) take[color[i]]++; for(int i=1;i<=num;i++) if(take[i]>1) ans++; printf("%d",ans); return 0; }
2.https://www.luogu.com.cn/problem/P2002
【題目大意】:
有n個城市,中間有單向道路連線,訊息會沿著道路擴散,現在給出n個城市及其之間的道路,問至少需要在幾個城市釋出訊息才能讓這所有n個城市都得到訊息。
【題目分析】:
當1->2,2->3,3->1時,三點構成一個環,這時無論在哪個城市釋出訊息,1,2,3三個城市都能得到訊息,此時該環等效於一個點,用Tarjan演算法縮點,得到有向無環圖(可能不止一個)
然後進行拓撲排序(更像一種思想,不會去看一下),在所有入度為0的點(每一個有向無環圖的起點)釋出訊息,然後所有點都可以得到訊息
【圖解】:
顯而易見,只要在所有入度為0的點(有向無環圖的起點)(1,7兩點)釋出訊息,所有點就都可以收到訊息
【程式碼】:
#include<bits/stdc++.h>
using namespace std;
int n,m,cr,dsc,col,top,ans;
int c[100005],h[100005],dfn[100005],low[100005],sta[100005],rd[100005]; //rd[i]記錄i點的入度
struct node{
int ver,next;
}r[500005];
inline void add(int x,int y){
r[++cr].ver=y;
r[cr].next=h[x];
h[x]=cr;
}
inline void tarjan(int u){
dfn[u]=++dsc;
low[u]=dsc;
sta[++top]=u;
for(int i=h[u];i;i=r[i].next){
int v=r[i].ver;
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else
if(!c[v])
low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u]){
c[u]=++col;
while(sta[top]!=u){
c[sta[top]]=col;
top--;
}
top--;
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
}
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i);
for(int i=1;i<=n;i++)
for(int j=h[i];j;j=r[j].next){
int l=r[j].ver;
if(c[i]!=c[l]) rd[c[l]]++;
}
for(int i=1;i<=col;i++)
if(rd[i]==0)
ans++;
printf("%d",ans);
return 0;
}
3.https://www.luogu.com.cn/problem/P3387
【題目大意】:
給定一個n個點m條邊有向圖,每個點有一個權值,求一條路徑,使路徑經過的點權值之和最大。允許多次經過一條邊或者一個點,但是,重複經過的點,權值只計算一次。求權值和。
【題目分析+圖解】:
用一個數組w記錄每個點的點權,縮點,再用另一個數組W記錄縮點後的每個點的點權(為構成該縮點的所有點的點權之和),得到有向無環圖
可知:有三條路徑:1. 1->2->5
2. 1->3->5
3. 1->4->5
易得:三條路徑只需比較後半部分,若要使所選路徑的點權值和最大,則2,3,4三點中應選擇點權值最大的點
用sum[i]陣列進行DP操作,表示從入度為0的點(起點)到i點的路徑的最大權值和
注意:需要初始化sum[i]=W[i](只需要初始化入度為0的點,但所有點都初始化也沒關係),表示從i點走到i點經過的點的最大權值和(點權)
狀態轉移方程:sum[l]=max(sum[l],sum[i]+W[l])
【程式碼】#include<bits/stdc++.h>using namespace std;
queue<int> q; int n,m,cr,cR,col,top,arr,ans; int w[10005],W[10005],c[10005],h[10005],H[10005],sta[10005],dfn[10005],low[10005],rd[10005],sum[10005]; //小寫表示縮點前,大寫表示縮點後,c表示染色 struct node{ int ver,next; }r[100005],R[100005]; inline void add(int x,int y){ r[++cr].ver=y; r[cr].next=h[x]; h[x]=cr; } inline void Add(int x,int y){ R[++cR].ver=y; R[cR].next=H[x]; H[x]=cR; } inline void tarjan(int u){ dfn[u]=++arr; low[u]=arr; sta[++top]=u; for(int i=h[u];i;i=r[i].next){ int v=r[i].ver; if(!dfn[v]){ tarjan(v); low[u]=min(low[u],low[v]); } else if(!c[v]) low[u]=min(low[u],dfn[v]); } if(dfn[u]==low[u]){ c[u]=++col; W[col]+=w[u]; //計算縮點後的點的權值 while(sta[top]!=u){ W[col]+=w[sta[top]]; c[sta[top]]=col; --top; } --top; } } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&w[i]); for(int i=1;i<=m;i++){ int x,y; scanf("%d%d",&x,&y); add(x,y); } for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i); for(int i=1;i<=n;i++) for(int j=h[i];j;j=r[j].next){ int l=r[j].ver; if(c[i]!=c[l]){ Add(c[i],c[l]); rd[c[l]]++; //統計入度 } } for(int i=1;i<=col;i++) //初始化 sum[i]=W[i]; for(int i=1;i<=col;i++) if(rd[i]==0) q.push(i); //入隊,進行拓撲排序(bfs),佇列裡存放入度為0的點 while(q.size()){ int i=q.front(); q.pop(); if(rd[i]==0) for(int j=H[i];j;j=R[j].next){ int l=R[j].ver; rd[l]--; if(rd[l]==0) q.push(l); //如果入度為0,入隊,入隊後不會再次入隊,無需判斷 sum[l]=max(sum[l],sum[i]+W[l]); } } for(int i=1;i<=col;i++) ans=max(ans,sum[i]); //拓撲排序(搜尋)完後再更新ans,否則答案可能會出錯 printf("%d",ans); return 0; }
【拓展題型】:
4.https://www.luogu.com.cn/problem/P2341
【題目分析】:
易得:存在於同一個強聯通分量裡的所有牛一定互相受歡迎
那麼,找出入度為0的縮點後的點(反向建邊),這樣可以保證所有的奶牛都喜歡它,但是它不喜歡任何人,所以說不存在其他奶牛明星
特殊情況:如果有兩個入度為0的縮點,則不存在奶牛明星,因為這樣無法滿足所有的牛喜歡他
【程式碼】:
#include<bits/stdc++.h>
using namespace std;
int n,m,cnt,tot,dsc,col,top,ans;
int c[10005],h[10005],dfn[10005],low[10005],rd[10005],sta[10005],num[10005];
struct node{
int ver,next;
}r[200005];
inline void add(int x,int y){
r[++tot].ver=y;
r[tot].next=h[x];
h[x]=tot;
}
inline void tarjan(int u){
dfn[u]=++dsc;
low[u]=dsc;
sta[++top]=u;
for(int i=h[u];i;i=r[i].next){
int v=r[i].ver;
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else
if(!c[v])
low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u]){
c[u]=++col;
num[col]++;
while(sta[top]!=u){
num[col]++;
c[sta[top]]=col;
top--;
}
top--;
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
add(y,x); //反向建邊
}
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i);
for(int i=1;i<=n;i++)
for(int j=h[i];j;j=r[j].next){
int l=r[j].ver;
if(c[i]!=c[l])
rd[c[l]]++;
}
dsc=0;
for(int i=1;i<=col;i++)
if(rd[i]==0){
ans=num[i];
dsc++; //統計入度為0的點的個數
}
if(dsc>1) ans=0; //如果存在兩個及兩個以上的入度為0的點,則不存在明星奶牛
printf("%d",ans);
return 0;
}
5.https://www.luogu.com.cn/problem/P2746
【題目分析+圖解】:
先給你一條鏈,如何使點上任意一點都可以到達其他所有點
分析一下就很容易想到,只需要加一條邊,使該鏈構成一個環
接下來類比,將樹轉化為幾條鏈,鏈數為出度為0的結點(在下面的情況下可理解為樹的葉子節點)的個數
但存在另一種情況
此時鏈數為入度為0的點的數量
所以需要新增的邊的數量為 max(入度為0的點的數量,出度為0的點的數量)
特殊情況見程式碼
【程式碼】:
#include<bits/stdc++.h>
using namespace std;
int n,cr,cR,dsc,col,top,lck,ans;
bool V[1000];
int c[1000],h[1000],H[1000],dfn[1000],low[1000],sta[1000],rd[1000],cd[1000]; //cd[]表示出度
struct node{
int ver,next;
}r[100000],R[100000];
inline void add(int x,int y){
r[++cr].ver=y;
r[cr].next=h[x];
h[x]=cr;
}
inline void Add(int x,int y){
R[++cR].ver=y;
R[cR].next=H[x];
H[x]=cR;
}
inline void tarjan(int u){
dfn[u]=++dsc;
low[u]=dsc;
sta[++top]=u;
for(int i=h[u];i;i=r[i].next){
int v=r[i].ver;
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else
if(!c[v])
low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u]){
c[u]=++col;
while(sta[top]!=u){
c[sta[top]]=col;
top--;
}
top--;
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
int x;
scanf("%d",&x);
while(x!=0){
add(i,x);
scanf("%d",&x);
}
}
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i);
for(int i=1;i<=n;i++){
memset(V,0,sizeof(V));
for(int j=h[i];j;j=r[j].next){
int l=r[j].ver;
if(V[c[l]]) continue;
if(c[i]!=c[l]){
Add(c[i],c[l]);
V[c[l]]=1;
rd[c[l]]++;
cd[c[i]]++; //同時記錄出度和入度
}
}
}
for(int i=1;i<=col;i++){
if(rd[i]==0)
lck++;
if(cd[i]==0)
ans++;
}
if(col==1) //特殊情況:如果整個圖縮為一個點,則不需要加邊
printf("%d\n0",lck);
else printf("%d\n%d",lck,max(lck,ans));
return 0;
}
6.https://www.luogu.com.cn/problem/P3627
【題目分析】:
本題跟【基礎題型】3 類似,但如果採用同樣的解題方法會超時,那麼我們需要一些特殊操作
首先我們需要將點權轉化為邊權
一條邊的權值為該邊通向的縮點後的點的點權
然後取負,用SPFA演算法搜最短路,然後求出的最小值取負,得到最長路的結果,即為答案
【程式碼】:
#include<bits/stdc++.h>
using namespace std;
int n,m,s,p,cr,cR,col,dsc,top,ans;
bool V[500005],jb[500005],JB[500005]; //jb[i]表示縮點前i點是否為酒吧,JB[i]表示縮點後i點是否為酒吧
int c[500005],w[500005],W[500005],h[500005],H[500005],dfn[500005],low[500005],sta[500005],dis[500005];
struct node{
int ver,edge,next;
}r[500005],R[500005];
queue<int> q;
inline void add(int x,int y){
r[++cr].ver=y;
r[cr].next=h[x];
h[x]=cr;
}
inline void Add(int x,int y,int z){
R[++cR].ver=y;
R[cR].edge=z;
R[cR].next=H[x];
H[x]=cR;
}
inline void tarjan(int u){
dfn[u]=++dsc;
low[u]=dsc;
sta[++top]=u;
for(int i=h[u];i;i=r[i].next){
int v=r[i].ver;
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else
if(!c[v])
low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u]){
c[u]=++col;
W[col]+=w[u];
if(jb[u]) JB[col]=1; //如果該強連通分量中有一點為酒吧,則縮點後可以在該點(結束)統計答案
while(sta[top]!=u){
W[col]+=w[sta[top]];
c[sta[top]]=col;
if(jb[sta[top]]) JB[col]=1;
--top;
}
--top;
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
}
for(int i=1;i<=n;i++)
scanf("%d",&w[i]);
scanf("%d%d",&s,&p);
for(int i=1;i<=p;i++){
int x;
scanf("%d",&x);
jb[x]=1;
}
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i);
memset(dis,0x7fffffff,sizeof(dis)); //初始化
dis[c[s]]=-W[c[s]]; //初始化,縮點c[s]沒有入度,所以dis[c[s]]權值為點c[s]的點權的相反數
for(int i=1;i<=n;i++){
for(int j=h[i];j;j=r[j].next){
int l=r[j].ver;
if(c[i]!=c[l])
Add(c[i],c[l],-W[c[l]]); //取負
}
}
q.push(c[s]);
while(q.size()){
int x=q.front();
q.pop();
V[x]=0;
for(int i=H[x];i;i=R[i].next){
int j=R[i].ver;
int l=R[i].edge;
if(dis[j]>dis[x]+l){
dis[j]=dis[x]+l;
if(!V[j]) q.push(j);
V[j]=1;
}
}
}
for(int i=1;i<=col;i++)
if(JB[i]) //判斷是否可以在該點結束(更新答案)
ans=max(ans,-dis[i]);
printf("%d",ans);
return 0;
}
2020-07-25