Tip

这是我在北京大学算法设计与分析课上所作的一次课堂报告。

动态表

题目

分别给出使用下列策略的动态表的插入与删除操作的平摊复杂度以及势能函数定义.

  1. 元素满时扩大 1 倍空间, 元素不足 1/3 时减少 1/2 空间.
  2. 元素满时扩大 1/3 倍空间, 元素不足 4/9 时减少 1/3 空间.

动态表的势函数

  1. ;
  2. ;
  3. 在扩张或收缩前, 取极大值;
  4. 在扩张或收缩完成后, 取极小值; 插入/删除的开销为 , 扩张/收缩操作的开销为 . 发生扩张/收缩时, 势函数的下降抵消了扩张/收缩的开销.

问题 1

元素满时扩大 1 倍空间, 元素不足 1/3 时减少 1/2 空间. 装载比变化:扩张:1 → 1/2;收缩:1/3 → 2/3;范围:.

未触发扩张/收缩时, , 平摊代价 .

触发扩张/收缩时,, 平摊代价 . 每个操作的平摊代价为 , 总代价为 .

问题 2

未触发扩张/收缩时, , 平摊代价 .

触发扩张/收缩时,, 平摊代价 . 每个操作的平摊代价为 , 总代价为 .

伸展树

题目

  1. 单旋伸展树的 Splay 操作平摊复杂度不是 ;

  2. 双旋伸展树的 Splay 操作平摊复杂度为 .

def splay(x):
    while x.p is not None:
        if x.p.p is None:
            zig(x)
        elif (x.p.l == x) == (x.p.p.l == x.p):
            zig_zig(x)
        else:
            zig_zag(x)

伸展树的势函数

为以 为根的子树. 单个节点的势函数为

总势函数为

实际上描述了树的平衡程度, 接近平衡时 较小.

zig 操作的平摊分析

对于 zig(x), 记 的父节点为 , 旋转后的节点为 .

平摊代价 .

单旋伸展树

在 Splay 到根节点的过程中访问的节点为 , 其中 为根节点, 为树高.

总的平摊代价为

zig-zig 操作的平摊分析

对于 zig_zig(x), 记 的父节点与祖父为 , 旋转后的节点为 , .

zig_zig(x) 的常数为 , 可以调整势函数

平摊代价

zig-zag 操作的平摊分析

对于 zig_zag(x), 记 的父节点与祖父为 , 旋转后的节点为 , .

zig_zag(x) 的常数为 , 再次调整势函数

平摊代价

双旋伸展树

在 Splay 到根节点的过程中访问的节点为 , 其中 为根节点.

总的平摊代价为

并查集

题目

对给定的 个元素, 进 xing 次并查集操作, 证明:

  1. 只采用按秩合并的并查集的最差复杂度为 .
  2. 采用按秩合并和路径压缩的并查集的平摊复杂度为 .
def find_set(x):
    if x != x.parent:
        # return find_set(x.parent)
        x.parent = find_set(x.parent)
    return x.parent

按秩合并

秩: 为以 为根的树高度的一个上界(如果没有路径压缩, 界是精确的).

def union(x, y):
    x, y = find_set(x), find_set(y)
    if x.rank > y.rank:
        y.parent = x
    else:
        x.parent = y
        if x.rank == y.rank:
            y.rank += 1

引理(秩的性质): .

证明概要: 加强结论为 , 其中 所在的树的大小, 然后使用归纳法.

将大小分别为 的树合并后, 不妨设 , • 若 , 最大秩不超过 ; • 若 , 最大秩不超过 .

因此, .

按秩合并的时间复杂度

  • find_set: ;

  • union:

进行 次操作的总时间复杂度为 .

最差情况下, 每次合并从叶子节点出发, 且每次合并的两棵树高度相同, 此时复杂度为 .

路径压缩的势函数

定义单个节点的势函数:

其中 的具体取值留待之后讨论. 需要满足:

  • , 等号成立当且仅当 为根节点或 .
  • 只和 的父节点的秩有关, 且对 的父节点的秩单调递减.

秩的性质

  • ;
  • 按秩合并: ;
  • 满足 的节点均为叶子节点(可能同时也为根节点);
  • 节点的秩在并查集操作中不会减少.

推论:

  • 路径压缩的过程不会增加节点的势.

union 操作的平摊分析

union(x, y) 拆分为三个操作: find_set(x), find_set(y)link(x, y).

对于 link(x, y), 不妨设 成为 的父节点:

  • 的子节点的势不变;
  • 的子节点的势不增加;
  • 的势减少 ( );
  • 的势增加或不变, 增量不超过 .

平摊代价: .

find-set 操作的平摊分析

对于 find_set(x), 记 的深度为 , 欲说明 find-set 的平摊代价为 , 只需证明在 的祖先中有至少 个节点的势减少了, 如此则有:

引入 , 其值只和 及其父节点的秩有关, 且满足 , 根据鸽笼原理, 至少有 个节点存在一个祖先节点, 其 值与这个节点相同.

根据 的定义, .

, 满足上述条件.

的祖先节点, 且 .

路径压缩完成后, 的父节点变为根节点 , 有

上式没有对证明起到什么帮助, 但注意到上式最后出现的函数迭代形式. 令 , 则

因此路径压缩使得 增大了.

现在讨论 的构造, 需要满足:

  • .
  • 只和 的父节点的秩有关.
  • 的父节点的秩单调递减.
  • 单调递减.

, 则 find-set 的平摊代价为 .