第四關——圖論:強連通分量


14:27:28 寫一首十幾歲聽的情歌,可惜我沒在那個時候遇見你,否則我努力活到百歲以后,就剛好愛你一整個世紀  ——《零幾年聽的情歌

今天是待在學校的最后一天了,撒花,慶祝!!!那也祝自己十六歲生日快樂

最近肺炎傳染有點嚴重,大家能點外賣點外賣,能躺床躺床,少出門,你肆無忌憚賴在家的機會來了!!!

好了,今天要講的呢,是要待在家好好學習一下的強連通分量

  • 概念

連通分量:在無向圖中,即為連通子圖。

有向圖強連通分量:在有向圖G中,如果兩個頂點vi,vj間(vi>vj)有一條從vi到vj的有向路徑,同時還有一條從vj到vi的有向路徑,則稱兩個頂點強連通(strongly connected)。如果有向圖G的每兩個頂點都強連通,稱G是一個強連通圖。有向圖的極大強連通子圖,稱為強連通分量(strongly connected components)

極大強連通子圖:G是一個極大強連通子圖,當且僅當G是一個強連通子圖且不存在另一個強連通子圖G’,是得G是G'的真子集

下圖中,子圖{1,2,3,4}為一個強連通分量,因為頂點1,2,3,4兩兩可達。{5},{6}也分別是兩個強連通分量。

  • 用雙向遍歷取交集的方法求強連通分量,時間復雜度為O(N^2+M)。

  • Kosaraju算法或Tarjan算法求強連通分量,兩者的時間復雜度都是O(N+M)。

  •  Tarjan算法

基於對圖深度優先搜索,每個強連通分量為搜索樹中的一棵子樹。

算法流程

  • 搜索時,把當前搜索樹中未處理的節點加入一個堆棧
  • 回溯時可以判斷棧頂到棧中的節點是否為一個強連通分量。
  • 定義DFN(u)為節點u搜索的次序編號(時間戳),Low(u)為u或u的子樹能夠追溯到的最早的棧中節點的次序號。
  • 當DFN(u)=Low(u)時,以u為根的搜索子樹上所有節點是一個強連通分量。

 以下為網上找的算法演示流程:

從節點1開始DFS,把遍歷到的節點加入棧中。搜索到節點u=6時,DFN[6]=LOW[6],找到了一個強連通分量。退棧到u=v為止,{6}為一個強連通分量。

image

返回節點5,發現DFN[5]=LOW[5],退棧后{5}為一個強連通分量。

image

返回節點3,繼續搜索到節點4,把4加入堆棧。發現節點4向節點1有后向邊,節點1還在棧中,所以LOW[4]=1。節點6已經出棧,(4,6)是橫叉邊,返回3,(3,4)為樹枝邊,所以LOW[3]=LOW[4]=1。

image

繼續回到節點1,最后訪問節點2。訪問邊(2,4),4還在棧中,所以LOW[2]=DFN[4]=5。返回1后,發現DFN[1]=LOW[1],把棧中節點全部取出,組成一個連通分量{1,3,4,2}。

image

 

P1407 [國家集訓隊]穩定婚姻

此題需要用到一個map,就是類似於此。

map<string,int> a;、
for(int i=1;i<=n;i++)
    {
        string x,y;
        cin>>x>>y;
        a[x]=++tot;a[y]=++tot;
    }

因為需要存圖,我在這里用的vector的鄰接表,詳細請看第三關。

這道題主要用到的就是targan算法,具體看代碼,當然也有其他方法可以用

