1. 程式人生 > 實用技巧 >演算法初探 - 縮點

演算法初探 - 縮點

更新記錄

【1】2020.08.08-17:59

  • 1.完善內容

正文

在一些特殊的問題中,例如在一有向有環圖中求最長路徑,點(邊)可以重複經過,但是點權(邊權)只算一次
(要是無限算不沿著環瞎就就彳亍嘛)
容易想到,在遇到環的時候我們要將這個環走一遍以獲取最大值

那麼縮點就是將環縮成一個點,然後進行DAG上的動態規劃

我們回想強連通分量的定義:

在有向圖G中,如果兩個點u,v間有一條從u到v的有向路徑,同時還有一條從v到u的有向路徑,則稱兩個點強連通
如果有向圖G的每兩個點都強連通,稱G是一個強連通圖
有向非強連通圖的極大強連通子圖,稱為強連通分量。

也就是說強連通分量一定是一個環

這樣我們第一步就通過tarjan演算法找到強連通分量

就可以

\(dfn\)代表搜尋的\(dfs\)
\(low\)代表在中所能追溯到的最小(早)的\(dfs\)

那麼這個棧是幹啥呢,讓我們按演算法的順序一步一步來

[演算法開始]

遍歷每個點的出邊對應的點,如果沒有搜尋過就先搜尋出邊所對應的點
搜尋後,記最小的\(low\)

如果搜尋過且在棧中,就說明這兩個點可以互相到達,記最小的\(low\)

如果其\(dfs\)序和\(low\)相等,就說明遍歷完畢,並且這個點不能從其他的地方走過來
此時棧中的\(n\)上面的點(包括\(n\)就是一個強連通分量

出棧,統計

inline void tarjan(int n){
	dfn[n]=++time;
	low[n]=time;
	stk[++stks]=n;
	for(int i=head[n];i;i=e[i].na){
		if(!dfn[e[i].np]){
			tarjan(e[i].np);
			low[n]=min(low[n],low[e[i].np]);
		}
		else if(!vis[e[i].np])
			low[n]=min(low[n],dfn[e[i].np]);
	}
	if(dfn[n]==low[n]){
		vis[n]=++tot;
		while(stk[stks]!=n){
			stnum[tot]+=1;
			vis[stk[stks]]=tot;
			stks-=1;
		}
		stnum[tot]+=1;
		stks-=1;
	}
}

【模板】縮點

將環縮為點之後重建圖

此時的圖是一個有向無環

拓撲排序跑一個DAG上的dp就好

縮點之前

縮點之後

#include<iostream>
#include<queue>
#include<cstring>
#define N 200001
using namespace std;
struct Edge{
	int na,np;
}e[N],dag[N];
int head[N],head2[N],num2,num,now,n,m,fr,to,w[N],vis[N],dfn[N],lian[N],ru[N],low[N],times,stnum[N],stk[N],stks,maxn;
queue<int>q;
inline void add(int f,int t){
	e[++num].na=head[f];
	e[num].np=t;
	head[f]=num;
}
inline void add2(int f,int t){
	dag[++num2].na=head2[f];
	dag[num2].np=t;
	head2[f]=num2;
}
inline void tarjan(int n){
	dfn[n]=++times;
	low[n]=times;
	stk[++stks]=n;
	for(int i=head[n];i;i=e[i].na){
		if(!dfn[e[i].np]){
			tarjan(e[i].np);
			low[n]=min(low[n],low[e[i].np]);
		}
		else if(!vis[e[i].np]){
			low[n]=min(low[n],dfn[e[i].np]);
		}
	}
	if(dfn[n]==low[n]){
		vis[n]=n;
		while(stk[stks]!=n){
			stnum[n]+=w[stk[stks]];
			vis[stk[stks]]=n;
			stks-=1;
		}
		stnum[n]+=w[stk[stks]];
		stks-=1;
	}
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>w[i];
	for(int i=1;i<=m;i++){
		cin>>fr>>to;
		add(fr,to);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]) tarjan(i);
	}
	for(int i=1;i<=n;i++){
		for(int o=head[i];o;o=e[o].na){
			if(vis[i]!=i&&vis[e[o].np]!=e[o].np) continue;
			if(vis[i]==vis[e[o].np]) continue;
			add2(vis[i],vis[e[o].np]);
			ru[vis[e[o].np]]+=1;
		}
	}
	for(int i=1;i<=n;i++){
		if(!ru[i]){
			q.push(i),lian[i]=stnum[i];
			maxn=max(maxn,stnum[i]);
		}
	}
	while(q.size()){
		now=q.front();
		q.pop();
		for(int i=head2[now];i;i=dag[i].na){
			ru[dag[i].np]-=1;
			lian[dag[i].np]=max(lian[dag[i].np],lian[now]+stnum[dag[i].np]);
			maxn=max(maxn,lian[dag[i].np]);
			if(!ru[dag[i].np]){
				q.push(dag[i].np);
			}
		}
	}
	cout<<maxn;
}

