Wilson 的 Uniform Spanning Tree 算法

若干年前,我花了很久才看懂 Wilson 算法,若干年后才发现其实当初没真看懂。本文重述我对这个算法的理解。

 

什么是 Wilson 算法

 

给定一个简单连通图,怎样在它的所有生成树中等概率地任选一种? 我们熟悉的 Prim 算法、Kruskal 算法得到的都不是完全随机的生成树,目前已知最快的算法是 Wilson 算法,它借助于随机游动来实现。

 

 Wilson 算法:设 $G$ 是一个有限的连通的图。

1. 任取一个顶点 $r$,维护一个树 $T$,初始时 $T=\{r\}$。
2. 任取一个不属于 $T$ 的顶点 $v$,从 $v$ 出发作图上的随机游动,一边走一边随时擦掉路径中的圈(loop erased random walk),即每当走到一个以前访问过的顶点 $x$,则两次访问 $x$ 之间的路径都被擦掉,直到与 $T$ 相遇为止,这样得到一条从 $v$ 到 $T$ 的不含圈的路径 $p$,把 $p$ 加入到 $T$ 中。
3. 重复此步骤直到 $T$ 包含所有的顶点,这就得到了一棵服从一致分布的生成树。

注意连通性保证了算法会以概率 1 结束。 

 

俗话说 "一图抵千言",没有什么描述能比一个直观的演示来的更生动:http://bl.ocks.org/mbostock/11357811

 

Wilson 算法的描述很简单,但是单从描述上完全看不出算法的正确性来。Wilson 的证明相当有技巧性,而且有一些晦涩的部分,我会一一解释。

 

Wilson 算法正确性的证明


1. 对每个 $v\ne r$,定义一个栈结构 $S_v=\{S_{v,1},S_{v,2},\ldots\}$,这里每个 $S_{v,i}$ 都是随机变量,其取值为 $v$ 的邻居,所有的 $\{S_{v,i},v\ne r, i\geq 1\}$ 都是独立的。顶点 $r$ 的栈是一个空栈:$S_r=\{\emptyset\}$。我们把每个栈的第 $i$ 个元素 $S_{v,i}$ 染成颜色 $i$。

  

2. 在任何时刻,这些栈 $\{S_v,v\ne r\}$ 的栈顶元素都定义了一个有向图 $G_S$:$v\rightarrow u$ 当且仅当 $u$ 是 $S_v$ 的栈顶元素。每个 $v\ne r$ 的出度都恰好是 1,顶点 $r$ 的出度是 0,于是若 $G_S$ 不含回路的话它就是一棵以 $r$ 为根的生成树(所有顶点都指向 $r$)。

 

3. 设 $C$ 是 $G_S$ 中的一个回路,我们可以将其 "弹出"(cycle popping):对每个 $v\in C$,弹出 $S_v$ 的栈顶元素(于是若当前 $S_v$ 的栈顶元素为 $S_{v,i}$,则弹出 $S_{v,i}$ 以后栈顶元素变为 $S_{v,i+1}$),这样得到一个 "更新的" $G_S$。反复地进行回路弹出的操作,结果有两种可能:要么经过若干次操作以后,得到的 $G_S$ 不再含有回路,这时 $G_S$ 是一个生成树;要么总是有回路冒出来,操作永不停止。

注意弹出 $C$ 以后可能又引入了新的回路,一个(无色的)回路可能会出现许多次,但是每个染色回路至多只能被弹出 1 次。

 

4.  擦圈的随机游动可以看做是一种特殊的 "弹出回路" 的过程。这是因为一个随机游动 $X$ 可以用栈 $\{S_v\}$ 来描述,假设把 $X$ 中的圈都擦掉,得到的无圈的随机游动记作 $Y$,则 $Y$ 对应的栈 $\{\widetilde{S}_v\}$ 就是对 $S$ 进行某种 "回路弹出" 操作后的结果。(想象回路是随机游动中的一些多余的无用路径,擦圈就是删去这些多余路径。相应地回路弹出就是清理栈中这些多余的信息)

 

5. 假设已知了所有栈 $\{S_v,v\ne r\}$ 的状态(即所有的 $S_{v,i}$ 都是确定的量,不再是随机变量),而且我知道存在一种操作顺序,依次弹出染色的回路 $C_1,\dots,C_n$ 以后可以得到一棵生成树,现在让你来操作,你是否也能得到一棵生成树? 你需要几次弹出操作? 你最终得到的生成树和我的一样吗?

答案都是肯定的:只要你每次任选一个回路操作即可。不仅如此,你也一定恰好使用 $n$ 次弹出操作,而且你弹出的回路 $\{D_1,\ldots,D_n\}$ 与我的 $\{C_1,\ldots,C_n\}$ 是相同的(这里的相同包括了颜色的相同),仅仅顺序可能不同,最后得到的生成树也是相同的。我把这个结论的证明留给你(几句话足矣)。

 

