Tarjan’s off-line lowest common ancestors algorithm

来源:互联网 发布:淘宝销量多久更新一次 编辑:程序博客网 时间:2024/06/08 17:16

问题描述

最近遇到一个问题就是判断变量声明是否处于其最小的作用域中,小的作用域基本上是更受欢迎的。与作用域相关的一个问题是“变量是定义在循环内还是定义在循环外[1]”?

int i = 0;while(i < 100){    int var = 4;    i++;}//---------------------or----------------------int i = 0;int var;while(i < 100){    var = 4;    i++;}

上面和下面两种方式,哪一个更好?对于POD类型的变量(非POD类型需要考虑其constructor,destructor以及operator assignment的开销),其实两者没有区别,开销是一样的。这里最重要的区别是scope,C/C++是block scope模型,所以declaration inside-loop就可以有更小的“affected scope”。

For primitive types and POD types, it makes no difference. The compiler will allocate the stack space for the variable at the beginning of the function and deallocate it when the function returns in both cases.

For non-POD class types that have non-trivial constructors, it WILL make a difference – in that case, putting the variable outside the loop will only call the constructor and destructor once and the assignment operator each iteration, whereas putting it inside the loop will call the constructor and destructor for every iteration of the loop. Depending on what the class’ constructor, destructor, and assignment operator do, this may or may not be desirable.

变量更小的scope意味着更安全。但是如何判断变量定义是否处于最小的scope中呢?这个问题可以转变成“在一个多叉树中,求多个子节点的最近公共父节点问题”。

Generally, in computer science, it is encouraged to keep your variables as local as possible in terms of scope. This usually results in much clearer code with less side-effects and reduces chances of someone else using that global variable screwing up your logic).

我们以CoreEngine::ExecuteWorkList()方法为例,该方法对应的Scope Tree示意图如图-1所示。

/// ExecuteWorkList - Run the worklist algorithm for a maximum number of steps.bool CoreEngine::ExecuteWorkList(const LocationContext *L, unsigned Steps,                                   ProgramStateRef InitState) {  if (G.num_roots() == 0) { // Initialize the analysis by constructing    // the root if none exists.    const CFGBlock *Entry = &(L->getCFG()->getEntry());    assert (Entry->empty() &&            "Entry block must be empty.");    assert (Entry->succ_size() == 1 &&            "Entry block must have 1 successor.");    // Mark the entry block as visited.    FunctionSummaries->markVisitedBasicBlock(Entry->getBlockID(),                                             L->getDecl(),                                             L->getCFG()->getNumBlockIDs());    // Get the solitary successor.    const CFGBlock *Succ = *(Entry->succ_begin());    // Construct an edge representing the    // starting location in the function.    BlockEdge StartLoc(Entry, Succ, L);    // Set the current block counter to being empty.    WList->setBlockCounter(BCounterFactory.GetEmptyCounter());    if (!InitState)      InitState = SubEng.getInitialState(L);    bool IsNew;    ExplodedNode *Node = G.getNode(StartLoc, InitState, false, &IsNew);    assert (IsNew);    G.addRoot(Node);    NodeBuilderContext BuilderCtx(*this, StartLoc.getDst(), Node);    ExplodedNodeSet DstBegin;    SubEng.processBeginOfFunction(BuilderCtx, Node, DstBegin, StartLoc);    enqueue(DstBegin);  }  // Check if we have a steps limit  bool UnlimitedSteps = Steps == 0;  // Cap our pre-reservation in the event that the user specifies  // a very large number of maximum steps.  const unsigned PreReservationCap = 4000000;  if(!UnlimitedSteps)    G.reserve(std::min(Steps,PreReservationCap));  while (WList->hasWork()) {    if (!UnlimitedSteps) {      if (Steps == 0) {        NumReachedMaxSteps++;        break;      }      --Steps;    }    NumSteps++;    const WorkListUnit& WU = WList->dequeue();    // Set the current block counter.    WList->setBlockCounter(WU.getBlockCounter());    // Retrieve the node.    ExplodedNode *Node = WU.getNode();    dispatchWorkItem(Node, Node->getLocation(), WU);  }  SubEng.processEndWorklist(hasWorkRemaining());  return WList->hasWork();}

例如我们要确定变量UnlimitedSteps是否处于做小的scope中,我们需要找到UnlimitedSteps在哪个scope中进行了引用,变量UnlimitedSteps使用的地方有两个位置,分别是图-1中的红色scope与绿色scope。只要变量定义所在的scope是所有变量引用所在scope的最近公共父节点,就可以说明变量处在最小的scope中。


Scope Tree

图-1 Scope Tree of The Sample Code

但是现有的LCA算法都是用来求两个节点的最近公共父节点,而变量的引用肯定不止两处,所以需要将多个节点的最近公共父节点问题转化为两个节点的最近公共父节点问题。在Stack Overflow上有一个关于求多个节点最近公共父节点的问题[4],该问题下有人建议递归的求多个节点LCA,例如将lca(A, B, C, D) 转化为 lca(lca(A, B), lca(C, D)),但是这个方式很呆。

实际上,多个节点的最近公共父节点可以转化为最小DFS序节点与最大DFS序节点的最近公共父节点问题[5]

并查集

