1. 程式人生 > >DP之倍增演算法解決LCA問題

DP之倍增演算法解決LCA問題

LCA,最近公共祖先,也就是樹上兩個節點的相同的祖先裡距離它們最近的(也就是深度最深的)。倍增演算法用於解決LCA問題的線上查詢。

比如要找x和y的LCA,一般會怎麼做?

首先是暴力。一種是DFS遍歷說有點,無需預處理,查詢是O(n)的。還有一種暴力是先一步一步將x和y提到同一高度,然後同時看x和y的父節點,相等則是LCA,不等則讓x=father(x),y=father(y),這個查詢的複雜度顯然是跟x和y距離有關,平均複雜度也是線性的,即O(n)~~

這裡要說的倍增是對第二種暴力的優化,O(1)的查詢——額,看到將O(n)的查詢優化成了O(1),就應該能想到O(n log n)的預處理~~~這裡的倍增也不例外。

首先理解f[i][j]陣列,f[i][j]表示的是結點i的第2^j(這是次方不是異或)代祖先的編號,這個陣列是這個演算法跟DP唯一的關係~~轉移方程很容易就可以寫出來:f[i][j] = f[ f[i][j-1] ][j-1],這很容易理解,因為2^j==2^{j-1} + 2^{j-1}理解這個陣列後,預處理就好弄了~預處理主要就是求出每一個節點的深度和f[][]陣列即可~~

首先,讓x和y處於同一高度:因為x和y高度都已知了,只要求出它們的高度差,然後由深度更深的那個倍增查詢直到高度相等即可。如果這不能理解的話,那就舉個例子吧,假設x深度為3,y深度為8,高度差為5==2^2+2^0,也就是說只要依次令y=f[y][2],y=f[y][0]即可(這樣先理解了思想,具體實現後面有程式碼)。

然後,此時x和y高度一樣了,怎麼求公共祖先呢?仍然是用倍增,每次使x=f[x][t],y=f[y][t]~這個t是最大的f[x][t] != f[y][t]的t。顯然t具有單調遞減的特性,所以一次for迴圈,即可求出最後的x和y——最後的x和y也就是最初x和y的所有處於同一深度的祖先裡深度最小的不等的一對(這個不明白就自己想一下吧)。然後它們的父節點就是LCA了。

這個演算法差不多講完了,思想上就是隻要先用倍增使x和y高度一樣,然後使用倍增查詢到最近公共祖先即可。

額,差點忘記了,這個演算法是線上的,所以是支援修改的——允許樹增加葉節點,只需要在加邊的時候求出新節點高度,以及新節點的f陣列即可~~

下面是基本的預處理+查詢的程式碼,至於加邊的就沒寫了(手動滑稽~~~)

/***************************************************************************
 *LCA,最近公共祖先的線上演算法——倍增演算法,常用於線上詢問樹上兩點間的距離
 *即二者距離最近公共祖先的距離之和,以下程式碼用於解決這個問題
 ***************************************************************************/

struct Edge // 邊,用於數組裡
{
    int e, dis; // edge[i].e表示i與e之間有邊,dis是i和e間的距離
    Edge(int ee=0, int diss=0):e(ee), dis(diss) {}
};
vector<Edge> edge[maxn];
int f[maxn][20];   //  f[i][j]表示i的第2^j代祖先
int dis[maxn][20]; //  dis[i][j]表示i到其第2^j代祖先的距離
int dep[maxn];     //  dp[i]表示i的深度

void lca(const int& rt)// nlog n建樹,並得到每一個結點的深度,以及f和dis
{
    for(int i = 0; i < edge[rt].size(); ++ i)
    {
        const int& t = edge[rt][i].e, &d = edge[rt][i].dis;
        if(t != f[rt][0])//遍歷每一個和rt相連的非父節點,即rt的子節點
        {
            f[t][0] = rt;   // 第一代祖先,即父節點
            dis[t][0] = d;  // 與父節點間的距離
            dep[t] = dep[rt]+1; // 比父親深度大1
            for(int j = 1; (1<<j) < dep[t]; ++ j)
            {//求t的f和dis
                f[t][j] = f[f[t][j-1]][j-1];
                dis[t][j] = dis[t][j-1] + dis[f[t][j-1]][j-1];
            }
            //深度遍歷rt的子樹
            lca(t);
        }
    }
}

int queue(int x, int y)//查詢節點x和節點y的距離
{
    if(dep[x] < dep[y])
        swap(x, y);//便於後面將它們提到同一高度

    int ret = 0;

    for(int i = 19; ~i; -- i)
    {// 讓x,y處於同一高度,這個19是因為我f陣列第二維開的20
        if(dep[f[x][i]] >= dep[y])
        {
            ret += dis[x][i];
            x = f[x][i];
        }
    }
    if(x != y)
    {// x != y 才繼續找公共祖先,因為如果x==y,則表示y是x的祖先,它們的公共祖先就是y,不需要進一步尋找了
        for(int i = 19; ~i; -- i)
        {
            if(f[x][i] != f[y][i])
            {
                ret += dis[x][i] + dis[y][i];
                x = f[x][i];
                y = f[y][i];
            }
        }
        ret += dis[x][0] + dis[y][0];
        x = f[x][0];
        y = f[y][0];
    }

    return ret;//這裡返回的是距離,如果只是要找祖先,只需要返回x或者y就可以了
}