LCA算法
2016-02-22 21:37
417 查看
LCA问题:如何求树(不限于二叉树)中两个节点(不限于叶子节点)的最近公共祖先节点。
LCA算法分为在线算法与离线算法。
在线算法:可以以序列化的方式一个个的处理输入,也就是说在开始时并不需要已经知道所有的输入。
离线算法:在开始时就需要知道问题的所有输入数据,而且在解决一个问题后就要立即输出结果。
在线算法与离线算法都基于DFS。在线算法与RMQ算法(区间最值查询)相关,离线算法与Tarjan算法相关。
在线算法 LCA RMQ的相互转换
各自意义:
LCA:基于有根树最近公共祖先问题。 LCA(T, u, v):在有根树T中,询问一个距离根最远的结点x,使得x同时为结点u、v的祖先。
RMQ:区间最小值询问问题。 RMQ(A, i, j):对于线性序列A中,询问区间[i, j]上的最小(最大)值。
RMQ对于LAC的转换:
设一个长度为N的序列A,按照如下方法将其递归建立为一棵树:
1)设序列中最小值为Ak,建立优先级为Ak的根节点Tk;
2)将A(1…k-1)递归建树作为Tk的左子树;
3)将A(k+1…N)递归建树作为Tk的右子树;
如序列A=(7, 5, 8, 1, 10)建树的结果为:
对于RMQ(A, i, j):
1)设序列中的最小值为Ak,若i<=k<=j,那么答案为k;
2)若k>j,那么答案为RMQ(A1…k-1, i, j);
3)若k< i,那么答案为RMQ(Ak+1…N, i, j);
而RMQ问题可以采用ST算法解决,则将LCA转换为RMQ再根据ST算法解决,时间复杂度为O(nlogn)的预处理+O(1)的查询。
LCA的离线Tarjan算法
基于Tarjan算法与并查集。
Tarjan算法
算法用集合表示一类节点,这些节点跟集合外的点的LCA都一样,并把这个LCA设为这个集合的祖先。当搜索到节点x时,创建一个由x本身组成的集合,这个集合的祖先为x自己。然后递归搜索x的所有儿子节点。当一个子节点搜索完毕时,把子节点的集合与x节点的集合合并,并把合并后的集合的祖先设为x。因为这棵子树内的查询已经处理完,x的其他子树节点跟这棵子树节点的LCA都是一样的,都为当前根节点x。所有子树处理完毕之后,处理当前根节点x相关的查询。遍历x的所有查询,如果查询的另一个节点v已经访问过了,那么x和v的LCA即为v所在集合的祖先。
对于每个节点u,关于它的询问(u,v)只有两种。
1、v在u的子树内。
此时LCA(u,v) = u.
2、v不在u的子树内。
⑴假设v在u的父亲的另一棵子树内。
此时LCA(u,v) = father[u].
⑵如果不满足条件⑴,则v可能在u的父亲的父亲的另一棵子树内。
而此时LCA(u,v) = father[ father[u] ].
⑶……
不论是哪种情况,LCA(u,v)都与u和father[ ]有某种关系。我们能不能抓住这种关系呢?
我们继续观察,一直向上取father[ ],貌似和并查集的FIND操作很像呢。
我们用并查集的角度依次考虑上面的情况试试看。
1、v在u的子树内。
此时dfs(u)还在栈中,没有执行完,此时没有向上取father[ ],说明此时u是根。
2、v不在u的子树内。
⑴假设v在u的父亲的另一棵子树内。
此时的dfs(u)已经执行完并出栈。此时向上取了一次father[ ],说明此时u的父亲是根。
⑵如果不满足条件⑴,则v可能在u的父亲的父亲的另一棵子树内。
同理,此时dfs(u的父亲)也已经执行完并出栈。此时向上取了两次father[ ],说明此时u的父亲的父亲是根。
⑶……
综上,我们只要保证当dfs(u)在栈中的时候,u是根;当dfs(u)不在栈中的时候,father[u]是根就行了。
LCA算法分为在线算法与离线算法。
在线算法:可以以序列化的方式一个个的处理输入,也就是说在开始时并不需要已经知道所有的输入。
离线算法:在开始时就需要知道问题的所有输入数据,而且在解决一个问题后就要立即输出结果。
在线算法与离线算法都基于DFS。在线算法与RMQ算法(区间最值查询)相关,离线算法与Tarjan算法相关。
在线算法 LCA RMQ的相互转换
各自意义:
LCA:基于有根树最近公共祖先问题。 LCA(T, u, v):在有根树T中,询问一个距离根最远的结点x,使得x同时为结点u、v的祖先。
RMQ:区间最小值询问问题。 RMQ(A, i, j):对于线性序列A中,询问区间[i, j]上的最小(最大)值。
RMQ对于LAC的转换:
设一个长度为N的序列A,按照如下方法将其递归建立为一棵树:
1)设序列中最小值为Ak,建立优先级为Ak的根节点Tk;
2)将A(1…k-1)递归建树作为Tk的左子树;
3)将A(k+1…N)递归建树作为Tk的右子树;
如序列A=(7, 5, 8, 1, 10)建树的结果为:
对于RMQ(A, i, j):
1)设序列中的最小值为Ak,若i<=k<=j,那么答案为k;
2)若k>j,那么答案为RMQ(A1…k-1, i, j);
3)若k< i,那么答案为RMQ(Ak+1…N, i, j);
而RMQ问题可以采用ST算法解决,则将LCA转换为RMQ再根据ST算法解决,时间复杂度为O(nlogn)的预处理+O(1)的查询。
LCA的离线Tarjan算法
基于Tarjan算法与并查集。
Tarjan算法
算法用集合表示一类节点,这些节点跟集合外的点的LCA都一样,并把这个LCA设为这个集合的祖先。当搜索到节点x时,创建一个由x本身组成的集合,这个集合的祖先为x自己。然后递归搜索x的所有儿子节点。当一个子节点搜索完毕时,把子节点的集合与x节点的集合合并,并把合并后的集合的祖先设为x。因为这棵子树内的查询已经处理完,x的其他子树节点跟这棵子树节点的LCA都是一样的,都为当前根节点x。所有子树处理完毕之后,处理当前根节点x相关的查询。遍历x的所有查询,如果查询的另一个节点v已经访问过了,那么x和v的LCA即为v所在集合的祖先。
对于每个节点u,关于它的询问(u,v)只有两种。
1、v在u的子树内。
此时LCA(u,v) = u.
2、v不在u的子树内。
⑴假设v在u的父亲的另一棵子树内。
此时LCA(u,v) = father[u].
⑵如果不满足条件⑴,则v可能在u的父亲的父亲的另一棵子树内。
而此时LCA(u,v) = father[ father[u] ].
⑶……
不论是哪种情况,LCA(u,v)都与u和father[ ]有某种关系。我们能不能抓住这种关系呢?
我们继续观察,一直向上取father[ ],貌似和并查集的FIND操作很像呢。
我们用并查集的角度依次考虑上面的情况试试看。
1、v在u的子树内。
此时dfs(u)还在栈中,没有执行完,此时没有向上取father[ ],说明此时u是根。
2、v不在u的子树内。
⑴假设v在u的父亲的另一棵子树内。
此时的dfs(u)已经执行完并出栈。此时向上取了一次father[ ],说明此时u的父亲是根。
⑵如果不满足条件⑴,则v可能在u的父亲的父亲的另一棵子树内。
同理,此时dfs(u的父亲)也已经执行完并出栈。此时向上取了两次father[ ],说明此时u的父亲的父亲是根。
⑶……
综上,我们只要保证当dfs(u)在栈中的时候,u是根;当dfs(u)不在栈中的时候,father[u]是根就行了。
相关文章推荐
- html标签的属性可以用双引号、单引号或无引号, js语句的分号可以不要------真任性啊
- 使用JavaScript在Canvas上画出一片星空
- 使用JavaScript在Canvas上画出一片星空
- Class类文件结构浅析 http://blog.csdn.net/kobejayandy/article/details/39620833
- 【三层架构】对于三层架构的认识和总结
- 【转载】NIO服务端序列图
- gcdlcm[组合数学]
- mysql - 分组group by、having
- Unity 5 初学
- hdu5605geometry
- android正式包点击Home键切出应用后再点击桌面图标返回导致应用重启问题
- 并行学习总结
- Menu
- 大数据学习路线(自己制定的,从零开始)
- JAVA 内部静态类-避免内存泄漏的原因
- URAL 1792. Hamming Code (枚举)
- 文章标题
- linux uart
- HBase-LSM树理解
- Angular源码理解–启动过程