动态树简介 - 严格log n LCT(预备篇)

又是两周没更……但是这篇paper实在是有点精神污染(也可能是我太菜了),至于动态图算法的中的话,有一篇还挺长的证明窝要慢慢看……先把我一直很想看的这个填了好了qaq。


在严格log n LCT中,我们不能再使用splay来作为维护实链的数据结构,因为无法解决splay的均摊问题,而为了 O(\log n) 的复杂度,我们也不能使用普通的平衡树。不过其实Tarjan一开始的版本并不是用splay作为其数据结构的,本篇介绍的就是最初的数据结构:Biased Search Tree(我也不知道叫什么,简称BiST好了)。

我们要处理的问题是:令BST中的每个节点有频率 w_i ,令 W=\sum w_i ,如果我们能做到深度 d_i=O(\log(W/w_i)) ,我们就做到了渐进意义上的最优解。(关于这个问题的静态版有 O(n\log n) 的算法,用贪心可以在 O(n) 的时间内求出距最优解只差常数的解)


Biased 2,b Tree

平衡树我们选择比较经典的2,b Tree(不需要所有叶子都深度一样),在此基础上进行修改。

对于这个问题,我们可以用类似轻重链剖分的方法,定义

s(x)=\begin{cases} \lfloor\lg w_i\rfloor & \text{$x$ is a leaf containing item $i$} \\ 1+\max\{s(y)|y \in child_x\} & \text{$x$ is an internal node} \end{cases}

显然若y是节点x的儿子, s(y)\leqslant s(x)-1 ,若 s(y)=s(x)-1 则y是x的重儿子,否则为轻儿子。

为了保证节点的深度,我们要求一个与轻儿子相邻的儿子都要是重叶子。容易证明这样深度是符合要求的(一个点向上爬的时候,要么是第一次(重叶子),要么就是轻儿子或两个重儿子,都可以保证size*2)。


一些简单的定义:

一个2,b树节点有2~b个儿子,叶节点即没有儿子的节点,我们把所有权值放在叶节点上。

一个节点的儿子按顺序排列,左儿子是最左的儿子,右儿子是最右的,相邻的儿子即一个节点左面一个(如果有的话)和右面一个(如果有的话)兄弟节点。节点x的L-path就是从x开始一直走左儿子得到的路径,R-path类似。

一个子树的leftmost leaf是dfs序中最左的,rightmost leaf类似。一个点x的left neighboring leaf是点x子树中leftmost leaf在dfs序中的上一个叶节点(如果有的话),right neighboring leaf类似。


Worst-case Join

对于合并 u,v ,我们可以直接令a是u的R-path,b是v的L-path,直接按照rank归并两条路径就可以在 O(d_u+d_v) 的时间复杂度内完成。


Amortized-time Join

试着写一写:

def merge(x, y):
    if s(x) == s(y) or (s(x) > s(y) and x.isLeaf) or (s(x) < s(y) and y.isLeaf): # Case 1
        return newNode([x,y])
    else if s(x) > s(y):
        u = x.ch[-1]
        del x.ch[-1]
        v = merge(u, y)
        if s(v) < s(x): # Case 2A
            x.ch.append(v)
            return x
        else: # Case 2B
            x.ch.extend(v.ch)
            l = len(x.ch)
            if l <= B:
                return x
            pos = (l+1)/2
            a = newNode(x.ch[0:pos])
            b = newNode(x.ch[pos:])
            return newNode([a,b])
    else: # 此处同 Case 2 略去


分析一下代码:

首先是儿子的数量,Case 1是2个,Case 2A是原来的子节点数量,Case 2B是2个,新建的节点a,b的数量显然都是合法的。

然后是重儿子,Case 1要么是两个重儿子要么是一个重叶子和一个轻儿子,Case 2A完全不变,重点在Case 2B。

显然merge(u,y)时只有可能是Case 1 or Case 2A,但如果是Case 2A的话不可能s(v)==s(x),因此v一定是有两个儿子u和y,等同于x.child在右面加了节点y。如果u是轻儿子,那么y和u的左兄弟一定都是重叶子,如果y是轻儿子u一定是重叶子,如果u的左兄弟是轻儿子u一定是重叶子,因此split前一定是合法的,于是分裂后也一样合法。

分析一下复杂度(我们希望的复杂度是 O(|s(x)-s(y)|) ):

每一个轻儿子上预存 s(x)-s(y)-1 的势能,每次操作的时候预先得到 s(x)-s(y)+1 的势能。

Case 1: O(1) 代价,势能加 s(x)-s(y)+1

