最近公共祖先问题

来源:互联网 发布:淘宝评分影响 编辑:程序博客网 时间:2024/04/30 17:33

原文链接:最近公共祖先问题

最近公共祖先(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<<20int 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),返回NULLTreeNode *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

0 0