关于最近公共父节点(lowest common ancestors algorithm,LCA)问题,我暂时只了解了Tarjan的离线方法,这里做个记录,Tarjan基于并查集实现(我的理解看来,Tarjan方法的思想本质不是并查集,而在于树的遍历方式)。

并查集是在线等价类问题[6]

在线等价类问题中,初始时有 n 个元素,每个元素都属于一个独立的等价类。需要执行以下的操作:
1) combine(a, b) 操作,把包含 ab 的等价类合并成一个独立的等价类。
2)find(theElement),确定元素 theElement在哪一个类,目的是对给定的两个元素,确定是否是同一个类。

并查集从字面意思上看就是合并查找集合,并查集是“没有最好的方法,只有合适的方法”最好的体现,并查集操作对象是集合,除了维护某个元素属于某个集合之外不需要维护元素之间的其他关系。并查集的实现有两种[7]quick-find 以及 quick-union,较为常用的方法是基于 quick-union ,而 quick-union 有两个维度的改进,”union by rank” 以及 “path compression“,该方法称作 weighted quick-union with path compression [7][8],感兴趣的可以查看并查集的可视化实现[9]

基于Tarjan的LCA实现基于并查集实现,那么在Tarjan的LCA实现中哪里需要 union 操作,哪里需要 find 操作?

在解释这个问题之前,先介绍一下Tarjan’s LCA的主要思想。Tarjan’s LCA方法需要通过DFS实现,如果要获得 PQ 两个节点的LCA,在DFS遍历过程中(假如先遍历到 P 节点,再遍历到 Q ,并且是中序遍历),那么在遍历完 Q 时,两者的LCA就是 P 节点沿着 parent 域一直向上直到第一个未遍历完成的节点。如图-2所示,root 为节点7, P 节点为 5,Q节点为6,我们沿着 root 节点通过中序DFS遍历该树,在遍历完 Q (序号6)节点时,沿着 P 节点 parent 域向上寻找,找到第一个未遍历完的节点就是节点9,节点9就是节点5和节点6的LCA节点。


DFS of LCA

图-2 Inorder DFS of Tarjan’s LCA

从上图中我们可以看出,{5,0,11}构成一个等价类,等价关系是它们沿着parent域向上寻找的第一个未完成的节点相同,它们的未完成节点是9。同理节点{4,6}构成一个等价类,它们的未完成节点是3。当节点3遍历完成后,3首先与{4,6}合并,因为它们的未完成节点是9,然后{4,6,3}要与{5,0,11},也就是进行 union 操作,整个过程如图-3所示。最后{5,0,11,9,3,4,8}构成一个等价类,它们的共同的未完成节点为7。


Tarjan-union

图-3 Union of Tarjan’s LCA

Tarjan’s LCA中的 union 操作,是在处理完一个节点时,将其与已经处理过的其它同一层的子节点所代表的等价类进行合并。当然上面的描述有些绕口,主要原因在于我们的实现方式不是很自然。更加自然的方法是,每次处理完一个子节点时,将该节点所属的等价类与它的父节点进行 union 操作。这样的方法如图-4所示。

  • 每次访问到新的节点,创建其“等价类”集合。并将其设置为该集合的代表节点。
  • 访问完一个节点时,使用 find 操作查找其“等价类”集合,也就是找到该“等价类”集合的代表节点。
  • 将该节点的“等价类”集合与其父节点的“等价类”集合进行 union。设置新集合的代表节点。


Tarjan's LCA1

Tarjan's LCA2

Tarjan's LCA3


图-4 Tarjan’s LCA

注:粉色椭圆表示一个“等价类”集合。图中的棕色节点表示其对应集合的代表节点。图中绿色为等价集合-并查集森林

union 操作用于在访问完一个节点以后,将该节点代表的“等价类”集合与其父节的“等价类”集合进行合并。find 操作主要用于查找某个节点所在的“等价类”集合。

Tarjan’s off-line lowest common ancestors algorithm

Tarjan’s LCA方法在算法导论中有一些介绍,见[21-3 Tarjan’s off-line least-common-ancestors algorithm],该算法如图-5所示。该算法由Make-Set,Union,Find-Set三个方法组成,该方法的完整实现见Tarjan’s off-line lowest common ancestors algorithm[10],该实现通过path compression和union by rank进行了优化。


这里写图片描述

图-5 Tarjan’s LCA of CLRS

图-5算法是递归实现,但是如果树高很大的话,有可能会爆栈,leetcode-236是关于LCA的一道题,其中有对爆栈的测试,所以最好将该递归实现改为循环。

future blog:

  • LCA的其它算法
  • 递归改循环的实现

[1]. Is there any overhead to declaring a variable within a loop?
[2]. What is the difference between declaring a variable outside of loop and declaring static inside loop?
[3]. Is it better to declare a repeatedly-used variable inside a loop or outside?
[4]. Least common ancestor of multiple nodes in DAG?
[5]. 如何在 DAG 中找多个点的 LCA ?
[6]. 萨尼. 数据结构, 算法与应用: C++ 语言描述[M]. 机械工业出版社, 2015.
[7]. Union-Find Algorithms
[8]. Disjoint-set Data Structures
[9]. UNION-FIND DISJOINT SETS (UFDS)
[10]. Tarjan’s off-line lowest common ancestors algorithm
[11]. 236. Lowest Common Ancestor of a Binary Tree

原创粉丝点击