淺談倍增法求解LCA

DengDuck發表於2022-06-09

Luogu P3379 最近公共祖先

原題展現

題目描述

如題,給定一棵有根多叉樹,請求出指定兩個點直接最近的公共祖先。

輸入格式

第一行包含三個正整數 \(N,M,S\),分別表示樹的結點個數、詢問的個數和樹根結點的序號。

接下來 \(N-1\) 行每行包含兩個正整數 \(x, y\),表示 \(x\) 結點和 \(y\) 結點之間有一條直接連線的邊(資料保證可以構成樹)。

接下來 \(M\) 行每行包含兩個正整數 \(a, b\),表示詢問 \(a\) 結點和 \(b\) 結點的最近公共祖先。

輸出格式

輸出包含 \(M\) 行,每行包含一個正整數,依次為每一個詢問的結果。

樣例輸入 #1

5 5 4
3 1
2 4
5 1
1 4
2 4
3 2
3 5
1 2
4 5

樣例輸出 #1

4
4
1
4
4

提示

對於 \(30\%\) 的資料,\(N\leq 10\)\(M\leq 10\)

對於 \(70\%\) 的資料,\(N\leq 10000\)\(M\leq 10000\)

對於 \(100\%\) 的資料,\(N\leq 500000\)\(M\leq 500000\)

樣例說明:

該樹結構如下:

第一次詢問:\(2, 4\) 的最近公共祖先,故為 \(4\)

第二次詢問:\(3, 2\) 的最近公共祖先,故為 \(4\)

第三次詢問:\(3, 5\) 的最近公共祖先,故為 \(1\)

第四次詢問:\(1, 2\) 的最近公共祖先,故為 \(4\)

第五次詢問:\(4, 5\) 的最近公共祖先,故為 \(4\)

故輸出依次為 \(4, 4, 1, 4, 4\)

解析

本題是 LCA 的模板

LCA 的做法很多,比如暴力跳,倍增

暴力跳

讓深度大的一點不斷向上跳,直到兩點深度相等

如果兩點深度相同但是並不相等,可以兩點一起跳

在隨機資料下表現優異,因為樹會比較平衡,所以近似\(O(\log n)\)

通常會被卡成單次\(O(n)\),其實不難構造,可以構造一個深度大的樹(比如鏈)

本人出的一道題思想類似這樣,不過這道題保證了平衡

倍增法

考慮一次跳多一點

\(fa_{u,k}\)表示距離\(u\)的邊數為\(2^k\)的祖先節點則\(fa_{u,k}=fa_{fa_{u,k-1},k-1}\)可以通過dfs求出\(fa\)

如果求LCA,我們可以很快讓兩點來到相同的深度

考慮求兩點深度差,將差二進位制拆分,每次跳一個\(2\)的冪,時間複雜度\(O(\log n)\)

當然,沒必要真的二進位制拆分,因為我們要知道是\(2\)的幾次冪,所以用cmathlog2更加方便

這裡有一個優化:用\(O(n)\)的時間複雜度遞推求出log2的值

然後,如果兩點深度相同不相等,有一個自認為巧妙的方法求解

一個性質:如果兩點跳到LCA了,繼續向上跳依然相等(易證)

如果兩點向上跳不相等,那麼一定可以繼續跳

於是想到一個辦法:嘗試列舉\(i\)\(31\)\(0\),表示嘗試跳\(2^i\)

如果向上跳不相同的話,就向上跳,這樣,列舉完,LCA就是\(fa_{x,0}\)

核心程式碼如下,首先是預處理

void dfs(long long x,long long fa)
{
    f[x][0]=fa;
    dep[x]=dep[fa]+1;
    for(int i=1;i<=31;i++)
    {
        f[x][i]=f[f[x][i-1]][i-1];
    }
    for(int i=h[x];i;i=a[i].next)
    {
        if(a[i].to!=fa)
        {
            dfs(a[i].to,x);
        }
    }
}

