Tarjan算法求有向圖的強連通分量


算法描述

tarjan算法思想:從一個點開始,進行深度優先遍歷,同時記錄到達該點的時間(dfn記錄到達i點的時間),和該點能直接或間接到達的點中的最早的時間(low[i]記錄這個值,其中low的初始值等於dfn)。如圖:

極易懂得Tarjan算法(求有向圖的強聯通分量)

  假設我們從1開始DFS,那么到達1的時間為1,到達2的時間為2,到達3的時間為3。同時,點1能直接或間接到達的點中,最小時間為1,點2能通過3間接到達點1,所以點2可到達最早的點時間為1,點3可以直接到達點1,故點3到達的最早的點的時間為1。)。對於每一個沒有被遍歷到的點A,如果從當前點有一條到未遍歷點A的有向邊,則遍歷到A,同時將點A入棧,時間戳+1並用dfn[a]記錄到達點A的時間,枚舉從A發出的每一條邊,如果該邊指向的點沒有被訪問過,那么繼續dfs,回溯后low[a]=min(low[a],low[j])(其中j為A可以到達的點。)如果該點已經訪問過並且該點仍在棧里,那么low[a]=min(low[a],dfn[j])。

解釋:

  若點j沒有被訪問過,那么回溯后low[j]就是j能到達最早點,a能到達的最早點當然就是a本身能到達的最早點,或者a通過j間接到達的最早點。若點j已經被訪問過,那么low[j]必然沒有被回溯所更新。所以low[a]就等於a目前能到達的最小點或a直接到達點j時點j的訪問時間。注意:兩個句子中的“或”其實指的是兩者的最小值。

那么如果我們回溯到一個點K他的low[k]=dfn[k]那么我們將K及其以前在棧中的點依次彈出,這些點即為一個強連通分量。(說明從k出發又回到k)

證明:

  因為該點dfn=low,所以在棧中的該點以上的點都能由該點直接或間接的到達。同時棧中在該點前的任意一點j,其dfn[j] != low[j](否則點j比點k靠前,又因為dfn[j]=low[j],j一定先被彈出了。)那么這個點j通過low[j]這個時間的點,一定能到達點k,否則,low[j]能到達點i,又因為dfn>=low所以有2種情況1、dfn>low:那么我們可以找到前面一個更小的點。2、dfn=low:應該在回溯到i的時候就找到了一個強連通分量,從而出棧了。而點k前的點沒有出棧,證明其中任意一點都能直接或者間接到達點k,進而證明這些點可以兩兩互達。

先用簡單模板刷一道水題入門:

迷宮城堡

本題的邊不帶權值,可以用vector表示鄰接鏈表:

#include <cstdio>
#include <memory.h>
#include <vector>
#include <algorithm>

using namespace std;

//本題的頂點號從1到n,故可直接用作vector的下標,不需要用head數組離散化 

const int MAXN = 10001;

int top;
int Stack[MAXN];
bool inStack[MAXN];
//DFN(u)為節點u搜索的次序編號(時間戳),Low(u)為u或u的子樹能夠追溯到的最早的棧中節點的次序號  
int dfn[MAXN], low[MAXN];
int Bcnt, Dindex; //記錄強連通的個數和當前時間 
vector<int> ve[MAXN]; //鄰接表保存邊

void init() {
    Bcnt = Dindex = top = 0;
    memset(dfn, -1, sizeof dfn);
    memset(inStack, false, sizeof inStack);
    for (int i = 1; i < MAXN; ++i) ve[i].clear();
}

void tarjan(int u) {
    int v = 0;
    dfn[u] = low[u] = ++Dindex;
    inStack[u] = true;
    Stack[++top] = u;
    int t = ve[u].size();
    for (int i = 0; i < t; ++i) {
        v = ve[u][i];
        if (dfn[v] == -1) {
            tarjan(v);
            low[u] = min(low[u], low[v]);
        } else if (inStack[v])
            low[u] = min(low[u], dfn[v]);
    }
    if (dfn[u] == low[u]) {
        Bcnt++;
        do {
            v = Stack[top--];
            inStack[v] = false;
        } while (u != v);
    }
} 