[USACO03FALL][HAOI2006]受歡迎的牛 G

首先讀題,整理資訊可知:

  • 環上的牛互相喜歡
  • 能被所有牛喜歡的牛肯定是沒有出邊

由性質一可知我們可以進行縮點

重建圖之後是一個有向無環圖
由性質二可知能當明星的牛肯定是有向無環圖的終點

終點也可能是縮點而得來的,所以要檢查終點所對應的原來的強連通分量

#include<iostream>
#include<queue>
#include<cstring>
#define N 200001
using namespace std;
struct Edge{
	int na,np;
}e[N],dag[N];
int head[N],head2[N],num2,num,now,n,m,fr,to,w[N],vis[N],dfn[N],out,lian[N],chu[N],low[N],times,stnum[N],stk[N],stks,maxn;
queue<int>q;
inline void add(int f,int t){
	e[++num].na=head[f];
	e[num].np=t;
	head[f]=num;
}
inline void add2(int f,int t){
	dag[++num2].na=head2[f];
	dag[num2].np=t;
	head2[f]=num2;
}
inline void tarjan(int n){
	dfn[n]=++times;
	low[n]=times;
	stk[++stks]=n;
	for(int i=head[n];i;i=e[i].na){
		if(!dfn[e[i].np]){
			tarjan(e[i].np);
			low[n]=min(low[n],low[e[i].np]);
		}
		else if(!vis[e[i].np]){
			low[n]=min(low[n],dfn[e[i].np]);
		}
	}
	if(dfn[n]==low[n]){
		vis[n]=n;
		while(stk[stks]!=n){
			stnum[n]+=1;
			vis[stk[stks]]=n;
			stks-=1;
		}
		stnum[n]+=1;
		stks-=1;
	}
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		cin>>fr>>to;
		add(fr,to);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]) tarjan(i);
	}
	for(int i=1;i<=n;i++){
		for(int o=head[i];o;o=e[o].na){
			if(vis[i]==vis[e[o].np]) continue;
			chu[vis[i]]+=1;
		}
	}
	for(int i=1;i<=n;i++){
		if(!chu[i]&&vis[i]==i){
			if(out){
				cout<<"0";return 0;
			}
			out=stnum[vis[i]];
		}
	}
	cout<<out;
}

訊息擴散

這道題太水了所以就只寫個思路:

統計入度為0的點的個數

結束了

[Wind Festival]Running In The Sky

這道題我給個好評
首先亮度和是很簡單的,跑個DAG dp就可以了

重要的是路徑上最大的亮度值

不要以為跑dp的時候順便取個\(maxn\)最大值就行了,看題

如果有多條符合條件的路徑,輸出能產生最大單隻風箏亮度的答案

也就是說在保證了第一問的前提下才能選最大值

