1. 程式人生 > >tarjan縮點(洛谷P387)

tarjan縮點(洛谷P387)

此題解部分借鑑於九野的部落格

題目分析

  • 給定一個 \(n\) 個點 \(m\) 條邊有向圖,每個點有一個權值,求一條路徑,使路徑經過的點權值之和最大。你只需要求出這個權值和。
  • 允許多次經過一條邊或者一個點,但是,重複經過的點,權值只計算一次。

  • 假如沒有後面這條限制的話,那圖一定是一個無環圖。因為有環的話我可以一直在環上跑,所以答案就沒有一個上界

  • 沒有環的話我萌可以很自然地想到一個 \(O(n)\) 的 拓撲\(dp\) 做法,先做入度為 \(0\) 的點,更新入度不為 \(0\) 的點,把更新後入度為 \(0\) 的點加入佇列裡,繼續做之前的事情

  • 現在考慮有環怎麼做

  • 有一個貪心的思路是,到環上就先把環上的點都走完,再從環上任意一點出發

  • 其實這個環可以看做一個大點,也就是我們今天要介紹的主角 \(\to\) 縮點


tarjan縮點

下面這張圖是從 九野的部落格 那 copy 過來的

  • 把可以互相抵達的點集叫做一個連通分量
  • 最大的那個可以互相抵達的點點集即為強連通分量
  • 特別的,單個的點也可以是強連通分量

比如說 :\(\{ 4, 5 \}\) 是一個聯通分量,而 ${4,5,6 } $ 則是一個強連通分量(一個大點)

tarjan的過程就是通過 dfs 找強連通分量(大點)的過程

對圖dfs一下,遍歷所有未遍歷過的點 ,會得到一個有向樹,顯然有向樹是沒有環的。(注意搜過的點不會再搜

則能產生環的 只有(指向已經遍歷過的點)的邊

我們發現 \(7 \to 3\) (紅邊 / 橫叉邊)這種邊一定不會產生連通分量

而 \(6 \to 4\) (綠邊 / 返祖邊)這種邊一定會產生聯通分量

具體來說:具有父子關係的邊一定會產生聯通分量

我們在dfs的時候需要用一個棧來儲存當前所在路徑上的點(棧中所有點一定是有父子關係的)

我們用一些陣列來表示dfs的過程

int tim, dfn[MAX], low[MAX]

\(dfn[i]\) 表示遍歷到節點 \(i\) 的時間戳(第幾次遍歷)

\(low[i]\) 表示往上可以到達最早的點

初始化 \(dfn[i] = low[i] = ++tim\)

我們可以根據上面過程的步驟寫出以下程式碼

    for(int i = head[u]; i; i = nex[i]) {
        if(dfn[to[i]]) {
            if(instack[to[i]]) {
                if(low[to[i]] < low[u]) {
                    low[u] = low[to[i]];
                }
            }
        } else {
            dfs(to[i]);
            if(low[to[i]] < low[u]) {}
                low[u] = low[to[i]];
            }
        }
    }

假如當前節點 \(u\), \(dfn[u] == low[u]\) 那就說明棧頂元素一直到節點 \(u\) 都屬於一個強聯通分量,感性理解

把棧中元素彈出並且把他們都給標記為同一種顏色(同一個強連通分量)

    if(dfn[u] == low[u]) {
        ++totcol;
        do {
            int v = stk[top];
            col[v] = totcol;
            instack[v] = false;
        } while(stk[top--] != u);
    }

具體實現看程式碼

\(\color {Deepskyblue} {Code}\)

這裡還有一道 tarjan練手好題