int main() {
#ifndef ONLINE_JUDGE
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
#endif
    int n, m;
    while (scanf("%d%d", &n, &m) != EOF) {
        if (m == 0 && n == 0) break;
        init();
        while (m--) {
            int a, b;
            scanf("%d%d", &a, &b);
            ve[a].push_back(b); 
        }
        for (int i = 1; i <= n; ++i) 
            if (dfn[i] == -1) tarjan(i);
        if (Bcnt == 1) puts("Yes");
        else puts("No");
    } 

    return 0;
}
View Code

Summer Holiday

只需找出所有的強連通分量,然后遍歷所有的邊,對於邊u->v,若u、v在不同的連通子圖,顯然v的子圖可以從邊u->v達到,對於這樣的兩個強連通分量,可以視為一個子圖,只需打電話給u所所在的子圖中人,然后讓這個人再聯系u,v所在的兩個強連通分量中的所有人。故只需找出所有的子圖,打給每個子圖中話費最低的那個人即可。

#include <cstdio>
#include <memory.h>
#include <algorithm> 

const int MAXN = 1001;
struct N {
    int u, v;//u to v
    int next;//下一個頂點 
} edge[MAXN * 2]; //m <= 2000
int head[MAXN], edgenum;

int DFN[MAXN], Low[MAXN], Dindex;//到達某頂點的費用,該子圖最小費用 ,當前費用 
int Bcnt, Belong[MAXN];    //強連通分量的個數, 當前頂點所屬的連通分量 
int Stack[MAXN], top;
bool inStack[MAXN];


void addedge(int u, int v) {
    N e = {u, v, head[u]};
    edge[edgenum] = e;
    head[u] = edgenum++; 
}

void init() {
    edgenum = top = Bcnt = Dindex =  0;
    memset(head, -1, sizeof head);
    memset(DFN, -1, sizeof DFN);
    memset(inStack, false, sizeof inStack);
}

void tanjar(int u) {
    int v = 0;
    DFN[u] = Low[u] = ++Dindex;
    Stack[++top] = u;
    inStack[u] = true;
    for (int i = head[u]; i != -1; i = edge[i].next) {
        v = edge[i].v;
        if (DFN[v] == -1) {
            tanjar(v);
            Low[u] = std::min(Low[u], Low[v]);
        } else if (inStack[v]) 
            Low[u] = std::min(Low[u], DFN[v]);
    }
    if (DFN[u] == Low[u]) {
        ++Bcnt;
        do {
            v = Stack[top--];
            Belong[v] = Bcnt; //將點加入該強連通圖 
            //printf("bcnt = %d v = %d\n", Bcnt, v); 
            inStack[v] = false;
        } while (u != v);
    }
}

int main() {
#ifndef ONLINE_JUDGE
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
#endif
    int n, m;
    int w[MAXN] = {0};
    while (scanf("%d%d", &n, &m) != EOF) {
        init();
        for (int i = 1; i <= n; ++i)
            scanf("%d", &w[i]);
        while (m--) {
            int a, b;
            scanf("%d%d", &a, &b);
            addedge(a, b);
        }
        for (int i = 1; i <= n; ++i) {
            if (DFN[i] == -1) tanjar(i);
        }
        //合並強連通圖 
        int mfee[MAXN] = {0};//記錄下每個強連圖集的最小話費 
        bool inode[MAXN] = {false}; //不可達的連通圖 
        int pos = 0;
        for (int i = 0; i < edgenum; ++i) {
            int u = edge[i].u;
            int v = edge[i].v;
            if (Belong[u] != Belong[v])  //v所在的連通子圖可以由u到達 
                inode[Belong[v]] = true;
        }
        for (int i = 1; i <= Bcnt; ++i) {
            if (!inode[i]) pos++;
            mfee[i] = 1e9;
        }
        for (int i = 1; i <= n; ++i) {
            int t = Belong[i];
            if (!inode[t]) 
                mfee[t] = std::min(mfee[t], w[i]);
        }
        int sum = 0;
        for (int i = 1; i <= Bcnt; ++i) {
            if (mfee[i] != 1e9) sum += mfee[i];
        }
        printf("%d %d\n", pos, sum);
    } 

    return 0;
}
View Code

