一、圖的深度優先遍歷
圖的深度優先遍歷,就是在遍歷到一個點\(x\)時任取一條邊繼續遍歷,直到回溯到\(x\),再考慮走其他的邊。
圖的深度優先遍歷會訪問每個點,每條邊各一次(無向圖正反邊各訪問一次),故時間復雜度為\(O(N+M)\)。
利用圖的深度優先遍歷可以統計圖或樹上的各種信息。
具體實現:
void dfs(int u){
vis[u]=1;
for(int i=first[u];e;e=next[e]){
int v=go[e];
if(vis[v]) continue;
dfs(v);
}
}
1.時間戳
我們對圖按照深度優先遍歷的時候,以每次訪問到的節點的順序標記(也可以理解為vis[u]被標記成1的時候),這樣的標記就是時間戳,通常用\(dfn[ ]\)表示。
具體實現:
void dfs(int u){
vis[u]=1;
dfn[u]=dfn[0]++; //默認節點編號為1~n
for(int i=first[u];e;e=next[e]){
int v=go[e];
if(vis[v]) continue;
dfs(v);
}
}
2.樹的DFS序
當我們對樹進行深度優先遍歷時,我們分別在一個點遞歸開始和遞歸結束(即將回溯)時各記錄一次這個節點的編號,最后產生長度為2N的序列就是樹的DFS序。
特點:
設一個點\(x\)在DFS序中出現的位置分別為\(L,R\),那么[L,R]就是以\(x\)為根結點的子樹的DFS序,這樣就可以把對子樹的操作轉化為在序列上操作。
具體實現:
void dfs(int u){
vis[u]=1;
num[++cnt]=u;
for(int i=first[u];e;e=next[e]){
int v=go[e];
if(vis[v]) continue;
dfs(v);
}
num[++cnt]=u;
}
3.樹的深度
一棵樹的深度就是根結點到葉子節點的最大距離,我們在遍歷每個點時,不斷遞推子節點高度。
最初,根結點的深度為0,每次遍歷都有\(dep[v]=dep[u]+1;\),這樣可以求出每個點的深度。
具體實現:
void dfs(int u){
vis[u]=1;
for(int i=first[u];e;e=next[e]){
int v=go[e];
if(vis[v]) continue;
dep[v]=dep[u]+1;
dfs(v);
}
}
4.樹的重心
正如字面意思,我們要尋找這樣一個節點,使得樹的左右兩邊盡量平衡。平衡意味着我們要找到這樣一個節點:
它最大的子樹的大小要小於任意其他節點的最大子樹的大小。
我們任選一個節點作為我們樹的重心,這時求出該節點最大子樹的大小\(maxsize\),並記錄着個節點為答案。如果之后遍歷的節點能夠使\(maxsize\)減小,那么這個點作為重心一定更優,因此我們更新\(maxsize\),以及最終答案。
具體實現:
void dfs(int u){
vis[u]=1;size[u]=1;//只有一個點的大小為1
for(int i=first[u];e;e=next[e]){
int v=go[e];
if(vis[v]) continue;
dfs(v);
size[u]+=size[v];//在回溯的時候(由下至上)更新根結點的子樹大小
maxsize=max(size[v],maxsize);//目前節點最大子樹的大小,對應"子樹1","子樹2"
}
maxsize=max(maxsize,n-size[u]);//更新其余部分的大小
if(maxsize<ansize){//如果產生更小的最大子樹
ansize=maxsize;//更新最小的最大子樹大小
pos=u;//記錄這個點
}
}
5.樹的直徑
樹的直徑就是一棵樹中任意兩點的最遠距離。要求樹的直徑,可以先從根結點遍歷,找到最遠的一個節點,再由這個節點作為根結點進行遍歷就能求出這棵樹的直徑。我們可以知道,該直徑的兩端點必然是樹的兩個葉子節點。
具體實現:
#include<bits/stdc++.h>
#define N 5000
struct node{
int next,to;
}edge[N];
int num_edge,head[N],dis[N],n,a,b,y;
inline void add_edge(int from,int to){
edge[++num_edge].next=head[from];
edge[num_edge].to=to;
head[from]=num_edge;
}
int dfs(int x){
for(int i=head[x];i;i=edge[i].next)
if(!dis[edge[i].to]){
dis[edge[i].to]=dis[x]+1;
dfs(edge[i].to);
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<n;++i){
scanf("%d%d",&a,&b);
add_edge(a,b);add_edge(b,a);
}
dfs(1);
for(int i=y=1;i<=n;i++) if(dis[i]>dis[y]) y=i;
memset(dis,0,sizeof(dis));
dfs(y);
for(int i=y=1;i<=n;i++) if(dis[i]>dis[y]) y=i;
printf("%d",dis[y]);
return 0;
}
二、圖的廣度優先遍歷
圖的深度優先遍歷會沿着一條邊走並回溯,而廣度遍歷會將該節點能到達的所有的節點全部插入隊列(已訪問過的除外),直到隊列為空。
性質:
- 隊列里只會保存第\(x\)層和第\(x+1\)層的節點。
- 只有第\(x\)層節點遍歷完以后,才會遍歷第\(x+1\)層的節點。
具體實現:
void bfs(){
queue<int> q;
q.push(1);
while(!q.empty()){
int u=q.front();q.pop();
vis[u]=1;
for(int i=first[u];i;i=next[i]){
int v=go[i];
if(vis[v]) continue;
vis[v]=1;
q.push(v);
}
}
}
三、練習
P2986 [USACO10MAR]偉大的奶牛聚集
首先計算所有牛到根結點所用的路程之和,之后依次推出到其他節點的路程。
由原來集合的節點u到節點v所走的路程變化為:
少走了\(cow[u]×dist[u]\) (u的子樹節點的牛的個數×由u到v的路徑長度),多走了\((all-cow[u])*dist[u]\)(其余節點牛的個數×由u到v的路徑長度)。
Code:
#include<bits/stdc++.h>
#define N 200010
#define ll long long
const ll INF=0x3f3f3f3f3f3f3f3f;
using namespace std;
int n,all,c[N];
int tot,first[N<<1],nxt[N],go[N],cost[N];
ll cow[N],Ans=INF,dist[N],sum[N],ans[N];
inline void add_edge(int u,int v,int w){
nxt[++tot]=first[u];
first[u]=tot;
go[tot]=v;
cost[tot]=w;
}
inline void calc_sum(int u,int fath){
cow[u]=c[u];//節點u的子樹中牛的數量
for(int e=first[u];e;e=nxt[e]){
int v=go[e],w=cost[e];
if(v==fath) continue;
dist[v]=w;//記錄u,v的邊權
calc_sum(v,u);
cow[u]+=cow[v];
sum[u]+=sum[v]+w*cow[v];//求出子樹中所有奶牛到節該點的路程
}
}
inline void calc_ans(int u,int fath){
if(u==1) ans[u]=sum[u];//從根結點擴展
else ans[u]=ans[fath]+(all-cow[u])*dist[u]-cow[u]*dist[u];//計算左右牛到節點u的路程
for(int e=first[u];e;e=nxt[e]){
int v=go[e];
if(v==fath) continue;
calc_ans(v,u);
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&c[i]);
all+=c[i];//求出所有奶牛的數量
}
for(int i=1,u,v,w;i<n;i++){
scanf("%d%d%d",&u,&v,&w);
add_edge(u,v,w);
add_edge(v,u,w);
}
calc_sum(1,0);//計算所有牛到根結點的路程
calc_ans(1,0);//求出所有牛到樹上每一個點的路程
for(int i=1;i<=n;i++) Ans=min(Ans,ans[i]);
printf("%lld",Ans);
return 0;
}