最近公共祖先问题
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查询。下面分别进行说明。
首先可以计算出结点u和v的深度d1和d2(由于只有parent指针,沿着parent指针一直向上移动即可计算出它的深度)。如果d1>d2,将u结点向上移动d1-d2步,如果d1<d2,将v结点向上移动d2-d1步,现在u结点和v结点在同一个深度了。下面只需要同时将u,v结点向上移动,直到它们相遇(到达同一个结点)为止,相遇的结点即为u,v结点的最小公共祖先。
该算法时间复杂度为O(h),空间复杂度为O(1),其中h为树的高度。
第一种算法每一次查询的时间复杂度都是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数组代码如下:
另外我们还需要预处理计算出每个结点的深度L[],预处理之后,查询node1和node2的LCA算法如下。
时间复杂度分析:假设树包含n个结点,由于P数组有nlogn个值需要计算,因此预处理的时间复杂度为O(nlogn)。查询两个结点的LCA时,函数
这里我们只考虑二叉树,树中结点包含左右儿子结点指针。给定树根结点T,以及树中u,v结点,需要计算LCA(T,u,v)。可以采用递归的方法,对于结点node,如果在node左子树或者右子树中找到了LCA(u,v),那么直接返回这个答案。否则如果node子树同时包含了u,v结点,那么node结点即为LCA(u,v)。否则在当前node子树中找不到LCA(u,v)。
时间复杂度分析:该递归算法最多访问每个树结点一次,因此时间复杂度为O(n)。
这种情况同样可以使用算法2来提高每次查询的效率,预处理过程中先遍历树,记录每个结点的深度和父亲结点指针,然后计算P数组,查询过程和算法2一样。这样,预处理的时间复杂度为O(nlogn),查询一次的时间复杂度为O(logn)。
现在就去在线练习题库练习:http://www.itint5.com/oj/#7
最近公共祖先(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
相关文章推荐
- 最近公共祖先(LCA)和RMQ问题
- LCA(最近公共祖先)问题的离线算法
- POJ1330Nearest Common Ancestors最近公共祖先LCA问题
- 最近公共祖先LCA问题
- 【14】最近公共祖先问题
- 最近公共祖先与RMQ问题
- 二叉树中的最近公共祖先问题
- LCA问题——最近公共祖先(Least Common Ancestors)
- LCA(最近公共祖先)问题的新老解法对比
- 【Leetcode】查找二叉树中任意结点的最近公共祖先(LCA问题)
- 二叉树中的最近公共祖先问题
- Tarjan算法应用 (割点/桥/缩点/强连通分量/双连通分量/LCA(最近公共祖先)问题)(转载)
- LCA(最近公共祖先)问题
- 【LCA】最近公共祖先问题Lowest Common Ancestors
- Tarjan算法应用 (割点/桥/缩点/强连通分量/双连通分量/LCA(最近公共祖先)问题)(转载)
- 【转】最近公共祖先问题
- LCA问题(最近公共祖先问题)+ RMQ问题
- LCA问题(最近公共祖先问题)+ RMQ问题
- LCA(最近公共祖先)问题的离线算法
- LCA问题:求二叉树中任意两个节点的最近公共祖先