Instantaneous Transference

題意:讓礦車采到盡可能多的礦。#表示牆,數字表示礦的數目,*表示定點傳送,可選擇傳或者不傳,礦車只能向右和向下開,不能倒退。

先來看下SPFA算法,可以求帶環最短(最長路徑):Currency Exchange

 

寶藏

#include <cstdio>
#include <memory.h>
#include <vector>

using namespace std;

const int MAXN = 100001;

inline int Min(int a, int b) {
    return a < b ? a : b;
}

inline int Max(int a, int b) {
    return a > b ? a : b;
}

struct arc {
    int u, v, next;
} edge[150001];
int head[MAXN];
int edgenum;

int DFN[MAXN], Low[MAXN], Dindex;
int Stack[MAXN], top;
bool inStack[MAXN];
int  Belong[MAXN], Bnum[MAXN], Bcnt;

void addedge(int u, int v) {
    arc na = {u, v, head[u]};
    edge[edgenum] = na;
    head[u] = edgenum++;
}

void init() {
    Bcnt = top = Dindex = edgenum = 0; 
    memset(head, -1, sizeof head);
    memset(DFN, 0, sizeof DFN);
    memset(inStack, false, sizeof inStack);
    memset(Bnum, 0, sizeof Bnum);
}

void tarjan(int u) {
    int v = 0;
    DFN[u] = Low[u] = ++Dindex;
    Stack[++top] = u;
    inStack[u] = true;
    for (int i = head[u]; i != -1; i = edge[i].next) {
        v = edge[i].v;
        if (DFN[v] == 0) {
            tarjan(v);
            Low[u] = Min(Low[u], Low[v]);
        } else if (inStack[v]) 
            Low[u] = Min(Low[u], DFN[v]);
    }
    if (DFN[u] == Low[u]) {
        Bcnt++;
        do {
            v = Stack[top--];
            inStack[v] = false;
            Belong[v] = Bcnt;
            Bnum[Bcnt]++;
        } while (u != v);
    }
}

vector<int> G[MAXN]; 
bool vis[MAXN]; 
int dfs(int s) {
    int maxnum = 0;
    vis[s] = true;
    int size = G[s].size();
    for (int i = 0; i < size; ++i) {
        int v = G[s][i];
        if (!vis[v]) {
            int tmp = dfs(v);
            maxnum = Max(maxnum, tmp);
            vis[v] = false;
        }
    }
    return Bnum[s] + maxnum;
}

int main() {
#ifndef ONLINE_JUDGE
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
#endif
    int n, m, s;
    int caseid = 0;
    while (scanf("%d%d%d", &n, &m, &s) != EOF) {
        init();
        while (m--) {
            int u, v;
            scanf("%d%d", &u, &v);
            addedge(u, v);
        }
        tarjan(s);
        for (int i = 1; i <= Bcnt; ++i) G[i].clear();
        for (int i = 0; i < edgenum; ++i) {
            if (DFN[edge[i].u] == 0) continue;
            int u = Belong[edge[i].u];
            int v = Belong[edge[i].v];
            if (u != v) G[u].push_back(v);
        }
        memset(vis, false, sizeof vis);
        printf("Case %d:\n%d\n", ++caseid, dfs(Belong[s]));
    }
    return 0;
}
View Code

 

 

 

 


免責聲明!

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



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