Case 2A: O(1) 代价,从u取出 s(x)-s(u)-1 势能,要在v上放 s(x)-s(v)-1 ,若s(y)>=s(u),则 s(y)-s(u)+s(x)-s(v)+1\leqslant 2s(x)-s(y)-s(u) ;若 s(y)<s(u) ,则 s(u)-s(y)+s(x)-s(v)+1\leqslant 2s(x)-s(y)-s(u)

Case 2B: O(1) 代价,分裂的时候势能没有改变,分析同2A。


Amortized-time Split

我们使用一种比较简单的合并策略:

def split(u, val):
    if u.isLeaf:
        return u, nil

    i = u.ch.findIndex(val)
    v = u.ch[i]

    L, R = split(v, val)

    left = newNode(u.ch[0:i])
    right = newNode(u.ch[i+1:])

    return join(left, L), join(R, right)

分析一下复杂度(w是权值为val的节点):

递归调用需要s(v)-s(w)的势能,s(left)=s(u)或s(u)-1,我们可以支付s(u)-s(v)+1的势能把left, L合并,右面也是类似的。

总体比较显然可以在均摊 O(s(u)-s(w)) 的时间内完成。

Worst-case Split

这个时候分割就很难受了,因为我们既不能用均摊的Join,也不能像Worst-Case Join那样暴力(因为我们要做到 O(s(u)-s(w)) 的复杂度)。

我们采取和Amortized-time Split一样的分割策略。

为了保证复杂度,我们加强我们的要求,我们要求x的轻儿子y的left neighboring leaf和right neighboring leaf都要满足 s(leaf)\geqslant s(x)-1 。(显然满足这个条件一定满足之前的条件)

上图为一个满足要求的树,数字代表s(x)

q=left

我们首先验证分割之后是不是合法的,用归纳法,假设L是合法的,且L的L-path是原树中的一条路径(除L本身),显然此时q是合法的,且q的R-path是原树中的一条路径(除left本身)。因为v在原树中不是叶子,因此其左兄弟必然不是轻儿子,这意味着 s(q)=s(u)-1s(q)=s(u) 。显然 s(L)\leqslant s(v)<s(u) ,于是有 s(L)\leqslant s(q) 。由于我们规定的性质,我们从q一直走右儿子直到节点t使得 s(t)\leqslant s(L) ,显然t的所有祖先都是重儿子(因为L子树中的Leftmost leaf的rank<=s(L)),而且要么t是重儿子要么L是叶子。于是这里合并的复杂度显然是 O(s(q)-s(l)) (递归找到t,然后让L和t变成兄弟)。而且“L的L-path是原树中的一条路径(除L本身)”这一条也显然证出。t的祖先都是重儿子所以不会有任何问题,而L和t子树中的情况,和在原树中是一样的,没有发生改变,因此一定是合法的。

最后我们处理复杂度,设我们第i步的时候的变量是 u_i,q_i,L_{i-1} ,那么显然有 s(q_i)\leqslant s(L_i)\leqslant s(u_i)\leqslant s(u_{i+1})-1\leqslant s(q_{i+1})\leqslant s(u_{i+1}) 。那么对 i>1 显然有 s(q_{i+1})-s(l_i)=O(s(u_{i+1})-s(u_i)) ,而对 i=1,q_1=l_1 ,若 s(q_1)\geqslant s(u_1)-1 ,那么有 s(q_2)-s(l_1)\leqslant s(u_2)-s(u_1)+1 ,否则显然 q_1u_1 的轻儿子, q_1 的 left neighboring leaf显然是 q_2 子树内的rightmost leaf,从而合并时 s(t)\geqslant s(u_1)-1 ,而 s(q_2)-s(u_1)=O(s(u_2)-s(u_1)) ,于是可以得到在y处把x的子树分隔开的复杂度是 O(s(x)-s(y))


Worst-case Join Revisited

最后再来回顾一下worst-case join的实现。

//代码待填

复杂度不需要证明了,只需要证明正确性即可。

会出现问题的只有u的R-path和v的L-path相邻的轻儿子,u子树中的left neighboring leaf和v子树中的right neighboring leaf都没有改变。令新树为join(u,v)的时候不切开过大child的树,考虑u的R-path上的一点q(令其新树中的父亲为w),令v的L-path上相邻两点g,f满足 s(g)\leqslant s(v)<s(f) (如果g不存在显然v的子树是合法的),而s(w)<=s(f),则g也是轻儿子,而g的right neighboring leaf等于q的left neighboring leaf,而这个叶子的rank必然>=s(u)-1(可以对g归纳),得证。切开过大child不会影响合法性。


后记:看了好久好久,玩命写完以后已经有点神志不清了……感觉一直都没怎么get到point,论文里也完全没说为什么要这样……还缺点图描述的也不太清楚,慢慢补吧……

编辑于 2017-09-19

文章被以下专栏收录