并查集的复杂度
路径压缩并查集
路径压缩并查集是指把访问过的点都连到根上的并查集。
按秩合并给每个点 \(rnk(u)\) 值,初始为 \(1\),若树根 \(x,y\) 合并,把 \(rnk\) 较小的合并到较大的上,同时 \(rnk\) 不改变。若 \(rnk(x)=rnk(y)\),则把 \(x\) 合并到 \(y\) 上,并使 \(rnk(y)\leftarrow rnk(y)+1\)。换句话说就是维护深度。
众所周知,路径压缩+按秩合并并查集的复杂度是 \(O(\alpha(n))\) ,而 \(\alpha\) 的是反阿克曼函数。反阿克曼函数定义基于阿克曼函数 \(A(n,m)\),定义如下:
其中 \(n\not =0\) 时总共进行 \(m+1\) 次函数运算。
换句话说,\(A(0,m)\) 的功效就是给 \(m\) 加一后返回;\(A(1,m)\) 的功效则是进行 \(m+1\) 次加一,\(A(2,m)\) 则是 \(m\) 次调用 \(A(1,m)\),每次调用时加一的次数会快速增加。
增长速度快到 \(A(4,1)=33554431\)。而反阿克曼函数 \(\alpha(n)\) 的含义就是最大的 \(x\) 使得 \(A(x,1)\le n\),可想而知不会超过 \(4\)。但是虽然它很小以至于并查集近似 \(O(1)\),我还是很好奇并查集和阿克曼函数怎么联系起来!
尝试证明
用势能分析的话,先考虑像 Splay 一样把子树深度 \(de\)、子树大小 \(size\) 什么的定义为势能。但是假设一棵满二叉树,从某个节点开始 find 的话,并没有什么点的子树深度或子树大小被改变。诚然,这样最多经过 \(\log\) 个点,但是似乎不能把这种分析和势能分析混用的样子。
考虑把各个点的深度当做势能,这样更改不仅仅会涉及到祖先,还会涉及到后代,感觉没法做。
之后意识到,势能函数可能要在父子间定义。
再尝试证明
讲道理,启发式合并比按秩合并直观吧!那就假设是启发式合并。
设计 \(\phi(u)=[size(fa)\le 2\times size(u)]\)。也就是说如果我父亲的子树大小不大于我的二倍,就会累计势能,总势能是每个点 \(\phi(u)\) 的和。
那么 find 操作时,一系列点的父亲变成根,\(size(fa)\) 肯定会变大,\(size(u)\) 则减小,哪怕算作 \(size(u)\) 不变,势能也应该变小,听起来挺有道理)
势能分析需要满足:势能有下界;每次操作增加的总势能被限制;每次被算入复杂度的运算都会有总势能减少。
首先本势能函数肯定有下界 \(0\)。其次令根节点 \(\phi(root)=1\),则合并操作势能不会增加。find 操作虽然不一定访问的每个点都会 \(\phi(u)=1\) 变为 \(\phi(u)=0\),但一开始 \(\phi(u)=0\) 的只有 \(O(\log n)\) 个节点,这个数量是被限制的。那么可否视为访问的每个点都令总势能减一,然后这 \(O(\log n)\) 个节点在本次操作内使势能增加 \(O(\log n)\),不就符合条件了吗?
但是哪怕这样也不行,因为那些一开始 \(\phi(u)=1\) 的点,很可能路径压缩后仍然 \(\phi(u)=1\)。比如这个情况:

