1. 程式人生 > 其它 >【全程NOIP計劃】圖論演算法

【全程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