最近公共祖先问题

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

最近公共祖先(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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值