#include<bits/stdc++.h>
using namespace std;
int n,m,d[20009],tot,cnt,l[20009];
bool v[20009];
vector<int> f[8005];
map<string,int> a;
stack<int> s;
void tarjan(int x)
{
    d[x]=l[x]=++cnt;
    v[x]=true;
    s.push(x);
    for(int i=0;i<f[x].size();++i)
    {
        int o=f[x][i];
        if(!d[o])
        {
            tarjan(o);
            l[x] =min(l[x], l[o]);
        }
        else if(v[o])l[x]=min(l[x],d[o]);
    }
    if(d[x]==l[x])
    {
        v[x]=false;
        while(s.top()!=x)
        {
            v[s.top()]=false;
            s.pop();
        }
    }
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        string x,y;
        cin>>x>>y;
        a[x]=++tot;a[y]=++tot;
        f[a[x]].push_back(a[y]);
    }
    scanf("%d",&m);
    for(int i=1;i<=m;i++)
    {
        string x,y;
        cin>>x>>y;
        f[a[y]].push_back(a[x]);
    }
    cnt = 0;
    for(int i=1;i<=n*2;i++)
    if(!d[i])
    tarjan(i);
    cnt=0;
    for(int i=1;i<=n;i++)
    {
        if(l[++cnt]==l[++cnt])
        printf("Unsafe\n");
        else
        printf("Safe\n");
    }
    return 0;
 } 
  • Kosaraju算法

基於對有向圖及其逆圖兩次DFS的方法Kosaraju算法可能會稍微更直觀一些。但是Tarjan只用對原圖進行一次DFS,不用建立逆圖,更簡潔。在實際的測試中,Tarjan算法的運行效率也比Kosaraju算法高30%左右。

算法流程:

  • 先用對原圖G進行深搜生成樹
  • 然后任選一棵樹對其進行深搜(注意這次深搜節點A能往子節點B走的要求是EAB存在於反圖GT)
  • 能遍歷到的頂點就是一個強連通分量
  • 余下部分和原來的樹一起組成一個新的樹
  • 直到沒有頂點為止。

首先了解kosarajuo法,要想了解逆圖

逆圖(Tranpose Graph ):

我們對逆圖定義如下:

      GT=(V, ET),ET={(u, v):(v, u)∈E}}

以下為網上找的算法演示流程:

上圖是對圖G,進行一遍DFS的結果,每個節點有兩個時間戳,即節點的發現時間u.d和完成時間u.f

我們將完成時間較大的,按大小加入堆棧

1)每次從棧頂取出元素

2)檢查是否被訪問過

3)若沒被訪問過,以該點為起點,對逆圖進行深度優先遍歷

4)否則返回第一步,直到棧空為止

 

對逆圖搜索時,從一個節點開始能搜索到的最大區塊就是該點所在的強連通分量。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
struct edge{
    int to,next;
}edge1[maxn],edge2[maxn];
//edge1是原圖,edge2是逆圖
int head1[maxn],head2[maxn];
bool mark1[maxn],mark2[maxn];
int tot1,tot2;
int cnt1,cnt2;
int st[maxn];//對原圖進行dfs,點的結束順序從小到大排列。
int belong[maxn];//每個點屬於哪個聯通分量
int num;//每個聯通分量的個數
int setnum[maxn];//每個聯通分量中點的個數
 
void addedge(int u,int v){
    edge1[tot1].to=v;edge1[tot1].next=head1[u];head1[u]=tot1++;
    edge2[tot2].to=u;edge2[tot2].next=head2[v];head2[v]=tot2++;
} 
void dfs1(int u){
    mark1[u]=true;
    for(int i=head1[u];i!=-1;i=edge1[i].next)
        if(!mark1[edge1[i].to])
            dfs1(edge1[i].to);
    st[cnt1++]=u;
}
void dfs2(int u){
    mark2[u]=true;
    num++;
    belong[u]=cnt2;
    
    for(int i=head2[u];i!=-1;i=edge2[i].next)
        if(!mark2[edge2[i].to])
            dfs2(edge2[i].to);
}
void solve(int n){//點編號從1開始
    memset(mark1,false,sizeof(mark1)); 
    memset(mark2,false,sizeof(mark2)); 
    cnt1=cnt2=0;
    for(int i=1;i<=n;i++)
        if(!mark1[i])
            dfs1(i);
            
    for(int i=cnt1-1;i>=0;i--)
        if(!mark2[st[i]]){
            num=0;
            dfs2(st[i]);
            setnum[cnt2++]=num;
        }
}
int main(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int s,d;
        cin>>s>>d;
        addedge(s,d);
    }
    solve(1);
    return 0;
} 

 P2002 消息擴散