我們很容易想到:在跑dp的時候順便將每個點的最大值都記錄下來
然後呢?

然後我們用一個變數將最大值答案位置記錄下來

這個位置不是隨便更新的,只有在總和最大值更新的時候才能更新

這裡的更新是指:
現在的總和值大於等於原來的總和值

跑完了這些,答案也就呼之欲出了

注意

  • 在初始化dp壓佇列的時候也要統計
#include<iostream>
#include<queue>
#include<cstring>
#define N 1000001
using namespace std;
struct Edge{
	int na,np;
}e[N],dag[N];
int head[N],head2[N],num2,num,now,n,m,fr,to,w[N],vis[N],dfn[N],lian[N],lian2[N],maxst[N],ru[N],low[N],times,stnum[N],stk[N],stks,maxn,wei,maxn2;
queue<int>q;
inline void add(int f,int t){
	e[++num].na=head[f];
	e[num].np=t;
	head[f]=num;
}
inline void add2(int f,int t){
	dag[++num2].na=head2[f];
	dag[num2].np=t;
	head2[f]=num2;
}
inline void tarjan(int n){
	dfn[n]=++times;
	low[n]=times;
	stk[++stks]=n;
	for(int i=head[n];i;i=e[i].na){
		if(!dfn[e[i].np]){
			tarjan(e[i].np);
			low[n]=min(low[n],low[e[i].np]);
		}
		else if(!vis[e[i].np]){
			low[n]=min(low[n],dfn[e[i].np]);
		}
	}
	if(dfn[n]==low[n]){
		vis[n]=n;
		while(stk[stks]!=n){
			stnum[n]+=w[stk[stks]];
			maxst[n]=max(maxst[n],w[stk[stks]]);
			vis[stk[stks]]=n;
			stks-=1;
		}
		stnum[n]+=w[stk[stks]];
		maxst[n]=max(maxst[n],w[stk[stks]]);
		stks-=1;
	}
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>w[i];
	for(int i=1;i<=m;i++){
		cin>>fr>>to;
		add(fr,to);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]) tarjan(i);
	}
	for(int i=1;i<=n;i++){
		for(int o=head[i];o;o=e[o].na){
			if(vis[i]!=i&&vis[e[o].np]!=e[o].np) continue;
			if(vis[i]==vis[e[o].np]) continue;
			add2(vis[i],vis[e[o].np]);
			ru[vis[e[o].np]]+=1;
		}
	}
	for(int i=1;i<=n;i++){
		if(!ru[i]){
			q.push(i);
			lian[i]=stnum[i];
			lian2[i]=maxst[i];
			if(maxn<stnum[i]){
				maxn=stnum[i];
				wei=i;
			}
			else if(maxn==stnum[i]){
				wei=(lian2[wei]>=lian2[i]?wei:i);
			}
		}
	}
	while(q.size()){
		now=q.front();
		q.pop();
		for(int i=head2[now];i;i=dag[i].na){
			ru[dag[i].np]-=1;
			lian[dag[i].np]=max(lian[dag[i].np],lian[now]+stnum[dag[i].np]);
			lian2[dag[i].np]=max(lian2[now],maxst[dag[i].np]);
			if(maxn<lian[dag[i].np]){
				maxn=lian[dag[i].np];
				wei=dag[i].np;
			}
			else if(maxn==lian[dag[i].np]){
				wei=(lian2[wei]>=lian2[dag[i].np]?wei:dag[i].np);
			}
			if(!ru[dag[i].np]) q.push(dag[i].np);
		}
	}
	cout<<maxn<<" "<<lian2[wei];
}

技巧

縮點其實沒啥技巧,就是我發現\(stnum\)這個陣列原本是記錄某個強連通分量集合中強連通分量的點的個數
但是在某些題目中這個陣列是沒用的
我們可以用這個陣列來記錄這個強連通分量所有點權(邊權)的和