而且退一万步,哪怕这个势能分析成立,复杂度也是 \(O(\log n)\) 相当于只是启发式合并。
宽泛的证明
上面那个图,合并后 \(size\) 并没有倍数级别的变化,仅仅是加上 \(O(1)\)。我们希望这个微小的变化也能引起势能的减小,这提示我们把儿子和父亲的子树大小差也算进来,如下设计:
这里用 \(size(u)\) 减的目的是保证非负。正是由于刨除了第二种点,我们才能这么做。
总势能仍然定义为 \(\sum \phi(u)\)。这样仍然能保证下界 \(0\)、合并操作势能不增。
注意是启发式合并。
一次 find 操作时,仍然经过 \(O(\log n)\) 个 \(\phi(u)=0\) 的节点,把它们看做势能增加 \(\log n\)。这样假设一次访问了 \(tot\) 个节点,如果剩下的 \(tot-\log n\) 个节点势能均会减少至少 \(1\) 我们的假设就成立了。
而事实上 \(size(u)\) 不会增加,\(size(fa)\) 一定会增加,所以符合条件!
那么我们用势能分析证明了复杂度小于等于 \(O(\log n)\)。接下来要考虑缩小这个界。
限制更紧的证明
让 \(size(fa)>2\times size(u)\) 的点 \(\phi(u)=0\) 的话它们太多了。
考虑这类 \(size(fa)>2\times size(u)\) 的点我们也记录一个能表示“\(size(u)\) 和 \(size(fa)\) 差了多少”的值,就像前一个势能函数的第三部分 \(size(u)-(size(fa)-size(u))\) 一样。
我们设这部分点的势能函数 \(\phi(u)=X-max(k)\{size(u)\times 2^k\le size(fa)\}\)。这里先不管 \(X\) 那一项,目前把它看成一个为了让势能函数非负而加的极大值即可。
后面把这个 \(k\) 称作 \(k(u)\)。
这样一次 find 操作中经历的所有这部分点,除了最上面的一个,也均会势能函数减小。原因是每有一个这样的点,表示跳到父亲子树大小至少 \(\times 2\),而对于 \(\phi(u)=X(u)-k(u)\) 的一个此类点 \(u\),若 \(u\) 上面存在一个点将子树大小乘二,则 \(size(u)\times 2^{k+1}\le size(fa)\times 2\le size(root)\),也就是说 \(\phi(u)\le X(u)-k(u)-1\)。
这样好像就只有 \(O(1)\) 个点会势能不变了耶!
但是这样一类点可能会转化为二类点,比如原先 \(size(u)=3,size(fa)=4,size(root)=24\),\(u\) 在被路径压缩后由一类点 \(\phi(u)=2\) 变为 \(\phi(u)=X(u)-3\)。这样的点有 \(O(\log n)\) 种,导致分析后还是 \(O(\log n)\),必须要避免。也就是说要让一类点的势能全部大于二类点的势能。注意到一类点 \(u\) 满足 \(0\le\phi(u)<size(u)\),那么可以使一类点 \(\phi(u)=size(u)-(size(fa)-size(u))+X(u)\) 就有 \(\phi(u)>X(u)\) 了。
但是此时,合并操作在 \(size(x)\leq size(y)\) 的情况下将 \(x\) 的父亲设置为 \(y\),此时 \(x\) 会变为上述的那种点,势能将增加 \(\phi(x)=X(x)-k(x)\)。我们考虑把根的势能改成合并后 \(x\) 点的势能一定达不到的极大值 \(\phi(u)=size(u)+X(u)\),这样合并操作后 \(\phi(x)\) 会减少,不用考虑了。但是 \(\phi(y)\) 则会增加 \(size(y)+X(y)-size(x)-X(x)\)。
到底如何设置 \(X\),考虑此时我们还没用到启发式合并的性质。
由于这里有 \(size(y)-size(x)\) 没法保证,考虑把启发式合并换成按秩合并,把上文中所有的 \(size\) 换成 \(rank\)。由于 \(rank\) 也单调增,所以不会有什么区别。
这样我们上文所说的提示就是 \(X\) 是 \(rnk\) 上加点什么,这样如果 \(X(u)=rnk(u)\) 的话,由于启发式合并的性质 \(rnk\) 最多加一,可以保证合并操作的势能增加在 \(2\) 以内。
这时唯一的问题就是 \(rnk(fa)>rnk(u)\times 2^{rnk(u)}\) 的话 \(\phi(u)<rnk(u)\),所以只能把它强行设置为 \(0\),但这会导致 find 到这个点时势能没变化。此时:
并没有什么卵用。因为路径上仍然可能有 \(\log n\) 个点 \(\phi(u)=0\)。
阿克曼函数
只要把 \(\phi(u)=0\) 的部分再应用下一层运算即可。
首先我们刚刚定义 \(k(u)=max(k)\{rnk(u)\times 2^k\le rnk(fa)\}\)。用指数幂是因为指数幂具有“结合律”,就是说 \(2^a\times 2^b=2^{a+b}\)。而每次乘二是因为要让当时所有 \(rnk(fa)>2\times rnk(u)\) 的点 \(k(u)\ge0\)。但事实上函数运算都是有结合律的,而我们需要覆盖的最小的 \(size(fa)=size(u)\times2+1\),不妨设函数 \(A(1,n)=n\times 2+1\),那么 \(k(u)=max(k)\{A(1,A(1,\dots A(1,rnk(u))\dots))\le rnk(fa)\}\),其中函数嵌套 \(k\) 次。
那么现在 \(\phi(u)=0\) 的最小值就是嵌套 \(rnk(u)+1\) 层,换句话说就是阿克曼函数 \(size(fa)= A(2,size(u))\) 的点。那么我们再记录 \(k'(u)=max(k)\{A(2,A(2,\dots A(2,rnk(u))\dots))\le rnk(fa)\}\),其中嵌套 \(k\) 层。
以此类推,第 \(q+1\) 层记录 \(A(q,rnk(u))\) 嵌套了几层。
设计势能函数 \(\phi(u)\) 时,后面的层势能函数要严格小于前面的层,故而这次多加层数 \(rnk(u)\) 即可。层数就是最多嵌套多少次会函数值达到 \(n\),也就是反阿克曼函数 \(\alpha(n)\)。
这里从 \(0\) 层开始数,数学描述一下就是:
其中嵌套 \(k\) 层。
注意这里 \(rnk(u)\ge1\) 所以肯定不会取无穷大。以及下界仍然是 \(0\)。
合并操作,每次把 \(x\) 连到 \(y\) 只会让 \(rnk(y)\gets rnk(y)+1\),势能增加为 \(\alpha(n)\)。
查询 find 操作,每次假设操作 \(tot\) 个点,只有每层的最后一个点,总共 \(O(\alpha(n))\) 个点势能不会变化,其它的要么变为势能严格变小的更高层,要么由于祖先有一个同层节点而使 \(k\) 值变化导致势能减小。这样此操作势能减小 \(tot\),再增加 \(O(\alpha(n))\),非常正确。
所以均摊复杂度就是 \(O(\alpha(n))\) 了。
后记
感觉还是没弄明白阿克曼函数咋来的,但是我尽力了。
阿克曼函数之所以要迭代前面的函数值是因为 \(rnk(fa)=A(q,rnk(u))\) 的点恰好是没能被上一层填好 \(\phi(u)\) 的点;要迭代 \(m\) 层是因为每一层都是 \(rnk\) 保证了合并的复杂度。这么看两个限制分别来源于合并和查询、分别从两个方向限定了函数的增长,感性上就非常对啊)))
另,把按秩合并换成启发式合并真对吗?那个 OIwiki 上的证明我也是看不懂了。
算了就这样吧,等以后再学学势能分析)

浙公网安备 33010602011771号