然後是求解

if(dep[x]<dep[y])
{
    swap(x,y);
}   
while(dep[x] > dep[y])
{
    x = f[x][lg[dep[x]-dep[y]] - 1];
}
if(x==y)
{
    cout<<x<<endl;
    continue;
}
for(int k = lg[dep[x]] - 1; k >= 0;k--) 
{
    if(f[x][k] != f[y][k]) 
    {
        x = f[x][k], y = f[y][k];
    }
}

於是,我們得到了一個嚴格的\(O(\log n)\)演算法

Luogu P1967 [NOIP2013 提高組] 貨車運輸

原題展現

題目描述

A 國有 \(n\) 座城市,編號從 \(1\)\(n\),城市之間有 \(m\) 條雙向道路。每一條道路對車輛都有重量限制,簡稱限重。

現在有 \(q\) 輛貨車在運輸貨物, 司機們想知道每輛車在不超過車輛限重的情況下,最多能運多重的貨物。

輸入格式

第一行有兩個用一個空格隔開的整數 $ n,m$,表示 \(A\) 國有 $ n$ 座城市和 \(m\) 條道路。

接下來 \(m\) 行每行三個整數 \(x, y, z\),每兩個整數之間用一個空格隔開,表示從 $x $ 號城市到 $ y $ 號城市有一條限重為 \(z\) 的道路。
注意: \(x \neq y\),兩座城市之間可能有多條道路 。

接下來一行有一個整數 \(q\),表示有 \(q\) 輛貨車需要運貨。

接下來 \(q\) 行,每行兩個整數 \(x,y\),之間用一個空格隔開,表示一輛貨車需要從 \(x\) 城市運輸貨物到 \(y\) 城市,保證 \(x \neq y\)

輸出格式

共有 \(q\) 行,每行一個整數,表示對於每一輛貨車,它的最大載重是多少。
如果貨車不能到達目的地,輸出 \(-1\)

樣例輸入 #1

4 3
1 2 4
2 3 3
3 1 1
3
1 3
1 4
1 3

樣例輸出 #1

3
-1
3

提示

對於 \(30\%\) 的資料,\(1 \le n < 1000\)\(1 \le m < 10,000\)\(1\le q< 1000\)

對於 \(60\%\) 的資料,\(1 \le n < 1000\)\(1 \le m < 5\times 10^4\)\(1 \le q< 1000\)

對於 \(100\%\) 的資料,\(1 \le n < 10^4\)\(1 \le m < 5\times 10^4\),$1 \le q< 3\times 10^4 $,\(0 \le z \le 10^5\)

解析

因為我們想要經過的最小邊最大,那麼不妨構造一個最大生成樹(建議使用克魯斯卡爾算 法),這樣每條邊都能儘可能大

然後問題轉換為樹上查詢,同樣利用倍增法求\(x->LCA,y->LCA\)路徑中的最小邊,也是可以預處理的

不過問題不保證樹聯通,需要判斷是否有解

克魯斯卡爾的優勢就體現出來了,我們已經處理了並查集,如果兩點祖先不同就直接判斷為無解

核心程式碼如下(碼風十分奇怪)

