樹上最近公共祖先(樹上倍增)

算法描述

倍增算法在ST表中的應用是直接在第二維的dp中直接以二進制的形式展現;而對於樹上的問題則可以這樣設計:設fa[i][j]表示i結點的第2^j個祖先,顯然的是fa[i][0]等於i的父節點,假設父節點的祖先結點都已經更新完畢,我們就可以根據父親的祖先來更新本結點的祖先,於是狀態轉移方程就成了fa[i][j]=fa[fa[i][j-1]][j-1],這是根據二進制中可以拆分成兩個相同小一次的二進制和的特性來構造的。於是,用dfsdfs把每個結點的fa處理好後求LCA就可以在lognlogn的複雜度求得。邏輯如下:

  • 將兩個點提到一個深度(所以在dfs中還要維護每個結點的深度)
  • 兩個結點一起向上跳,要從最多的步數然後越來越少地跳,如果兩個祖先相等了可能是跳過頭了,考慮不跳,然後不相等肯定沒到達就選擇往上跳,最後一定能到達LCA的兒子結點中,返回fa[u][0]即可。這裏是根據二進制數-1可以由多個小於它的數組合而成的原理來構造的

例子

以洛谷P3379板子題來檢驗代碼正確性

#include <bits/stdc++.h>
using namespace std;

struct Edge
{
    int to;
    int next;
};

Edge E[1000005];
int head[500005], cnt;

int n, m, rt;
int fa[500005][19]; //i結點的第2^j個祖先
int deep[500005];   //結點深度
int Log[500005];
int Mi[20];

void edge_add(int u, int v)
{
    E[cnt].to = v;
    E[cnt].next = head[u];
    head[u] = cnt++;
}

void init()
{
    memset(head, -1, sizeof(head));
    memset(fa, 0, sizeof(fa));
    cnt = 0;
    int u, v;
    for (int i = 1; i < n; i++)
    {
        scanf("%d%d", &u, &v);
        edge_add(u, v);
        edge_add(v, u);
    }
}

void dfs(int u, int parent) {
    fa[u][0] = parent; //直接雙親更新
    for (int cur = 1; Mi[cur] <= deep[u]; cur++)
        fa[u][cur]=fa[fa[u][cur-1]][cur-1]; //走到這個點說明前面的都已經更新完畢  利用前面的更新當前的
    for (int cur = head[u]; ~cur; cur=E[cur].next)
        if (E[cur].to^parent) //非父即子
            dfs(E[cur].to, u);
}

int LCA(int u, int v) {
    if (deep[u]<deep[v]) swap(u, v);
    while (deep[u]>deep[v]) //爬樹
        u=fa[u][Log[deep[u]-deep[v]]]; //拆分深度差的二進制,儘量往上跳
    if (u == v) return v;
    for (int len=Log[deep[u]]; len>=0; len--)
        if (fa[u][len]^fa[v][len]) { //相等考慮不跳,可能跳過頭了,顯然假設這個步長是解的話剩餘的二進制加起來等於這個長度的二進制-1,所以底下返回父親結點成立
            u = fa[u][len];
            v=fa[v][len];
        }
    return fa[u][0];
}

int main()
{
    Log[0] = -1;
    for (int i = 1; i <= 500000; i++)
        Log[i] = Log[i >> 1] + 1;
    Mi[0] = 1;
    for (int i = 1; i < 20; i++)
        Mi[i] = Mi[i - 1] << 1;
    int v, u;
    scanf("%d%d%d", &n, &m, &rt);
    init();
    dfs(rt, 0); //預處理fa表和深度表方便之後計算
    for (int i = 0; i < m; i++)
    {
        scanf("%d%d", &u, &v);
        printf("%d\n", LCA(u, v));
    }
    return 0;
}

優化常數的寫法

#include <bits/stdc++.h>
using namespace std;

/**
 * 常數最小
*/

int n, m, s;
int _to[1000005], _next[1000005], head[500005], cnt;
int deep[500005], fa[500005][19];
int Log[500005], Mi[20];

void edge_add(int u, int v) {
    _to[cnt] = v;
    _next[cnt] = head[u];
    head[u] = cnt++;
}

void init() {
    memset(head, -1, sizeof(head));
    cnt=0;
    int u, v;
    for (int i = 1; i < n; i++) {
        scanf("%d%d", &u, &v);
        edge_add(u, v);
        edge_add(v, u);
    }
    deep[0]=0; fa[0][0]=0;//根的虛擬祖先
}

void dfs(int rt, int parent) {
    fa[rt][0] = parent;
    deep[rt]=deep[parent]+1;
    for (int i = 1; Mi[i] <= deep[rt]; i++) //注意這裏是Mi而不是i
        fa[rt][i]=fa[fa[rt][i-1]][i-1];
    for(int cur=head[rt]; ~cur; cur=_next[cur])
        if (_to[cur]^parent)
            dfs(_to[cur], rt);
}

int LCA(int u, int v) {
    if (deep[u] < deep[v]) swap(u, v);
    while (deep[u] > deep[v])
        u=fa[u][Log[deep[u]-deep[v]]];
    if (u == v) return v;
    for (int i=Log[deep[u]]; i >= 0; i--)
        if (fa[u][i]^fa[v][i]) {
            u=fa[u][i];
            v=fa[v][i];
        }
    return fa[u][0];
}

int main() {
    Log[0]=-1;
    for (int i = 1; i < 500005; i++)
        Log[i] = Log[i>>1]+1;
    Mi[0]=1;
    for (int i = 1; i < 20; i++)
        Mi[i] = Mi[i-1]<<1;
    scanf("%d%d%d", &n, &m, &s);
    init();
    dfs(s, 0);
    int u, v;
    for (int i = 0; i < m; i++) {
        scanf("%d%d", &u, &v);
        printf("%d\n", LCA(u, v));
    }
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章