0x63.樹的直徑與最近公共祖先

聲明:
本系列博客是《算法競賽進階指南》+《算法競賽入門經典》+《挑戰程序設計競賽》的學習筆記,主要是因爲我三本都買了 按照《算法競賽進階指南》的目錄順序學習,包含書中的少部分重要知識點、例題解題報告及我個人的學習心得和對該算法的補充拓展,僅用於學習交流和複習,無任何商業用途。博客中部分內容來源於書本和網絡(我儘量減少書中引用),由我個人整理總結(習題和代碼可全都是我自己敲噠)部分內容由我個人編寫而成,如果想要有更好的學習體驗或者希望學習到更全面的知識,請於京東搜索購買正版圖書:《算法競賽進階指南》——作者李煜東,強烈安利,好書不火系列,謝謝配合。


下方鏈接爲學習筆記目錄鏈接(中轉站)

學習筆記目錄鏈接


ACM-ICPC在線模板


一、樹的直徑(Diameter)

樹上兩點的距離定義爲,從樹上一點到另一點所經過的權值

當樹上兩點距離最大時,就稱作樹的直徑,樹的直徑既可以指這個權值,也可以指這個路徑 (路徑也叫樹的最長鏈)。

樹的直徑有兩種方法,都是O(n)O(n)的時間複雜度。

1.樹形DP求樹的直徑

設以1號結點爲根,那麼n個結點n-1條邊的無向圖就可以看作一個有根樹。
D[x]D[x]表示從結點x 出發走向以x爲根的子樹,能夠到達的最遠結點的距離。
設x的子結點爲y1,y2...ytedge[x,y]y_1,y_2...y_t,edge[x,y]表示邊權
那麼顯然有:
D[x]=1itmax(D[yi]+edge[x,yi])D[x] = ^{max}_{1≤i≤t}(D[y_i] + edge[x,y_i])

F[x]F[x]爲經過結點x的最長鏈的長度。
然後就是代碼:

int ans;
int vis[N];
void dp(int u){
    vis[u] = 1;
    for(int i = head[u];i;i = nex[i]){
        int v = ver[i];
        if(vis[v])continue;
        dp(v);
        ans = max(ans,d[u] + d[v] + edge[i]);//看這條鏈是不是最大的
        d[x] = max(d[u],d[v] + edge[i]);//更新當前鏈長
    }
}

2.兩次BFS/DFS求樹的直徑

我們可以先從任意一點開始DFS,記錄下當前點所能到達的最遠距離,這個點爲P。

在從P開始DFS記錄下所能達到的最遠點的距離,這個點爲Q。

P,QP,Q就是直徑的端點,dis(P,Q)dis(P,Q)就是直徑。
具體代碼見下題

1.POJ 1985.Cow Marathon(DFS求樹的直徑模板題)

題意:有N個農田以及M條路,給出M條路的長度以及路的方向(這道題不影響,用不到),讓你找到一條 兩農田(任意的)間的路徑,使得距離最長,並輸出最長距離。
這裏用dfs求直徑,當然也可以用bfs和樹形DP來做。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<bitset>
#include<vector>
#include<queue>

#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;
const int INF = 0x3f3f3f3f;
const int N = 5e5+7;
const int M = 2007;

int head[N],ver[N],tot,edge[N],nex[N];
int n,m,ans;
int dis[N],vis[N];

inline void add(int u,int v,int w){
    ver[++tot] = v;
    edge[tot] = w;
    nex[tot] = head[u];
    head[u] = tot;
}

//兩次dfs一次求P一次求Q
void dfs(int u,int &ed){
    if(dis[u] > ans)ans = dis[u],ed = u;
    vis[u] = 1;
    for(int i = head[u];~i;i = nex[i]){
        int v = ver[i],w = edge[i];
        if(vis[v])continue;
        dis[v] = dis[u] + w;
        dfs(v,ed);
    }
    return ;
}