#include<bits/stdc++.h>
#define ll long long
using namespace std;
struct road
{
    ll s,t,w;
}r[200005];
struct node
{
    ll to,next,w;
}a[200005];
ll n,m,t,k,x,y,fa2[100005],h[100005],fa[100005][33],f[100005][33],dep[100005],lg[100005];
bool cmp(road x,road y)
{
    return x.w>y.w;
}
void add(int x,int y,int z)
{
    t++;
    a[t].to=y;
    a[t].w=z;
    a[t].next=h[x];
    h[x]=t;
}
int find(int x)
{
    if(fa2[x]==x)return x;
    return fa2[x]=find(fa2[x]);
}
void dfs(long long x,long long fn)
{
    fa[x][0]=fn;
    dep[x]=dep[fn]+1; 
    for(int i=1;i<=31;i++)
    {
        fa[x][i]=fa[fa[x][i-1]][i-1];
        f[x][i]=min(f[x][i-1],f[fa[x][i-1]][i-1]);//f陣列表示x到fa[x][i]路徑的最小值
    }
    for(int i=h[x];i;i=a[i].next)
    {
        if(a[i].to!=fn)
        {   
            f[a[i].to][0]=a[i].w;
            dfs(a[i].to,x);
        }
    }
}
int lca(int x,int y)
{
    if(dep[x]<dep[y])
    {
        swap(x,y);
    }   
    while(dep[x] > dep[y])
    {
        x = fa[x][lg[dep[x]-dep[y]] - 1];
    }
    if(x==y)
    {
        return x;
    }
    for(int k = lg[dep[x]] - 1; k >= 0;k--) 
    {
        if(fa[x][k] != fa[y][k]) 
        {
            x = fa[x][k], y = fa[y][k];
        }
    }    
    return fa[x][0];
}
int work(int x,int y)//求解x到y路徑的最小值,保證y是x祖先
{
    ll ans=1e9,deph=dep[x]-dep[y];
    while(deph!=0)
    {
        ll t=lg[deph]-1;
        ans=min(ans,f[x][t]);
        x=fa[x][t];
        deph=dep[x]-dep[y];
    }
    return ans;
}
int main()
{

    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        fa2[i]=i;
        lg[i] = lg[i-1] + (1 << lg[i-1] == i);
    }
    for(int i=1;i<=m;i++)
    {
        cin>>r[i].s>>r[i].t>>r[i].w;
    }
    sort(r+1,r+m+1,cmp);//克魯斯卡爾
    int k=n-1;
    for(int i=1;i<=m;i++)
    {
        if(k==0)break;
        if(find(r[i].s)!=find(r[i].t))
        {
            add(r[i].s,r[i].t,r[i].w);
            add(r[i].t,r[i].s,r[i].w);
            fa2[find(r[i].s)]=find(r[i].t);
            k--;
        }
    }
    for(int i=1;i<=n;i++)
    {
        if(find(i)==i)
        {
            dfs(i,0);
        }
    }
    cin>>k;
    for(int i=1;i<=k;i++)
    {
        cin>>x>>y;
        if(find(x)!=find(y))
        {
            cout<<-1<<endl;
            continue;
        }
        int lcah=lca(x,y);
        cout<<min(work(x,lcah),work(y,lcah))<<endl;
    }
}

Duck006[DuckOI]Kill the Duck

原題展現

溫馨提示

Duck非常不要臉,單推自己的題

後來發現其實有好多一樣的題

  • 貪玩的小孩
  • HDU 2586 How far away?

題目描述

XCR是世界名列前茅的OIer,今天在打模擬賽。

他已經AC了前四道題,準備暴切第五題,看著這個題面,突然發現不太對....

他一看五道題的名字

\[\mathtt{\color{red}{X}\color{black}{or}}\\ \mathtt{\color{red}{C}\color{black}{ount\;the\;Number\;of\;Dance\;Schemes}}\\ \mathtt{\color{red}{R}\color{black}{elaxing\;Time }}\\ \mathtt{\color{red}{A}\color{black}{n\; Easy\;Problem}}\\ \mathtt{\color{red}{K}\color{black}{ill\;the\;Duck}}\\ \mathtt{\huge{\color{red}{XCRAK}}} \]

XCR十分生氣,想要殺了DengDuck

DengDuck跑到了一個有\(n\)個結點,\(n-1\)條邊的樹上

這個樹的每個邊都是無向的,都有邊權

XCR現在有\(m\)次詢問,第\(i(1 \leq i \leq m)\)次給出兩個正整數\(x_i\)\(y_i\),含義如下

