這裏給你們介紹一種奇怪的作法,爲何奇怪呢,由於這玩意兒又不像並查集,又不像拓撲序ios
(實際上是我模擬賽的時候先想的拓撲序,又想的並查集,而後一步步改爲了如今這個樣子)算法
首先,咱們經過題目,發現這是一個有向非聯通圖,且每一個點有且僅有一條出邊。數組
其次,答案只會存在於如下兩種狀況中spa
圖一code
圖二blog
\(3.\;\)通常咱們使用拓撲序時,是要從入度爲 \(0\) 的點開始遍歷,可是顯然下面這張圖不會被遍歷到,那若是從入度爲 \(1\) 的點開始呢?ci
\(4.\;\)那麼這張圖也顯然,只有入度爲 \(0\) 和 \(2\) 的點,以此類推,單純從任何一種入度爲某個值的點開始遍歷是不合適的。string
\(5.\;\)因此咱們仍是考慮最上面兩張基本的圖,圖一帶着個「小尾巴」,必定有入度爲 \(0\) 的點;圖二自己是一個大環,只有入度爲 \(1\) 的點
\(6.\;\)若是一遍不行,爲何不遍歷兩遍呢?思路也就出來了。it
關於實現,我在模擬賽時還發現了兩個小問題io
$Sol: $ 因此咱們在兩次遍歷前,各預處理一下,處理掉沒用的點,以後在剩下的點裏面選起點遍歷就行了。
\(Sol:\) 我用一個數組,記錄一個點是被哪次的 \(DFS\) 遍歷的,另外,因爲我懶,我沒有新開數組,而是用的入度 \(in\) 數組,又防止混淆,我用的負數,下面舉個栗子
第一步,從 \(1\) 開始遍歷, \(1,2,3,4,5,6\) 的 \(in\) 數組均被更新成 \(-1\),第二次從 \(8\) 開始,\(8,7\) 被更新爲 \(-2\),這時遍歷到 \(2\),通過判斷,\(in[7]\not=in[2]\),不更新答案
各變量表示:
\(cnt, tot\) 計數用
\(to\) 每一個點指向的節點
\(vis\) 判斷是否被遍歷過,順便記錄深度
\(in\) 初期是每一個點的入度,後期也變成了一個判斷數組
\(zero\) 記錄入度爲 \(0\) 的點
\(one\) 記錄入度爲 \(1\) 的點
代碼自認爲可讀性較高
#include <iostream> #include <cstdio> #include <cstring> #include <cmath> #include <cstdlib> #include <algorithm> using namespace std; const int N = 200005; int cnt, to[N], n, vis[N], ans = 1000000007, in[N], zero[N], one[N]; void dfs(int k, int tot) { if(vis[to[k]] && in[to[k]] == tot) ans = min(ans, vis[k] + 1 - vis[to[k]]); else if(vis[to[k]] && in[to[k]] != tot) return; else { vis[to[k]] = vis[k] + 1; in[to[k]] = tot; dfs(to[k], tot); } } int main() { // freopen("message.in", "r", stdin); // freopen("message.out", "w", stdout); cin >> n; for(int i = 1; i <= n; i++) { cin >> to[i]; in[to[i]]++; } cnt = 0; for(int i = 1; i <= n; i++) { if(in[i] == 0 && vis[i] == 0) { zero[++cnt] = i; } } int tot = 0; for(int i = 1; i <= cnt; i++) { if(vis[zero[i]] == 0) { vis[zero[i]] = 1; dfs(zero[i], --tot); } } cnt = 0; for(int i = 1; i <= n; i++) { if(in[i] == 1 && vis[i] == 0) { one[++cnt] = i; } } for(int i = 1; i <= cnt; i++) { if(vis[one[i]] == 0) { vis[one[i]] = 1; in[one[i]] = 1; dfs(one[i], 1); } } cout << ans << "\n"; // fclose(stdin); // fclose(stdout); return 0; }