6. 现在我们来说明 "回路弹出" 操作得到的生成树 $T$ 确实服从一致分布。设 $\mathcal{C}=\{C_1,\ldots,C_n\}$ 是一组染色的环,$T$ 是任一生成树,那么弹出的回路恰好是 $\mathcal C$ 并且最终得到的生成树恰好是 $T$ 的概率是多少? 这个概率就是 $\mathcal C$ 和 $T$ 中的边各自指向正确位置的概率:

$$P(\mathcal{C},T)= \prod_{e\in \mathcal{C}\cup T} p_e=\Phi(T)\cdot \Phi(\mathcal{C}).$$ 这里 $\Phi(\bullet)$ 返回集合 $\bullet$ 中所有边的概率的乘积。
设 $\mathcal{C}_T$ 是所有可能导致 $T$ 的回路 $\mathcal{C}$ 组成的集合,在上式两边对 $\mathcal{C}_T$ 求和,则
$$p(T)=\left(\sum_{\mathcal{C}\in\mathcal{C}_T}\Phi(\mathcal C)\right)\cdot\Phi(T).$$ 但是注意 $\mathcal{C}_T$ 对所有 $T$ 都是一样的:就是全部的 $\mathcal{C}$ 组成的集合,这是因为在给定 $\mathcal{C}$ 后,任何生成树 $T$ 都有可能出现,因此
$$ p(T) ={\rm const}\cdot \Phi(T).$$ 而 $\Phi(T) = \prod\limits_{v\ne r}\frac{1}{d_v}$ 是一个与 $T$ 无关的量,从而 $p(T)$ 是常数,这就证明了 Wilson 算法的正确性。

 

再具体解释下为什么给定 $\mathcal{C}$ 以后任何 $T$ 都可能出现,打个比方,这其实就是一个向弹夹里面压子弹的过程:我们把树 $T$ 放在栈顶,然后依次用 $C_n,\ldots,C_1$ 将 $T$ 往下压,得到栈 $\{S_v\}$,对这个状态执行回路弹出,显然依次弹出的就是 $C_1,\ldots,C_n$,得到的是树 $T$。

 

以上是 Wilson 算法的证明思路,省略了一些不太重要的细节,读者可以参考 Wilson 的原文或者 Lyons 和 Peters 合著的 Probability on trees and networks 一书。

 

Wilson 算法的 Python 实现

 

Wilson 算法实现起来很简单,只有十几行代码,然而它为什么可行却需要一番思考。下面的  UST()  函数,输入的图  graph  是一个字典结构,输出的生成树也是字典结构,形如  {子节点: 父节点} 。

 

#coding = utf-8
import random
import matplotlib.pyplot as plt
import networkx as nx

"""
Wilson 一致生成树算法:
这个算法的关键概念是 LERW (loop erased random walk).
1. 从图 G 中任选一个顶点 v, 维护一个树 T, 初始时刻 T = {v}.
2. 从任一不属于 T 的顶点 u 出发作随机游动, 直到这个游动与 T 相遇为止, 设经过的路径为 p, 将 p 中的回路都擦除, 得到一条无回路的路径 q=LERW(q), 将路径 q 加入到 T 中.
3. 重复以上步骤直到 G 的所有顶点均属于 T 为止, 这时得到的 T 就是一颗一致生成树.
"""


n = input('Enter grid size: ')
G=nx.grid_2d_graph(n,n)

root = random.choice(G.nodes()) #任选一个顶点作为根节点
tree = set([root]) 
parent = dict() 

for vertex in G.nodes(): # 对 G 的每个顶点
    v = vertex
    while v not in tree: # 只要它还不在生成树内 
        neighbor = random.choice(G.neighbors(v)) # 任选其一个相邻顶点
        parent[v] = neighbor  # 记录下走的路径
        v = neighbor # 并且走到这个新顶点. 注意如果过程中出现绕了一个圈又回到某个顶点 v 的情形, 则 parent[v] 会更新为新的路径, 即沿着 v-->parent[v] --> ....行走, 圈是被跳过去的, 这就是擦圈!
    v = vertex
    while v not in tree:
        tree.add(v)   
        v = parent[v]  

fig = plt.figure(figsize=(5,5))
plt.subplots_adjust(left=0,right=1,bottom=0,top=1)
ax = fig.add_subplot(111,aspect='equal')
ax.axis('off')
ax.axis([-1,n,-1,n])

for key, item in parent.items():
    a,b = key
    x,y = item
    ax.plot([a,x],[b,y],'r-',lw=4)

x,y = root
ax.plot(x,y,'bo',ms=10) # 标记出选择的根节点
#plt.show()
plt.savefig('UST_on_Grid_{}.png'.format(n))
View Code

 

程序结果如下图:

 

你可能感兴趣的:(Wilson 的 Uniform Spanning Tree 算法)