int p,q;
void solve(){
    dfs(1,p);
    ans = dis[p] = 0;
    memset(vis,0,sizeof vis);
    dfs(p,q);
    cout<<ans<<endl;
}
int main()
{
    while(scanf("%d%d",&n,&m) != EOF){
        memset(head,-1,sizeof head);
        memset(vis,0,sizeof vis);
        memset(dis,0,sizeof dis);
        tot = 0;
        over(i,1,m){
            int u,v,w;
            char ch[2];
            scanf("%d%d%d%s",&u,&v,&w,ch);
            add(u,v,w);
            add(v,u,w);
        }
        solve();
    }
return 0;
}

2.AcWing 350. 巡邏

在這裏插入圖片描述

https://www.acwing.com/solution/content/2788/

二、最近公共祖先(LCALCA

搬一下我之前寫的博客

LCA(Least Common Ancestors),即最近公共祖先,是指在有根樹中,找出某兩個結點u和v最近的公共祖先。 ———來自百度百科

在這裏插入圖片描述
比如這一顆二叉樹,D和E的LCA很明顯是根A,要注意的是D和B的LCA應該是B它本身

1.樹上倍增法

F[x,k]F[x,k]表示x的2k2^k輩祖先,若該結點不存在則F[x,k]=0F[x,k]=0。除此之外,k[1,logn],F[x,k]=F[F[x,k1],k1]\forall k \in [1,logn],F[x,k] = F[F[x,k-1],k-1]
樹上倍增法的預處理階段時間複雜度爲O(nlogn)O(nlogn),之後多次對於不同的x,y進行詢問LCA,每次詢問的時間複雜度爲O(logn)O(logn)

(1) P3379 【模板】最近公共祖先(LCA)

P3379 【模板】最近公共祖先(LCA)

在這裏插入圖片描述
要找兩個節點的LCA,暴力走的話就一步一步地往上爬,當然時間複雜度會賊高,不可取,你會發現一步一步往上爬就跟開篇我分享的那一篇博客裏寫的小兔子往前走一模一樣,所以同樣可以用倍增算法來優化。

就是按2的倍數來增大,也就是跳 1,2,4,8,16,321,2,4,8,16,32…… 不過在這我們不是按從小到大跳,而是從大向小跳,即按32,16,8,4,2,1……32,16,8,4,2,1來跳,如果大的跳不過去,再把它調小。這是因爲從小開始跳,可能會出現“悔棋”的現象。拿 55 爲例,從小向大跳,51+2+45≠1+2+4,所以我們還要回溯一步,然後才能得出5=1+45=1+4;而從大向小跳,直接可以得出5=4+15=4+1。這也可以拿二進制爲例,5(101)5(101),從高位向低位填很簡單,如果填了這位之後比原數大了,那就不填了,這個過程是很好操作的。

所以整體思路就是用倍增算法來優化往上跳的時間,先用一個dfs預處理一下樹,把所有節點的深度,父節點和它的2i2^i級的祖先全部用數組存起來,方便直接跳

其中幾個重要的數組:

  • depth數組是記錄每個節點的深度
  • fa[i][j]fa[i][j]是指節點 ii2j2^j 級的祖先的編號
  • head數組是鏈式前向星的數組相信大家都會,這裏就不展開了
  • lg數組是常數優化的數組,存的是log2N+1的值,注意用的時候要-1,開始之前先初始化一下,這樣直接調用可以優化節約時間其中初始化的方法:lg[i]=lg[i1]+(1<<lg[i1]==i)lg[i]=lg[i-1]+(1<<lg[i-1]==i),自己手算一下很清楚的(lg[1~10]爲1 2 2 3 3 3 3 4 4 4,應該很好懂吧)

預處理完了就要倍增求LCA了,我們先把兩個點提到同一高度,再統一開始跳。

但我們在跳的時候不能直接跳到它們的LCA,因爲這可能會誤判,比如4和8,在跳的時候,我們可能會認爲1是它們的LCA,但1只是它們的祖先,它們的LCA其實是3。所以我們要跳到它們LCA的下面一層,比如4和8,我們就跳到4和5,然後輸出它們的父節點,這樣就不會誤判了。
然後就是代碼了,裏面藏着非常詳細的註釋,相信大家這麼強一看就懂qwqqwq

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<queue>
#include<math.h>

#define ls (p<<1)
#define rs (p<<1|1)
#define mid (l+r)/2
#define over(i,s,t) for(register long long i=s;i<=t;++i)
#define lver(i,t,s) for(register long long i=t;i>=s;--i)

using namespace std;
typedef long long ll;//全用ll可能會MLE或者直接WA,試着改成int看會不會A
const ll N=500007;
const ll INF=1e9+9;
const ll mod=2147483647;
const double EPS=1e-10;//-10次方約等於趨近爲0
const double Pi=3.1415926535897;
ll n,m;
struct node
{
    ll u,v,nex;
}e[N<<1];
ll head[N],cnt;

void add(ll u,ll v)
{
    e[++cnt].v=v;
    e[cnt].u=u;//沒什麼用,還白佔空間
    e[cnt].nex=head[u];
    head[u]=cnt;
}

ll depth[N],fa[N][30],lg[N],s,x,y;

/*dfs函數的作用就是更新該點的所有祖先的fa數組,並通過遞歸把
該節點的所有的子節點和該節點一樣去更新*/
void dfs(ll now,ll fath)//子節點和父節點
{
    fa[now][0]=fath;//更新一下fa數組,2^0=1就是父節點
    depth[now]=depth[fath]+1;//更新深度
    over(i,1,lg[depth[now]]-1)
        fa[now][i]=fa[fa[now][i-1]][i-1];
        /*更新now的所有 2^i 級的祖先。先找到now的2^(i-1)級祖先,再往上找
        該祖先的2^(i-1)級祖先,就是now的2^i祖先,必須一節一節地往上搜*/
    for(ll i=head[now];i;i=e[i].nex)//鏈式前向星遍歷
        //如果now有子節點的話,就遞歸往子節點的子節點走(禁止套娃)
        if(e[i].v!=fath)//if(deep[v])continue;
            dfs(e[i].v,now);
}

inline ll LCA(ll x,ll y)
{
    if(depth[x]<depth[y])//用數學語言就是說不妨設x的深度比y的深度大
        swap(x,y);//這樣下面只需要寫一種代碼就好了
    while(depth[x]>depth[y])
        //讓x跳到y的高度(同一高度)
        x=fa[x][lg[depth[x]-depth[y]]-1];
    //如果跳到一塊了那LCA肯定就是y了
    if(x==y)
        return x;
    for(ll k=lg[depth[x]]-1;k>=0;--k)//倒着從大到小地跳
        /*因爲我們要求跳到x和y的LCA的下一層,所以沒有跳到的時候就
        讓x和y利用dfs裏早就用倍增算法處理過的祖先路徑快速地一塊往上跳*/
        if(fa[x][k]!=fa[y][k])
            x=fa[x][k],y=fa[y][k];//往上跳
    return fa[x][0];//返回x,y的父節點(肯定是相同的嘛)
}

int main()
{
    scanf("%lld%lld%lld",&n,&m,&s);
    over(i,1,n-1)
    {
        scanf("%lld%lld",&x,&y);
        add(x,y),add(y,x);//無向圖一定要記得建雙向邊
    }
    over(i,1,n)//預處理一下
    lg[i]=lg[i-1]+(1<<lg[i-1]==i);//log2(8)=3//這個手寫的lg[]要-1才能用lg[8]=4;
    dfs(s,0);//從樹根開始,因爲用的是鏈式前向星所以給一個假想根0(其實就是到這兒停)
    //dfs一下,預處理各點的深度和祖先
    over(i,1,m)
    {
        scanf("%lld%lld",&x,&y);
        printf("%lld\n",LCA(x,y));
    }
    return 0;
}


(2)HDOJ2586 How far away(LCA)

題目大意:n結點的樹,輸出任意兩個結點間的最小距離
思路分析:求兩個點的LCA,最小距離即deep[a]+deep[b]2deep[lca]deep[a]+deep[b]-2*deep[lca]

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<bitset>
#include<vector>
#include<queue>

#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;
const int INF = 0x3f3f3f3f;
const int N = 5e4+7;
const int M = 2007;

int head[N],ver[N<<1],tot,edge[N<<1],nex[N<<1];
int n,m,ans,t;
int f[N][20],deep[N],dis[N];

inline void add(int u,int v,int w){
    ver[++tot] = v;
    edge[tot] = w;
    nex[tot] = head[u];
    head[u] = tot;
}

void init(){
    t = (int)(log(n) / log(2)) + 1;//以2爲底的log2(n)
    over(i,1,n)head[i] = deep[i] = 0;//初始化deep深度
    tot = 0;
}

queue<int>q;
void bfs(){
    q.push(1);deep[1] = 1;
    while(q.size()){
        int u = q.front();q.pop();
        for(int i = head[u];i;i = nex[i]){
            int v = ver[i],w = edge[i];
            if(deep[v])continue;
            deep[v] = deep[u] + 1;
            dis[v] = dis[u] + w;
            f[v][0] = u;//父結點
            for(int j = 1;j <= t;++j)
                f[v][j] = f[f[v][j-1]][j-1];
            q.push(v);
        }
    }
}

int lca(int x,int y){
    if(deep[x] > deep[y])swap(x,y);
    lver(i,t,0)//先提到同一個高度
        if(deep[f[y][i]] >= deep[x])//倍增思想
            y = f[y][i];
    if(x == y)return x;
    lver(i,t,0)//再找公共祖先
        if(f[x][i] != f[y][i])
            x = f[x][i],y = f[y][i];
    return f[x][0];//return公共父結點
}
int T;
int main(){
    cin>>T;
    while(T--){
        scanf("%d%d",&n,&m);
        init();
        over(i,1,n-1){//n-1條邊
            int x,y,z;
            scanf("%d%d%d",&x,&y,&z);
            add(x,y,z);
            add(y,x,z);
        }
        bfs();
        over(i,1,m){
            int x,y;
            scanf("%d%d",&x,&y);
            printf("%d\n",dis[x] + dis[y] - 2 * dis[lca(x,y)]);
        }
    }
    return 0;
}

2.LCA的Tarjan算法

LCA的Tarjan算法是離線算法,但時間複雜度也從樹上倍增的O((n+m)logn)O((n+m)logn)優化到了O(n+m)O(n+m)雖然實測樹上倍增更快一點,應該是意外
下面的代碼是上面那道How far away的AC代碼。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<bitset>
#include<vector>
#include<queue>

#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;
const int INF = 0x3f3f3f3f;
const int N = 5e4+7;
const int M = 2007;

int head[N],ver[N<<1],tot,edge[N<<1],nex[N<<1];
int n,m,T,t;
int fa[N],dis[N],vis[N],ans[N];
vector<int>query[N],query_id[N];


void add(int u,int v,int w){
    ver[++tot] = v;
    edge[tot] = w;
    nex[tot] = head[u];
    head[u] = tot;
}

void add_query(int x,int y,int id){
    query[x].push_back(y);query_id[x].push_back(id);
    query[y].push_back(x),query_id[y].push_back(id);
}

int Get(int x){
    if(x == fa[x])return x;
    return fa[x] = Get(fa[x]);
}

void tarjan(int u){
    vis[u] = 1;
    for(int i = head[u];i;i = nex[i]){
        int v = ver[i];
        if(vis[v])continue;
        dis[v] = dis[u] + edge[i];
        tarjan(v);
        fa[v] = u;
    }
    for(int i = 0;i < query[u].size();++i){
        int v = query[u][i],id = query_id[u][i];
        if(vis[v] == 2){
            int lca = Get(v);
            ans[id] = min(ans[id],dis[v] + dis[u] - 2 * dis[lca]);
        }
    }
    vis[u] = 2;
}

int main()
{
    cin>>T;
    while(T--){
        scanf("%d%d",&n,&m);
        over(i,1,n){
            head[i] = 0,fa[i] = i,vis[i] = 0;
            query[i].clear(),query_id[i].clear();
        }
        tot = 0;
        over(i,1,n-1){
            int x,y,z;
            scanf("%d%d%d",&x,&y,&z);
            add(x,y,z);add(y,x,z);
        }
        over(i,1,m){
            int x,y;
            scanf("%d%d",&x,&y);
            if(x == y)ans[i] = 0;
            else {
                add_query(x,y,i);
                ans[i] = 1<<30;
            }
        }
        tarjan(1);
        over(i,1,m)
        printf("%d\n",ans[i]);
    }
    return 0;
}

三、樹上差分

對邊差分

  • (u,v)上全部加上w,對於差分數組就是:
  • u加上w,v加上w,lca減去2 × w
  • 用子樹中差分數組的和來還原信息
  • 每個點的信息記錄的是其到父親的邊的信息

對點差分

  • (u,v)上全部加上w,對於差分數組就是:
  • u加上w,v加上w,lca減去w,Fatherlca減去w
  • 同樣用子樹中差分數組的和來還原信息

差分和數據結構結合

  • 對於一個支持單點修改、區間求和的數據結構,如果使用差分, 就可以支持區間加法、單點查詢
  • 甚至可以支持區間加法、區間求和
  • 一個經典的例子就是用樹狀數組來完成這些事情
  • 用DFS序還可以把放到樹上,區間變成子樹

P3258 [JLOI2014]松鼠的新家(樹上差分模板)

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<math.h>
#include<cstring>
#include<bitset>
#include<vector>
#include<queue>

#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;
const int INF = 0x3f3f3f3f;
const int N = 3e5+7;
const int M = 2007;

int head[N<<1],ver[N<<1],tot,nex[N<<1];
int n,m,T,t;
int f[N][30],vis[N];
int deep[N];
int s[N];//差分數組
int a[N];

void add(int u,int v){
    ver[++tot] = v;
    nex[tot] = head[u];
    head[u] = tot;
}

queue<int>q;

void bfs(){//lca的預處理
    q.push(1);
    deep[1] = 1;//根的深度爲1
    while(q.size()){
        int u = q.front();q.pop();
        for(int i = head[u];i;i = nex[i]){
            int v = ver[i];
            if(deep[v])continue;
            f[v][0] = u;
            deep[v] = deep[u] + 1;
            for(int j = 1;j <= t;++j)
                f[v][j] = f[f[v][j-1]][j-1];
            q.push(v);
        }
    }
}

int lca(int x,int y){
    if(deep[x] > deep[y])swap(x,y);
    lver(i,t,0)
    if(deep[f[y][i]] >= deep[x])
        y = f[y][i];
    if(x == y)return x;
    lver(i,t,0)
    if(f[x][i] != f[y][i])
        x = f[x][i],y = f[y][i];
    return f[x][0];
}

void dfs(int u,int fa){
    for(int i = head[u];i;i = nex[i]){
        int v = ver[i];
        if(v == fa)continue;
        dfs(v,u);
        s[u] += s[v];
    }
}

int main()
{
    cin>>n;
    t = (int)(log(n) / log(2)) + 1;
    over(i,1,n)
    scanf("%d",&a[i]);
    over(i,1,n-1){
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y);add(y,x);
    }
    bfs();
    over(i,1,n-1){
        s[a[i]]++;
        s[a[i+1]]++;
        s[lca(a[i],a[i+1])]--;
        s[f[lca(a[i],a[i+1])][0]]--;
    }
    dfs(1,0);//從差分數組還原至原數組
    over(i,2,n)
    s[a[i]]--;//我們是直接正序循環一遍,這樣從2開始每個點都是即當一次起點又當一次終點多加了一次
    over(i,1,n)
    printf("%d\n",s[i]);
    return 0;
}

POJ3417 闇の連鎖

在這裏插入圖片描述

四、LCA的綜合應用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章