Luogu P3387
強連通份量的定義以下:算法
有向圖強連通份量:在有向圖G中,若是兩個頂點vi,vj間(vi>vj)有一條從vi到vj的有向路徑,同時還有一條從vj到vi的有向路徑,則稱兩個頂點強連通(strongly connected)。若是有向圖G的每兩個頂點都強連通,稱G是一個強連通圖。有向圖的極大強連通子圖,稱爲強連通份量(strongly connected components)。數組
來源於百度百科spa
我本人的理解:有向圖內的一個不能再拓展得更大的強連通子圖叫作這個有向圖的一個強連通份量(也能夠說是一個環)
注意:單獨的一個孤立的點也會是一個強連通份量
求出強連通份量之後有什麼用呢?
很顯然,咱們能夠把整個強連通份量做爲單獨的一個點,權值按照題目要求取(在這題就是取全部點權的總和),這樣就可讓這一個有向有環圖轉化成一個有向無環圖。code
Tarjan算法(在這裏指Tarjan對於強連通份量提出的算法)就是用於求出一個有向圖內的全部強連通份量的有效算法。
基本思想就是利用DFS往下搜索,標記順序,若是找到返回祖先的一條邊,則說明會構成一個環(強連通份量)。
這裏要引入幾個Tarjan算法必備的數組
| 數組名 |做用 |
|--|--|
| dfn[i] | 用於記錄節點i的dfs序 |
|stk[i]|一個棧,用於記錄當前搜索的這一條鏈上的節點|
| low[i]| 用於記錄節點i能訪問到的節點中最小的dfs序 ,也就是最上層的祖先 |component
關鍵點:若是dfn[i]==low[i],意味着在節點i的子樹中沒有任何的節點能夠訪問到節點i的祖先,說明節點i與仍然在棧內子節點(必要條件)構成了一個強連通份量
不在棧內的子節點沒法與節點i構成強連通份量,緣由是不在棧內則說明它自己已經做爲一個強連通份量被彈出棧了。排序
void tarjan(int now) { dfn[now]=++tim;//記錄dfs序 low[now]=tim;//當前能訪問到dfs序最小的點就是本身 stk[++cnt]=now; vis[now]=true; for (int i=head[now];i;i=e[i].nxt) { int to=e[i].to; if (!dfn[to]) { tarjan(to); low[now]=min(low[now],low[to]); //若是該點沒被遍歷過,那麼就進行遍歷。 } else { if (vis[to]) low[now]=min(low[now],dfn[to]); //必須判斷是否在棧中。只有在同時在棧內的點纔有可能構成強連通份量。 } } if (low[now]==dfn[now]) { tot++;//強連通份量的編號 while (stk[cnt]!=now) { scc[stk[cnt]]=tot; val[tot]+=a[stk[cnt]]; vis[stk[cnt]]=false; cnt--; } scc[stk[cnt]]=tot; val[tot]+=a[stk[cnt]]; vis[stk[cnt]]=false; cnt--; //將棧中比u後進入的點和u自己出棧,這些點構成一個強聯通份量,打上標記 } }
結合代碼進行理解。隊列
對一個有向無環圖(Directed Acyclic Graph簡稱DAG)G進行拓撲排序,是將G中全部頂點排成一個線性序列,使得圖中任意一對頂點u和v,若邊<u,v>∈E(G),則u在線性序列中出如今v以前。一般,這樣的線性序列稱爲知足拓撲次序(Topological Order)的序列,簡稱拓撲序列。簡單的說,由某個集合上的一個偏序獲得該集合上的一個全序,這個操做稱之爲拓撲排序。get
來源於百度百科io
我我的的理解:對有向無環圖中全部節點排序造成一個合法的訪問次序。class
作一個比喻:吃飯以前要端盤子,端盤子以前要炒菜——那麼對於這三件事的拓撲次序就是炒菜→端盤子→吃飯。
那麼具體應該如何處理呢?
事實上有兩種實現方法,可是我我的暫時只會一種。
利用隊列的方式,把全部入度爲0的點入隊,而後把這幾個點對其餘點入度的貢獻刪除,而後再把入度爲0的點入隊,直到排序完成爲止。
完成拓撲排序後就能夠利用拓撲次序進行動態規劃了。
#include<cstdio> #include<queue> using namespace std; queue<int> que; struct data { int sta,to,nxt; }e[500005],ine[500005],oute[500005]; int dfn[100005],tim,low[100005],stk[100005],cnt,cnti,cnto,head[100005],inhead[100005],outhead[100005]; bool vis[10005]; int tot,scc[100005],val[100005],a[100005],n,in[100005],order[100005],m,u,v,f[100005],ans; void tarjan(int now) { dfn[now]=++tim;//記錄dfs序 low[now]=tim;//當前能訪問到dfs序最小的點就是本身 stk[++cnt]=now; vis[now]=true; for (int i=head[now];i;i=e[i].nxt) { int to=e[i].to; if (!dfn[to]) { tarjan(to); low[now]=min(low[now],low[to]); //若是該點沒被遍歷過,那麼就進行遍歷。 } else { if (vis[to]) low[now]=min(low[now],dfn[to]); //必須判斷是否在棧中。只有在同時在棧內的點纔有可能構成強連通份量。 } } if (low[now]==dfn[now]) { tot++;//強連通份量的編號 while (stk[cnt]!=now) { scc[stk[cnt]]=tot; val[tot]+=a[stk[cnt]]; vis[stk[cnt]]=false; cnt--; } scc[stk[cnt]]=tot; val[tot]+=a[stk[cnt]]; vis[stk[cnt]]=false; cnt--; //將棧中比u後進入的點和u自己出棧,這些點構成一個強聯通份量,打上標記 } } void topo()//拓撲排序 { cnt=0,cnti=0,cnto=0; for (int i=1;i<=n;i++) { for (int j=head[i];j;j=e[j].nxt) { if (scc[i]!=scc[e[j].to]) { oute[++cnto].to=scc[e[j].to]; oute[cnto].nxt=outhead[scc[i]]; outhead[scc[i]]=cnto; in[scc[e[j].to]]++; ine[++cnti].sta=scc[i]; ine[cnti].nxt=inhead[scc[e[j].to]]; inhead[scc[e[j].to]]=cnti; //out前綴的變量是出邊的記錄 //in前綴的變量是入邊的記錄,使用了一種另類的鏈式前向星 } } } for (int i=1;i<=tot;i++) if (in[i]==0) que.push(i);//入度爲零則入隊 cnt=0; while (!que.empty()) { int u=que.front(); que.pop(); order[++cnt]=u;//記錄順序 for (int i=outhead[u];i;i=oute[i].nxt) { int v=oute[i].to; in[v]--; if (in[v]==0) que.push(v); } } } int main() { scanf("%d%d",&n,&m); for (int i=1;i<=n;i++) scanf("%d",&a[i]); for (int i=1;i<=m;i++) { scanf("%d%d",&u,&v); e[i].to=v; e[i].nxt=head[u]; head[u]=i; //鏈式前向星存原圖 } tim=0; for (int i=1;i<=n;i++) if (!dfn[i]) tarjan(i); //若是沒有被遍歷過的點要繼續遍歷。 topo(); for (int i=1;i<=tot;i++) { f[order[i]]=val[order[i]]; for (int j=inhead[order[i]];j;j=ine[j].nxt) f[order[i]]=max(f[order[i]],f[ine[j].sta]+val[order[i]]); //很容易的一個動態規劃 } for (int i=1;i<=tot;i++) ans=max(f[i],ans);//統計答案 printf("%d",ans); return 0; }