DengDuck 在點 \(x_i(1 \leq x_i \leq n)\) 上,XCR在點 \(y_i(1 \leq y_i \leq n)\)

對於每次詢問,請問XCR離DengDuck的距離是多少?

輸入格式

第一行一個整數\(n\)

接下來\(n-1\)行每行三個正整數分別表示一條邊的起點,終點,邊權

\(n+1\)行一個正整數\(m\)

接下來\(m\)行每行兩個正整數\(x_i\)\(y_i\)

輸出格式

\(m\)行,每行一個正整數,表示DengDuck和XCR的距離

樣例輸入 #1

3
1 2 3
2 3 4
2

1 2
1 3

樣例輸出 #1

3
7

樣例輸入 #2

3
1 3 10
1 2 13
5
1 1
2 2
3 1
2 1
1 3

樣例輸出 #2

0
0
10
13
10

樣例輸入 #3

14
5 7 12
7 11 15
5 14 12
14 3 17
7 1 19
14 4 14
1 12 16
1 6 16
12 9 19
9 10 10
7 2 11
4 8 10
2 13 14
17
6 11
14 14
13 11
6 10
12 6
8 7
9 9
10 11
13 10
1 4
2 12
13 4
2 7
2 1
12 2
10 11
4 7

樣例輸出 #3

50
0
40
61
32
48
0
79
89
57
46
63
11
30
46
79
38

提示

對於一定的資料 \(n,m\)的範圍 特殊限制
\(5\%\)的資料 \(1~20\)
\(20\%\)的資料 \(1~3000\)
另外的\(5\%\)的資料 \(1~3000\) \(m=1\)
所有資料 \(1~100000\)

解析

預處理出\(dis_i\)表示點\(i\)到根\(1\)的距離,答案是\(dis_x+dis_y-2dis_{lca(x,y)}\)

非常容易證明

程式碼如下

#include <bits/stdc++.h>
using namespace std;
int n, k, b[1000005], x, y, z, tot, h[500005], len[500005], fa[500005][33], dep[500005], lg[500005],
    f[1000005], ans;
struct node {
    int to, next, w;
} a[1000005];
void dfs(long long x, long long fn, long long l) {
    fa[x][0] = fn;
    dep[x] = dep[fn] + 1;
    len[x] = l;
    for (int i = 1; i <= 31; i++) {
        fa[x][i] = fa[fa[x][i - 1]][i - 1];
    }
    for (int i = h[x]; i; i = a[i].next) {
        if (a[i].to != fn) {
            dfs(a[i].to, x, l + a[i].w);
        }
    }
}
int lca(int x, int y) {
    if (dep[x] < dep[y]) {
        swap(x, y);
    }
    while (dep[x] > dep[y]) {
        x = fa[x][lg[dep[x] - dep[y]] - 1];
    }
    if (x == y) {
        return x;
    }
    for (int k = lg[dep[x]] - 1; k >= 0; k--) {
        if (fa[x][k] != fa[y][k]) {
            x = fa[x][k], y = fa[y][k];
        }
    }
    return fa[x][0];
}
void add(int x, int y, int z) {
    ++tot;
    a[tot].to = y;
    a[tot].next = h[x];
    a[tot].w = z;
    h[x] = tot;
}
void answer(int x, int fn) {
    for (int i = h[x]; i; i = a[i].next) {
        if (a[i].to != fn) {
            answer(a[i].to, x);
            f[x] += f[a[i].to];
        }
    }
    ans = max(ans, f[x]);
}
int main() {
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
    }
    for (int i = 1; i <= n - 1; i++) {
        cin >> x >> y >> z;
        add(x, y, z);
        add(y, x, z);
    }
    dfs(1, 0, 0);
    cin >> k;
    for (int i = 1; i <= k; i++) {
        cin >> x >> y;
        int t = lca(x, y);
        cout << len[x] + len[y] - 2 * len[t] << endl;
    }
}