其實kosaraju的復雜度和空間都要費的多一些

  1.  有自環
  2. 縮點,然后找入度為0的強連通分量個數就好了。對此,需要用mp1和mp2數組記錄每條邊連接的點最后遍歷一遍所有的邊

 

#include <bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
const int maxx=5e5+5;
vector<int>g[maxn],g2[maxn],st;
bool vis[maxn];
int k,cmp[maxn],mp1[maxx],mp2[maxx],cnt,du[maxn];
int n,m;
void dfs(int x){
    vis[x]=1;
    for(int i=0;i<g[x].size();++i){
        int s=g[x][i];
        if(!vis[s]){
            dfs(s);
        }
    }
    st.push_back(x);
}
void dfs2(int x,int k){
    cmp[x]=k;
    vis[x]=1;
    for(int i=0;i<g2[x].size();++i){
        int s=g2[x][i];
        if(!vis[s]){
            dfs2(s,k);
        }
    }
}
void init(){
    for(int i=1;i<=n;i++){
        if(!vis[i]) dfs(i);
    }
    for(int i=1;i<=n;i++){
        vis[i]=0;
    }
    for(int i=st.size()-1;i>=0;i--){
        if(!vis[st[i]]){
            k++;
            dfs2(st[i],k);
        }
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;++i){
        int p1,p2;
        scanf("%d%d",&p1,&p2);
        cnt++;
        mp1[cnt]=p1;mp2[cnt]=p2;
        g[p1].push_back(p2);
        g2[p2].push_back(p1);
    }
    init();
    for(int i=1;i<=cnt;i++){
        int p1=mp1[i],p2=mp2[i];
        if(cmp[p1]!=cmp[p2]){
        //  g[cmp[p1]].push_back(cmp[p2]);
            du[cmp[p2]]++;
        }
    }
    int ans=0;
    for(int i=1;i<=k;i++){
        if(!du[i]) ans++;
    }
    printf("%d\n",ans);
    return 0;
}                  
  • 縮點

定義:將有向圖中的強連通分量縮成一個點。

在Targan算法與Kosaraju算法中有所體現

P2194 HXY燒情侶

這是一道Targan加縮點的題

vector數組記錄每個聯通塊里的每一個點

最小汽油費即為每個聯通塊里最小點權

方案數即為每個聯通塊里最小點權的點數之積(乘法原理)%1e9+7

#include<bits/stdc++.h>
#define N 501010
using namespace std;
int n,head[N],tot,w[N],m,ans1,ans2=1;
struct node {
    int to,next;
} e[N];
void add(int u,int v) 
{
    e[++tot].to=v,e[tot].next=head[u],head[u]=tot;
}
const int mod=1e9+7;
int dfn[N],low[N],item,b[N],a[N],cnt;
bool vis[N];
stack<int>S;
vector<int>g[N];
void tarjan(int u){
    dfn[u]=low[u]=++item;
    S.push(u);vis[u]=1; 
    for(int i=head[u];i;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(low[u]==dfn[u]){
        int v=u;++cnt;
        do{
            v=S.top();S.pop();
            vis[v]=0;b[v]=cnt;a[cnt]++;
            g[cnt].push_back(v);
        }while(v!=u);
    }
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) 
    scanf("%d",&w[i]);
    scanf("%d",&m);
    for(int a,b,i=1;i<=m;i++) 
    {
        scanf("%d%d",&a,&b);
        add(a,b);
    }
    for(int i=1;i<=n;i++)
    if(!dfn[i]) 
    tarjan(i);
    for(int i=1;i<=cnt;i++){
        int tpt=g[i].size(),sby=0,mi=mod;
        for(int j=0;j<tpt;j++){
            if(w[g[i][j]]<mi){
                mi=w[g[i][j]];
                sby=1;
            }else if(mi==w[g[i][j]]) ++sby;
        }
        ans1+=mi;
        ans2=(ans2%mod*sby%mod)%mod;
    }
    printf("%d %d",ans1,ans2);
    return 0;
}

21:37:37 我們也學會慢慢地慢慢地推卸,我們也有過一次又一次的越界。——王巨星《越界》

熱烈祝賀我的寒假開始!!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM