您的位置:首页 > 其它

最近公共祖先问题

2014-08-06 19:18 225 查看
原文链接:最近公共祖先问题

最近公共祖先(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问题的扩展主要在于结点是否只包含父结点指针,对于同一棵树是否进行多次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向上移动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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: