最近公共祖先問題

原文鏈接:最近公共祖先問題

最近公共祖先(Least Common Ancestors)問題是面試中經常出現的一個問題,這種問題變種很多,解法也很多。最近公共祖先問題的定義如下:

對於有根樹T的兩個結點u、v,最近公共祖先LCA(T,u,v)表示一個結點x,滿足x是u、v的祖先且x的深度儘可能大。另一種理解方式是把T理解爲一個無向無環圖,而LCA(T,u,v)即u到v的最短路上深度最小的點。

例如,對於下面的樹,結點4和結點6的最近公共祖先LCA(T,4,6)爲結點2。

lca_example

面試中LCA問題的擴展主要在於結點是否只包含父結點指針,對於同一棵樹是否進行多次LCA查詢。下面分別進行說明。

1.結點只包含父結點指針,只進行一次查詢

首先可以計算出結點u和v的深度d1和d2(由於只有parent指針,沿着parent指針一直向上移動即可計算出它的深度)。如果d1>d2,將u結點向上移動d1-d2步,如果d1<d2,將v結點向上移動d2-d1步,現在u結點和v結點在同一個深度了。下面只需要同時將u,v結點向上移動,直到它們相遇(到達同一個結點)爲止,相遇的結點即爲u,v結點的最小公共祖先。

int getDepth(TreeNode *node) {
    int d = 0;
    while (node) d++, node = node->parent;
    return d;
}
TreeNode *getLCA(TreeNode *node1, TreeNode *node2) {
    int d1 = getDepth(node1), d2 = getDepth(node2);
    if (d1 > d2) {
        swap(d1, d2);
        swap(node1, node2);
    }
    while (d1 < d2) d2--, node2 = node2->parent;
    while (node1 != node2) {
        node1 = node1->parent;
        node2 = node2->parent;
    }
    return node1;
}

該算法時間複雜度爲O(h),空間複雜度爲O(1),其中h爲樹的高度。

2.結點只包含父結點指針,進行多次查詢

第一種算法每一次查詢的時間複雜度都是O(h),如果需要對同一棵樹進行多次查詢,有沒有更快的算法呢?觀察第一種算法,主要進行的操作是將某個結點u沿着parent指針向上移動n步,我們可以對樹進行一些預處理加速這個過程,這裏使用到了動態規劃的思想。

設P[i][j]表示結點i往上移動2^j步所到達的結點,P[i][j]可以通過以下遞推公式計算:

P[i][j]=\left\{\begin{matrix} parent(i), j=0 \\P[P[i][j-1]][j-1] \end{matrix}\right.

利用P數組可以快速的將結點i向上移動n步,方法是將n表示爲2進制數。比如n=6,二進制爲110,那麼利用P數組先向上移動4步(2^2),然後再繼續移動2步(2^1),即P[ P[i][2] ][1]。

預處理計算P數組代碼如下:

map<TreeNode*, int> nodeToId;
map<int, TreeNode*> idToNode;
const int MAXLOGN=20; //樹中最大結點數爲1<<20
int P[1 << MAXLOGN][MAXLOGN];

//allNodes存放樹中所有的結點
void preProcessTree(vector<TreeNode *> allNodes) {
    int n = allNodes.size();
    // 初始化P中所有元素爲-1
    for (int i = 0; i < n; i++)
        for (int j = 0; 1 << j < n; j++)
            P[i][j] = -1;
    for (int i = 0; i < n; i++) {
        nodeToId[allNodes[i]] = i;
        idToNode[i] = allNodes[i];
    }
    // P[i][0]=parent(i)
    for (int i = 0; i < n; i++)
        P[i][0] = allNodes[i]->parent ? nodeToId[allNodes[i]->parent] : -1;
    // 計算P[i][j]
    for (int j = 1; 1 << j < n; j++)
        for (int i = 0; i < n; i++)
            if (P[i][j] != -1)
                P[i][j] = P[P[i][j - 1]][j - 1];
}

另外我們還需要預處理計算出每個結點的深度L[],預處理之後,查詢node1和node2的LCA算法如下。

TreeNode* getLCA(TreeNode *node1, TreeNode *node2, int L[]) {
    int id1 = nodeToId[node1], id2 = nodeToId[node2];
    //如果node2的深度比node1深,那麼交換node1和node2
    if (L[id1] < L[id2]) swap(id1, id2);
    //計算[log(L[id1])]
    int log;
    for (log = 1; 1 << log <= L[id1]; log++);
    log--;
    //將node1向上移動L[id1]-L[id2]步,使得node1和node2在同一深度上
    for (int i = log; i >= 0; i--)
        if (L[id1] - (1 << i) >= L[id2])
            id1 = P[id1][i];
    if (id1 == id2) return idToNode[id1];
    //使用P數組計算LCA(idToNode[id1], idToNode[id2])
    for (i = log; i >= 0; i--)
        if (P[id1][i] != -1 && P[id1][i] != P[id2][i])
            id1 = P[id1][i], id2 = P[id2][i];
    return idToNode[id1];
}

時間複雜度分析:假設樹包含n個結點,由於P數組有nlogn個值需要計算,因此預處理的時間複雜度爲O(nlogn)。查詢兩個結點的LCA時,函數getLCA中兩個循環最多執行2logn次,因此查詢的時間複雜度爲O(logn)。

3.結點包含兒子結點指針,只進行一次查詢

這裏我們只考慮二叉樹,樹中結點包含左右兒子結點指針。給定樹根結點T,以及樹中u,v結點,需要計算LCA(T,u,v)。可以採用遞歸的方法,對於結點node,如果在node左子樹或者右子樹中找到了LCA(u,v),那麼直接返回這個答案。否則如果node子樹同時包含了u,v結點,那麼node結點即爲LCA(u,v)。否則在當前node子樹中找不到LCA(u,v)。

struct TreeNode {
    TreeNode *left;
    TreeNode *right;
};
//在子樹node中查找LCA(u,v),同時u,v在node子樹中的出現情況記錄到flag中
//如果沒找到LCA(u,v),返回NULL
TreeNode *getLCAHelper(TreeNode *node, TreeNode *u, TreeNode *v, int &flag) {
    if (u == node && v == node) return node;

    int leftFlag = 0, rightFlag = 0;
    if (node->left != NULL) {
        ListNode *ret = getLCAHelper(node->left, u, v, leftFlag);
        if (!ret) return ret;
    }
    if (node->right != NULL) {
        ListNode *ret = getLCAHelper(node->right, u, v, rightFlag);
        if (!ret) return ret;
    }
    if (u == node) flag |= 1;  //標記u在子樹node中
    if (v == node) flag |= 2;  //標記v在子樹node中
    flag |= leftFlag;
    flag |= rightFlag;
    if (flag == 3) return node; //u,v都出現在node子樹中
    return NULL;
}
//計算LCA(root, node1, node2)
TreeNode *getLCA(TreeNode *root, TreeNode *node1, TreeNode *node2) {
    int flag = 0;
    return getLCAHelper(root, node1, node2, flag);
}

時間複雜度分析:該遞歸算法最多訪問每個樹結點一次,因此時間複雜度爲O(n)。

4.結點包含兒子結點指針,進行多次查詢

這種情況同樣可以使用算法2來提高每次查詢的效率,預處理過程中先遍歷樹,記錄每個結點的深度和父親結點指針,然後計算P數組,查詢過程和算法2一樣。這樣,預處理的時間複雜度爲O(nlogn),查詢一次的時間複雜度爲O(logn)。

現在就去在線練習題庫練習:http://www.itint5.com/oj/#7

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