演算法初探 - 縮點
更新記錄
【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\)這個陣列原本是記錄某個強連通分量集合中強連通分量的點的個數
但是在某些題目中這個陣列是沒用的
我們可以用這個陣列來記錄這個強連通分量所有點權(邊權)的和