【全程NOIP計劃】圖論演算法
【全程NOIP計劃】圖論演算法
最短路演算法
常用的最短路演算法SPFA,Dijkstra,Floyd演算法
最短路問題,就是對於有權圖的兩個點,找到一條連線兩個點的路徑,使得路徑的權值和最小
在說最短路演算法之前,必須瞭解鬆弛的概念
其實n簡單,如果\(a \rightarrow b+b \rightarrow c\)的距離比\(a \rightarrow c\)的小,那麼就可以用前者代替a到c的距離
各種各樣 最短路實際上就是不斷做鬆弛操作
Floyd
可以求出圖中任意兩點的最短路,過程很簡單
首先列舉鬆弛操作的中間點,再列舉鬆弛的左右兩個點,然後做鬆弛操作
由於Floyd演算法暴力列舉的特性,所以用鄰接矩陣很方便
很顯然,複雜度為\(O(n^3)\)
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
a[i][j]=min(a[i][j],a[i][k]+a[k][j]);
正確性
怎麼證明正確性?
對於任意兩個點之間的最短路,假設有m個節點那麼m一定小於n,因為重複經過同樣的點沒有意義(除非有負環,但是如果有負環最短路就沒有意義,因為可以通過刷負環來刷最短路)
那麼這m個點在外層迴圈都會被列舉一次,接著內層的兩重迴圈一定會列舉到這個點在最短路上相鄰的兩個點
這個點被鬆弛之後,我們就可以認為它已經不在最短路上了,因為此時的\(a \rightarrow b \rightarrow c\)和\(a \rightarrow c\)是一樣的
外層迴圈做完之後,兩個點之間的所有m個點也都被消除完了,這兩個點的距離就是最短路
易錯點
有一個易錯點,就是三個迴圈的順序不要搞錯了
先列舉中間,再列舉兩邊
因為,如果左邊先列舉,那麼n次迴圈之後,鬆弛的點對都是從左邊出發的,如果沒有恰好沿著路徑的順序去列舉中間的點,就無法鬆弛整條路徑
有一個神奇的結論,只要Floyd整個過程連續做三遍,不管三個迴圈是什麼順序,跑出來的結果都是對的,但是不建議使用,主要是慢啊
作用
裸的最短路實際上用的不多,用到了也很簡單,基本上是看做一個工具來用
Floyd實際上還可以處理除了最短路以外其他的問題
P2419 Cow Contest S
思路
拓撲排序可以,但是Floyd也可以
這樣點很少,邊很多的圖就很適合使用Floyd
如果用\(e[i][j]\)表示能不能推出i<j的關係,如果\(e[i][k]=1且e[k][j]=1\),那麼\(e[i][j]=1\)
這樣我們就可以處理出任意兩點間的大小關係,有或者沒有
然後如果對於一個點,我們能確定剩下所有點和它的大小關係
那麼這個點的排名就被確定了
如果一個點,對於其他任何一個點都能推出它的關係,然後它的排名就確定了
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
int n,m;
int e[105][105];
void floyd()
{
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
e[i][j]|=(e[i][k]&e[k][j]);//用floyd來解決直接的關係
}
int main()
{
cin>>n>>m;
int l,r;
while(m--)
{
cin>>l>>r;
e[l][r]=1;
}
floyd();
int cnt=0;
for(int i=1;i<=n;i++)
{
int mark=1;
for(int j=1;j<=n;j++)
if(j!=i&&e[j][i]==0&&e[i][j]==0)//如果有誰不能確定
{
mark=0;
break;
}
cnt+=mark;
}
cout<<cnt<<endl;
return 0;
}
P2888 Cow Hurdles S
思路
讓我想到了營救那道題目
實際上就是二分答案加判斷
但是用Floyd處理dp關係可以更簡單的做這個題目
\(e[i][j]\)表示從i到j經過的路徑中,最小的最大欄杆高度
容易得到類似於Floyd的關係
也就是說如果走\(i \rightarrow k \rightarrow j\)的路線,那麼欄杆的最大高度為\(max(e[i][k],e[k][j])\)
如果這個最大值小於\(i \rightarrow j\)之間的最大值,那麼就可以更新這個最小的最大值
也就是先用\(O(n^3)\)來處理一個Floyd,然後再用\(O(t)\)處理答案
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int INF=0x3f3f3f3f;
const int maxn=305;
int n,m,T;
int a[maxn][maxn];
int main()
{
memset(a,20,sizeof(a));
cin>>n>>m>>T;
for(int i=1;i<=m;i++)
{
int s,e,h;
cin>>s>>e>>h;
a[s][e]=h;
}
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
a[i][j]=min(a[i][j],max(a[i][k],a[k][j]));
while(T--)
{
int l,r;
cin>>l>>r;
if(a[l][r]!=336860180)
cout<<a[l][r]<<'\n';
else
cout<<-1<<'\n';
}
return 0;
}
P2047 社交網路
思路
這道題目和前兩道題目差不多,只不過除了求i到j的路徑數量,還要求i經過k到j的路徑數量
最短路徑比較好求,如果\(e[i][k]和e[k][j]\)更新了\(e[i][j]\)的話,讓\(cnt[i][j]=cnt[i][k]*cnt[k][j]\)就可以了
如果\(e[i][k]+e[k][j]=e[i][j]\)那麼還要讓\(cnt[i][j]+=cnt[i][k]*cnt[k][j]\)
因為資料量過小,所以這樣來表示是可以的,主要運用的是乘法原理
題目個每個點求\(\sum_{s!=v,t!=v}c(s,t,v)c(s,t)\)
那麼就先跑Floyd,然後列舉每個點,再列舉s和t,用\(cnt[s][v]*cnt[v][t]/cnt[s][t]\)計算就可以了
void floyd()
{
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
if(e[i][k]+e[k][j]<e[i][j])
{
e[i][j]=e[i][k]+e[k][j];
cnt[i][j]=cnt[i][k]*cnt[k][j];
}
else if(e[i][j]+e[k][j]==e[i][j])
{
cnt[i][j]+=cnt[i][k]*cnt[k][j];
}
}
}
int main()
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
e[i][j]=INF;
e[i][i]=0;
}
int l,r,v;
while(m-->0)
{
cin>>l>>r>>v;
e[l][r]=v;
e[r][l]=v;
cnt[l][r]=1;
cnt[r][l]=1;
}
floyd();
for(int k=1;k<=n;k++)
{
double ans=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(cnt[i][j]&&e[i][k]+e[k][j]==e[i][j])
ans+=(double)(cnt[i][k]*cnt[k][j]/cnt[i][j]);
printf("%.3lf",ans);
}
}
Bellman-Ford
這是一個單源最短路演算法
也就是能求出從某個點出發到剩下所有點的最短路
這個演算法用\(dis[v]\)表示從s到v的距離
演算法總共進行n-1輪,每輪列舉所有的邊u到v,來做s到u到u的鬆弛操作
不難理解,第一輪會求出和s間距一條邊的最短路,第二輪會求出間距小於等於2條邊的最短路,第n-1輪迴求出間距小於等於n-1條邊的最短路
由於最短路最多有n-1條邊,所以n-1輪之後每個點都求到了真正的最短路
顯然這個演算法的複雜度是\(O(NM)\)的,n是點數,m是邊數
顯然這個演算法適合直接結構體來存邊,也就是邊表
void bellman_ford()
{
for(int i=1;i<=n;i++)
dis[i]=INF;
dis[s]=0;
for(int k=1;k<n;k++)
{
for(int i=1;i<=ecnt;i++)
if(dis[e[i].x]+e[i].v<dis[e[i].y])
dis[e[i].y]=dis[e[i].x]+e[i].v;
}
}
SPFA
上面的一個演算法有一個優化,實際上就是spfa
我們每輪操作其實並不一定要把所有的邊都鬆弛一遍
因為有一些店在上一輪之後最短路並沒有發生變化,那麼從這個點出發的邊做鬆弛也一定沒有變化
所以我們不再列舉n-1輪,而是用一個佇列,表示剛剛發生過變化,準備要進行鬆弛的點
每次從佇列裡拿出一個點,然後列舉這個點的出邊並進行鬆弛,如果出點的值邊了,就把出點也加入佇列
一個進一步的優化用\(flag[v]\)表示點v是否在佇列裡,如果已經在了,那顯然是不用重複進隊的
顯然一開始就應該把s放進佇列,然後從s開始鬆弛
也是用鄰接連結串列來村邊
void spfa()
{
for(int i=1;i<=n;i++)
flag[i]=false;
for(int i=1;i<=n;i++)
dis[i]=INF;
dis[s]=0;
q[++head]=s;
for( ;tail<=head; ++tail)
{
flag[q[tail]]=0;
for(int i=1)
}
}
複雜度
時間複雜度為\(O(VE)\),有的時候並不比Bellman-Ford更快
作用
但是spfa並不是一無是處,當圖中有負環的時候,Dijkstra這種純粹求最短路的演算法就掛了
因為負環實際上意味著圖中沒有最短路
而spfa有三種方式來判斷是否存在負環:
1.用\(cnt[v]\)來表示從s到v的最短路經過了多少個點,如果u到v送出了s到v,那麼就讓\(cnt[v]\)更新為\(cnt[u]+1\),之前提到過,一個正常的最短路不應該有超過n個點的,因此當\(cnt[v]>n\)的時候有內鬼,終止交易
2.統計進隊次數,一個點如果入隊大於n次
3.dfs班也很簡單,如果一個點被鬆弛了,就直接遞迴進這個點去鬆弛別人就可以了。如果一個點遞迴了一圈又回到自己了,顯然有負環。一般第二種方法比第一種方法快一點,而第三種方法比前兩種都要快。spfa判負環只能判有沒有,不能找哪裡,如果要找哪裡,要用tarjan演算法
P3199 最小圈
思路
問題實際上是求\(C=\sum w[i]/k\),其中i是邊,\(w[i]\)是邊權,k是邊數
問題顯然存在二分單調性,也就是如果答案太大,那麼不符合最小的要求,但是一定可以找出來一個圈,使得比值小於等於這個答案,如果答案太小,那麼一定找不到一個圈,使得比值滿足這個答案
那麼就可以二分答案ans,如果不存在\(\sum w[i]/k \le ans\),則答案小,否則答案大
把這個式子變一下,就得到\(\sum w[i]-k*ans=\sum(w[i]-ans) \le 0\)
需要注意,負圈不一定是要每條邊都小於0,而是隻要權值和小於0的就可以刷最短路
所以滿足\(\sum(w[i]-ans)\le 0\)的圈實際上就是一個負圈,因為答案是浮點數,所以$<0和 \le0 $的區別不大
那麼枚舉出答案ans後給每個邊權都減去ans,然後spfa判負環就可以了
這個實際上是一個01分數規劃的過程
複雜度上線為\(O(nmlogw)\),比較危險
Dijkstra
只要出現單源最短路一定要卡spfa的今天,單源最短路的最佳解法就是Dijkstra
實際上如果用stl的優先佇列來寫,Dijkstra也不會比spfa複雜到哪裡去
Dijkstra其實就是一個堆優化的spfa
spfa每次是從已經準備要鬆弛別人的點中選出某一個,而Dijkstra則是直接使用優先佇列貪心地從中選出dis最小的那個
至於這個貪心的全域性最優的證明,可以使用歸納法嚴格證明
每條邊至多被訪問一次,所以每個點的鬆弛次數不會超過邊數
所以Dijkstra的時間複雜度為\(O(MlogN)\)
另一種理解
Dijkstra的思路其實是每次從dis中挑出dis最短的那個,之前沒有被挑過的點出來鬆弛
Dijkstra實際上有一個\(O(nm)\)的做法,就是直接用for迴圈去找這個最短的的點,這也是為什麼會有堆優化的Dijkstra的這種說法
本質上是用堆來維護dis陣列的,但是要注意
不能直接對dis建堆,然後隨著鬆弛操作改堆裡的資料
如果直接改堆的資料,會破壞堆的結構,導致堆不能完成它應有的功能
因此還要沒鬆弛一次就把出點入堆,然後出堆的時候去更新dis
void dijkstra()
{
for(int i=1;i<=n;i++)
dis[i]=INF;
dis[s]=0;
size=0;
push((nod){x,0});
while(size)
{
while(size&&heap[1].y>dis[heap[1].x]) pop();
if(!size) break;
x=heap[1].x;
dis[x]=heap[1].y;
pop();
for(int i=link[x];i;i=e[i].next)
if(dis[x]+e[i].v<dis[e[i].y])
{
dis[e[i].y]=dis[x]+e[i].v;
push((node){e[i].y,dis[e[i].y]});
}
}
}
P2837 Milk Pumping G
思路
這個題看上去有二分單調性,又涉及到分數,似乎是分數規劃
實際上並沒有這麼複雜,因為n很小,流量的上線也很小
所以直接列舉路徑的流量,流量確定之後最大化流量/花費,實際上就是最小化花費
於是就可以跑Dijkstra,要求流量大於等於列舉的流量的邊才能參與鬆弛就行了
int dijkstra(int y)
{
for(int i=1;i<=n;i++)
dis[i]=INF;
dis[1]=0;
size=0;
push((nod){1,0});
while(size)
{
while(size&&heap[1].y>dis[heap[1].x]) pop();
if(!size) break;
x=heap[1].x;
dis[x]=heap[1].y;
pop();
for(int i=link[x];i;i=e[i].next)
if(e[i].w>=y&&dis[x]+e[i].v<dis[e[i].y])
{
dis[e[i].y]=dis[x]+e[i].v;
push((node){e[i].y,dis[e[i].y]});
}
}
return dis[n];
}
int main()
{
……
dounble ans=0;
for(int i=1;i<=1000;i++)
ans=max(ans,(double)i/dij(i));
printf("%lld\n",(long long)(ans*1e6));
……
}
最短路建模
差分約束
差分約束系統就是給你一堆變數,然後給你一堆形如\(a_i-a_j \le c\)的不等式
像這樣兩個變數相減的形式就叫做差分,一堆變數相減就是差分系統
差分約束能告訴你差分系統中,任意兩個變數最多是多少,或者是最少是多少
做法
如果有a1-a2<=b,a2-a3<=d,a1-a3<=e
假設要求a1-a3的範圍,我們發現除了a1-a3<=e的條件之外,還有a1-a2+a2-a3=a1-a3<=b+d
那麼如果b+d<=e,a1-a3就<=b+d,否則a1-a3<=e
發現了沒,這個操作和最短路的鬆弛操作特別像
我們用\(e[i][j]\)來表示\(a_i-a_j\le e[i][j]\),那麼不難發現,對於一箇中間點k,我們有\(e[i][j]=min(e[i]][j],e[i][k]+e[k][j])\),於是我們就用一個最短路的模型表示一個差分約束系統,然後就可以用最短路演算法解出想要的變數的差分關係
有的人就會問,如果同時出現左邊右邊符號顛倒的情況怎麼辦,一般就可以把符號和兩個數的位置變化一下,一般不會出現變化不了的情況
還有一種題型就是判斷有沒有解,我們要考慮什麼情況下無解,情況有很多,但是可以轉化為一個情況,就是a-b<=c且a-b>=d而且c<d,可以看成c-d<0,這就意味著一條迴圈約束出現了負環,如果常規差分約束建完圖之後有負環,那麼就無解了
模板程式碼
#include <iostream>#include <cstdio>#include <algorithm>#include <cstring>#include <string>#include <queue>#define int long long using namespace std;const int maxn=50005;struct edge{ int e,next,val;}ed[maxn*2];int en,first[maxn];void add_edge(int s,int e,int val){ en++; ed[en].next=first[s]; first[s]=en; ed[en].e=e; ed[en].val=val;}int n,m;int d[maxn],num[maxn];bool vis[maxn];queue <int> q;bool spfa(int x){ d[x]=0; q.push(x); vis[x]=true; num[x]++; while(q.size()) { int x=q.front(); q.pop(); vis[x]=false; for(int i=first[x];i;i=ed[i].next) { int e=ed[i].e,val=ed[i].val; if(d[e]>d[x]+val) { d[e]=d[x]+val; if(!vis[e]) { q.push(e); vis[e]=true; num[e]++; if(num[e]==n+1) return false; } } } } return true; }signed main(){ cin>>n>>m; for(int i=1;i<=n;i++) d[i]=2147483647; for(int i=1;i<=m;i++) { int x,y,z; cin>>x>>y>>z; add_edge(y,x,z); } for(int i=1;i<=n;i++) add_edge(n+1,i,0); if(!spfa(n+1)) { cout<<"NO"<<'\n'; goto end; } for(int i=1;i<=n;i++) cout<<d[i]<<" "; end: ; return 0;}
P1993 小k的農場
思路
直接差分約束系統,判斷是否有解就可以了
#include <iostream>#include <cstdio>#include <algorithm>#include <cstring>#include <string>#include <queue>#define int long long using namespace std;const int maxn=50005;struct edge{ int e,next,val;}ed[maxn*2];int en,first[maxn];void add_edge(int s,int e,int val){ en++; ed[en].next=first[s]; first[s]=en; ed[en].e=e; ed[en].val=val;}int n,m;int d[maxn],num[maxn];bool vis[maxn];queue <int> q;bool spfa(int x){ d[x]=0; q.push(x); vis[x]=true; num[x]++; while(q.size()) { int x=q.front(); q.pop(); vis[x]=false; for(int i=first[x];i;i=ed[i].next) { int e=ed[i].e,val=ed[i].val; if(d[e]>d[x]+val) { d[e]=d[x]+val; if(!vis[e]) { q.push(e); vis[e]=true; num[e]++; if(num[e]==n+1) return false; } } } } return true; }signed main(){ cin>>n>>m; memset(d,0x3f,sizeof(d)); for(int i=1;i<=m;i++) { int op; cin>>op; if(op==1) { int a,b,c; cin>>a>>b>>c; add_edge(a,b,-c); } else if(op==2) { int a,b,c; cin>>a>>b>>c; add_edge(b,a,c); } else { int a,b; cin>>a>>b; add_edge(a,b,0); add_edge(b,a,0); } } for(int i=1;i<=n;i++) add_edge(n+1,i,0); if(!spfa(n+1)) { cout<<"No"<<'\n'; return 0; } cout<<"Yes"<<'\n'; return 0;}
P3275 糖果
思路
跟上一道題目一樣差不多,處理一下差分約束系統
a不比b少就是a>=b
a比b少怎麼辦?
實際上就是a<b等價於a<=b-1
於是a比b少表示為b-a>=1
現在約束能建了,問題是要求總數最小
可以建立一個抽象節點,表示0顆糖的基準線
然後抽象點往所有點約束為1的邊,表示每人至少分一顆糖
這裡需要注意,由於求的是最小值,約束是a-b>=c,因此這裡求的是最長路,要找出最大的那個下限
最後所有人dis就是不得不滿足的下限,然後就能得到答案了
傳遞閉包
傳遞閉包的數學概念比較抽象,不太好懂,但是做出來很簡單
簡單說就是,兩個關係i-->k和k-->j可以複合出一個新關係i-->j,這就是傳遞性
給你一個集合,集合裡定義了一個關係i-->j,再定義一個關係i-->j,滿足:
1.對於所有滿足i-->j,使得i能通過-->的複合關係連線到j,但是不滿足i-->j
2.除此之外不存在i和j,使得i能通過-->的複合關係連結到j,但是不滿足i-->j
精簡版說法,就是傳遞閉包是關係的極大生成集,因為
1.如果a是b的祖先,那麼a肯定是b的父母的父母的父母的……
2.不存在如果a是b父母的父母的父母的……,我們就不能稱a為b的祖先的情況
實際上還是有點難理解,但是直接看怎麼做吧
對於一個鄰接矩陣e,\(e[i][j]=0\)不滿足關係i-->j,\(e[i][j]=1\)表示滿足關係i-->j
然後我們對e做與操作的Floyd,也就是鬆弛操作為\(e[i][j]=e[i][k] and[k][j]\)
就這?對,我們之前實際上遇到了一個差不多的例題
強連通分量
強連通分量指的是圖的一個極大的子圖,滿足圖內任意兩點內任意兩點之間可以相互到達
需要注意的是,強連通分量的概念是針對有向圖的,無向圖沒有強連通分量的說法,因為對於無向圖,只要是一個連通圖就能任意互相到達
強連通分量分成兩個部分第一個是任意兩個點可以相互到達,這個比較好理解
比如完全圖,也就是任意兩個點都連線兩個方向的邊
再比如一個有向環,也滿足這個條件
而極大的子圖就是說,再加入原圖中的任意一個點以及這個點的邊到這個子圖內,都不能滿足互相到達的條件
比如一個完全圖子完全圖,雖然能互相到達,但是不是極大的
Tarjan演算法
求一個圖的強連通分量一般用tarjan演算法,這裡的tarjan演算法只指dfs樹,也就是dfn-low的那一套理論
首先對於任意一個有向圖,我們顯然可以用dfs來遍歷整個圖,每個點只經過一次,並不用管所有點都經過沒,那麼按照dfs遞迴的關係,把遍歷過程畫出來,就是一個dfs樹
tarjan在dfs樹的基礎上定義了dfn和low的概念
dfn就是dfs過程被訪問到的順序
而low表示的是這個點能到達的所有點中,dfn最小的點
tarjan演算法首先用一個棧按順序記錄dfs遍歷過的點,同時計算dfn和low,每當發現一個點的dfn等於low,那麼就把這個點以及棧中之後的所有點彈出來,作為一個強連通分量
void tarjan(int x,int fa){ vis[x]=true; dfn[x]=low[x]=++dfs_cnt; s[++top]=x; for(int i=link[x];i;i=e[i].next) { if(!dfn[e[i].y]) { tarjan(e[i].y,x); low[x]=min(low[x],low[e[i].y]); } else if(vis[e[i].y]) low[x]=min(low[x],dfn[e[i].y]); } if(dfn[x]==low[x]) { grouP_cnt++; int temp; do{ temp=s[top--]; group[temp]=group_cnt; vis[temp]=false; }while(temp!=x) }}
正確性
為什麼 ?
對於子圖的一個點,如果這個點可以到達任意一個點,而任意一個點也可以到達這個點,我們就說這個子圖是任意兩點互相到達的
在tarjan演算法中,這個點就是dfn等於low的點
我們注意到,在tarjan演算法中,這個點就是dfn等於low的點
我們注意到,在tarjan演算法中,只要\(dfn[x]=low[x]\),那麼x就會被彈出來,因此對於一個點x的子樹中的點y,只有兩種情況:要麼已經被彈出來了,要麼\(low[y]=low[x]\)
不可能\(low[y]<low[x]\),因為x可以到y,所以y能到的x也能到,那麼應該\(low[x]\le low[y]\)才對
而如果\(low[y]>low[x]\),那麼到回溯到\(low[y]\)對應的那個點的時候,y就被彈出棧了
所以棧裡面留下來的也一定都能到達x
同時顯然x也能到達它的子節點們
因此當\(dfn[x]=low[x]\)的時候,我們就說x的子樹內任意互達
同時這些子樹上的點往上最多隻能到達x,到不了子樹外面的點
因此把任意子樹外的點加進來,都會破壞任意互達的條件
再來看為\(dfn[y]=low[y]\)而被提前彈出棧的的y的子樹的點
這些碘同樣往外最多隻能到達y,到達不了x,因此把y子樹中的點加進來也不能滿足任意互達的條件
所以tarjan演算法找到的點的集合,當然也包括這些點之間的邊的集合,是強聯通分量
P2341 最受歡迎的牛
思路
我們發現對於一個強聯通分量內的牛的機會是相同的,要麼一起當明星,要麼都當不了明星
因為團體內的任意一頭牛會愛慕別的牛,如果團體外的所有牛都愛它,那麼就會傳遞到剩下的所有牛的身上
因此我們可以先做tarjan縮點,把一個強連通分量的牛看成一個整體來考慮
原來牛之間的邊要轉化為團體間的邊
這是我們發現,縮點之後的圖變成了一個DAG
前面我們提到過,環是強連通分量,因此如果圖中還有環,顯然還可以繼續縮點
因此把所有強連通分量都縮完的圖一定是一個DAG
如果整個DAG只有一個點的出度為n,那麼顯然所有的點都可以到達這個點
所以當我們縮完點之後發現DAG中只有一個點出度為0,那麼我們說這個點中的所有的奶牛都可以當明星
本博文為wweiyi原創,若想轉載請聯絡作者,qq:2844938982