本节由四个小节组成。在前两小节中,我们在概念层面上讨论了Euler算法及其树木生成的关键步骤,为实现细节的讨论做准备。在第三小节中,我们介绍了实现的算法工程细节。在第四小节中,我们描述了uShuffle工具的软件组织和用户界面。为了证明我们的算法选择并解释我们的优化技术,前三小节中的讨论必然是技术性的。对图形算法的理论讨论或算法工程的技术细节不感兴趣的读者可以安全地跳到软件组织和用户界面的第四小节。
欧拉算法
在本小节中,我们将回顾欧拉算法的一些基本概念。
有向多重图
A类k个-下面是k个序列中的连续元素。让S公司是要排列的序列。让T型
k个
是一个保持k个-让计数S公司(例如,T型1是的简单排列S公司,以及T型2是的排列S公司具有相同的二核苷酸计数。)要生成T型
k个
对于k个≥2,欧拉算法[2,15]首先构造一个有向多重图G公司我们参考图1例如。对于每个不同的(k个-1)-引入S公司,G公司有一个顶点。对于每个k个-出租L(左)在里面S公司,其中包含两个(k个-1)-出租L(左)1和L(左)2这样的话L(左)1先于L(左)2,G公司具有来自顶点的定向边L(左)1到的顶点L(左)2.的副本k个-lets可能存在于S公司,因此顶点之间可能有多条边。
排列与欧拉游动的对应
当我们扫描k个-让我们进去S公司一个接一个,我们也走在有向多重图中G公司从顶点到顶点。当所有k个-让我们扫描,每个边G公司只参观一次:步行欧拉学派另一方面,让一个欧拉式的人进来G公司,我们可以通过拼写出(k个-1)沿着行走的顶点的let(并放弃重叠)。自每个k个-让进来S公司对应于中的边G公司,每一次欧拉式的走进G公司对应于具有相同k个-让我们算作S公司坎德尔等人[15]表明,只要欧拉行走在相同的两个顶点开始和结束秒和t吨与开头和结尾相对应(k个-1)-出租S公司,的我-让所有1的计数≤我≤k个被保存了下来。因此,生成统一的随机序列T型
k个
简化为生成均匀随机欧拉步入G公司从秒到t吨.
欧拉步行与树木群落的对应关系
对于一个欧洲人来说G公司,每个顶点v(v)属于G公司除了结束顶点t吨有一个最后边缘e
v(v)
从中退出的v(v)最后一次。除以下顶点之外的所有顶点的最后一条边集t吨形成一个乔木植根于t吨:所有顶点都可以到达的有向生成树t吨.树木繁茂A类植根于t吨,从秒到t吨最后边缘符合A类可以很容易地生成[2,15]:
-
1
对于每个顶点v(v),收集边缘列表E类
v(v)
退出v(v).排列每个边缘列表E类
v(v)
分开保存e(电子)
v(v)
列表上的最后一条边。
-
2
浏览图表G公司根据边缘列表{E类
v(v)
}:开始于秒(套u个←秒),取第一个未标记的边(u、 v(v))从列表中E类
u个
,标记边,然后移动到下一个顶点v(v)(套u个←v(v)); 继续进行,直到所有边缘都标记好,步行结束于t吨.
在有向多重图中,欧拉游动和树状图之间有很好的对应关系:每个树状图都以t吨对应于完全相同数量的欧拉行走[三,15]. 因此,生成均匀随机的欧拉走进去G公司从秒到t吨简化为在G公司植根于t吨在下一小节中,我们将讨论生成随机树状图的算法,其中一些算法又基于随机漫步,这很有趣。
生成随机树丛
在本小节中,我们回顾了现有的树木生成算法,并解释了我们对Wilson算法的选择[19,23]. 生成随机树状图和生成树的主要方法有两种:行列式算法和随机遍历算法。
行列式算法
行列式算法基于矩阵树定理[3,第二章,定理14]。对于图形G公司,特定边缘的概率e(电子)出现在均匀随机生成树中的是两个数字的比率:包含边的生成树的数目e(电子),以及生成树的总数。矩阵树定理允许人们通过计算图的组合拉普拉斯矩阵(或基尔霍夫矩阵)的行列式来计算图的生成树的确切数目。根据边的概率,通过反复收缩或删除边,可以生成随机生成树。
第一个行列式算法是由Guénoche给出的[14]和库尔卡尼[16]:对于的图形n个顶点和米边,可以在中生成随机生成树O(运行)(n个三米)时间。这个运行时间后来被改进为O(运行)(n个三) [7]. 科尔伯恩、梅尔沃尔德和纽菲尔德[8]简化了O(运行)(n个三)时间算法,并表明运行时间可以进一步减少到O(运行)(n个2.376),二乘的最佳上界n个×n个矩阵[9].
随机遍历算法
随机遍历算法使用一种完全不同的方法来生成随机生成树。奥尔德斯[1]和Broder[5](在与Diaconis讨论矩阵树定理之后)独立地发现了随机生成树和随机游动之间的有趣联系:
模拟图中的均匀随机游动G公司从任意顶点开始秒直到访问了所有顶点。对于每个顶点v(v)≠秒,收集边缘{u、 v(v)}对应于第一个入口v(v).集合T型边的一致随机生成树G公司.
对于图形G公司和一个顶点v(v)在其中,定义覆盖时间C
v(v)
(G公司)作为从开始的随机行走的预期步数v(v)需要访问的所有顶点G公司.Aldous-Broder算法的运行时间[1,5]在覆盖时间内明显呈线性。在改变生物序列的背景下,Kandel等人[15]扩展Aldous-Broder算法[1,5]生成覆盖时间内欧拉有向图的均匀随机树。威尔逊和普罗普[24]然后提出了一种在18个覆盖时间内生成一般有向图的均匀随机树状图的算法。
威尔逊算法
威尔逊[19,23]结果表明,使用循环弹出算法(cycle-popping algorithm)可以比覆盖时间更快地生成随机树形图和生成生成树,该算法模拟了循环增强的随机行走。对于图形G公司和两个顶点u个和v(v)在其中,定义击球时间hu个,v(v)(G公司)作为随机行走的预期步数u个到v(v)威尔逊算法的运行时间[19,23]在相应随机图的最大或平均命中时间内是线性的。作为威尔逊[19,23]值得注意的是,平均和最大命中时间总是小于覆盖时间,并且在某些图形中差异可能非常显著。因此,为了生成均匀随机树状图,Wilson的算法[19,23]优于Kandel等人的算法[15].
为了表示的完整性,我们在下面包含了Wilson算法的伪代码[19,23]:
具有根的RandomTree(第页)
1个用于我← 1至n个
2 InTree中[我] ← 假
3个下一步[第页] ← 无
4 InTree中[第页] ← 真的
5用于我← 1至n个
6 u个←我
7而不是InTree中[u个]
8个下一步[u个] ← 随机后续任务(u个)
9 u个←下一步[u个]
10 u个←我
11而不是InTree中[u个]
12 InTree中[u个] ← 真的
2013年u个←下一步[u个]
14返回下一步
让E类
u个
是从顶点退出的有向边集u个.函数RandomSuccessor(u个)选择均匀随机边(u、 v(v))来自E类
u个
,然后返回顶点v(v).
与Aldous-Broder算法不同[1,5],它模拟从根到访问所有顶点的单个随机行走,Wilson的算法[19,23]模拟多个随机行走:从每个未访问的顶点开始,随机行走一直持续到它加入一个最初只包含根的生长乔木。随机行走遵循下一步[·]指针;每当再次遇到以前访问过的顶点时,就会形成一个循环并立即删除,因为下一步[·]指针被覆盖(在第一个while循环中)。当行走到达正在生长的树丛时,行走中的所有顶点都会作为一个分支加入树丛。
两种方法的比较
我们现在比较两种生成随机树状图的方法。Kandel等人[15]证明了欧拉有向多重图的覆盖时间n个顶点和米边缘是O(运行)(n个2米). 根据我们前面对覆盖时间和命中时间的讨论,可以得出Wilson算法的预期运行时间[19,23]最多在同一个多重图上O(运行)(n个2米)同样,忽略了对数n因素。
对于多重图,数字米边的数量可以任意大于n个个顶点。因此,看起来Colbourn等人的行列式算法[8],以确定性运行O(运行)(n个三)时间或甚至O(运行)(n个2.376)时间,将是比随机行走算法更好的选择[15,19,23]. 然而,我们注意到,当米行列式计算的中间值也可以很大。在当今典型的计算机系统中,浮点数的算术运算没有足够的精度来保证行列式算法数值计算的准确性和稳定性。随机行走算法[15,19,23]另一方面,只需要对小整数进行基本运算,而不需要这些数值问题。因此,我们决定实现Wilson的随机遍历算法[19,23]用于乔木生成。
实施详细信息
在本小节中,我们描述了欧拉算法实现的细节[2,15,19,23]用于生成k个-let表示随机序列。
Kandel等人的数据结构
正如Kandel等人建议的那样[15],Euler算法的简单实现[2,15]可以使用大小为的查找表σk个-1尽一切可能(k个-1)-lets作为有向多重图中的顶点G公司,然后构建大小为的邻接矩阵σk个-1×σk个-1对于中的边G公司。当两者同时存在时σ和k个是小常数,这是这种简单方法的空间要求,σ2k个-2个,可能看起来并不严重。然而,计算表明,即使对于σ=20(蛋白质的字母大小)和k个=3(典型的选择k个),所需空间总计
σ2k个-2个=204= 160, 000.
另一方面,蛋白质序列的典型长度低于1000。尽管序列本身可能仅存储在1KB中,但置换算法仍然需要数百倍的空间。当k个进一步增加了:即使是看起来很天真的参数σ=20和k个=5,空间要求
σ2k个-2个=208> 168= 232
超过了32位计算机所能容纳的全部4GB内存!我们注意到Coward的两组参数[11]用于实验他的洗牌程序的只有
σ= 4,k个= 6,σ2k个-2个= 1, 048, 576
和
σ= 20,k个= 3,σ2k个-2个= 160, 000.
我们将在结果和讨论部分对uShuffle和shufflet进行比较,进一步讨论这一点。
线性空间中有向多重图的表示
为了使uShuffle程序具有可伸缩性,很明显,在实现过程中需要仔细的算法工程。正如我们在前面关于欧拉算法的小节中所讨论的那样,有向多重图G公司包含每个不同的顶点(k个-1)-引入S公司.由于(k个-1)-进入S公司确实是我-k个+ 2,G公司最多有个我-k个+2个顶点,因此我-k个+连续之间有1条定向边(k个-1)-出租。这意味着G公司实际上长度是线性的我序列的S公司待置换。有了合适的数据结构,uShuffle只需要线性空间。
在下面,我们首先解释有向多重图的构造和表示G公司然后解释了图构造后随机序列的生成。图的构造包括两个步骤:确定顶点集,然后添加有向边。
确定顶点
我们使用哈希表来确定顶点集。哈希表由一个大小为b条=我-k个+2,数量(k个-1)进入S公司,以及每个bucket处的链接列表,以避免通过链接发生冲突[10]. 每个(k个-1)-出租x个=x个1x个2⋯x个k个-1具有多项式哈希代码
哪里一=/2是黄金比率的倒数;的索引x个到桶数组是
我(x个) =⌊小时(x个)·b条⌋国防部b条.
将哈希表初始化为空,然后尝试插入(k个-1)-逐个进入哈希表。如果(k个-1)-让它是同类中的第一个,它被分配一个新的顶点编号,然后插入到哈希表中;它的序列起始索引S公司也会被记录。如果(k个-1)-let之前已插入,但未插入到哈希表:它的顶点数和序列的索引S公司是从第一个(k个-1)-让它的种类。插入后,我们可以从指定的最大顶点数推断出有向多重图中的顶点总数。然后分配顶点的内存。
添加定向边
为了添加有向边,我们使用邻接列表表示法来避免邻接矩阵的过度内存需求。在邻接列表表达法中,每个顶点需要维护两个边列表:传入边列表和传出边列表。传出边列表是生成欧拉行走所必需的[2]. 当Kandel等人的算法时,传入边列表是生成树状图所必需的[15]使用(如Coward的实现[11])。我们使用威尔逊算法[19,23]用于生成树状结构。正如我们在上一节中所讨论的,Wilson的算法[19,23]比Kandel等人的算法更快[15]. 此外,我们在这里注意到Wilson的算法[19,23]与Kandel等人的算法相比还有一个优点[15]在易于实施方面。而不是一个向后的从终点随机行走t吨达到Kandel等人算法中的所有其他顶点[15],威尔逊算法[19,23]使用多个向前地从每个未访问的顶点随机行走以加入植根于t吨:仅输出边列表就足以生成欧拉行走和树丛。
表示边缘列表和管理内存
为了获得最大效率,我们将每个边列表实现为一个顶点数组。传出边的数量因顶点而异;如果我们为每个顶点分配一个固定大小的数组,那么在最坏的情况下,我们必须使每个数组足够大,以容纳所有边,由此产生的空间需求将变为长度的二次方我序列的S公司。我们当然可以首先计算每个顶点的传出边的数量,然后为每个顶点分配一个足够大的单独数组。然而,这需要我们为每个顶点调用一次相对昂贵的内存分配函数。
在我们的实现中,我们为所有边分配一个大数组(边的总数为我-k个+1),然后将块打包到各个顶点。为了实现这一点,我们首先扫描序列S公司要计算每个顶点的传出边数,请将每个顶点的数组(传出边列表)指向大数组的连续偏移。通过这种优化,内存分配的数量减少到只有4个:一个用于hashtable bucket数组,一个用于(k个-1)-将一个顶点数组和一个边数组作为散列表条目。一旦构造了定向多重图,就可以释放桶数组和哈希表项的内存。
图构造后的序列生成
在构造了有向多重图之后,我们可以分三步生成随机序列。如前一节所述,我们需要首先模拟循环增强的随机游动[19,23]为了生成树状图,接下来排列各个边列表,同时保持最后一条边,然后模拟由边列表引导的欧拉行走,并沿行走输出序列。由于每个边列表都是作为数组实现的,因此可以非常有效地执行置换。沿着行走输出随机序列也很容易,因为每个顶点都保持其在输入序列中第一次出现的起始索引。
uShuffle工具的软件组织和用户界面
在本小节中,我们将描述uShuffle工具的软件组织和用户界面。
C库和命令行工具
uShuffle的初始实现是用C编程语言实现的。uShuffle的C版本由两个组件组成:一个uShuffle库(uShuffle.C和uShuffle.h)和一个命令行工具(main.C)。
在典型场景中k个-为每个输入序列生成let-preserving随机序列。uShuffle程序的图形构建阶段只需对多个输出序列执行一次。为了给用户提供优化选项,我们在uShuffle库中导出了三个接口函数:
void shuffle(const char*s,char*t,int l,int k);
void shuffle1(const char*s,int l,int k);
无效洗牌2(字符*t);
函数shuffle接受四个参数:s是要置换的序列,t是输出随机序列,l是s的长度,k是let大小k个函数shuffle只需首先调用shuffe1,然后调用shuwe2:shuffle1实现有向多重图的构造;shuffle2实现了有向多重图中的循环随机游动和随机序列的生成。随机排列的统计行为在很大程度上取决于随机数生成器。
胆小鬼[11]注意到随机数生成器在各种平台上的默认实现通常不令人满意,所以他使用一种可以说更好的算法实现了自己的生成器。我们注意到,有许多随机数生成算法,并且不断有新算法被提出:一种算法是否优于另一种算法可能非常主观。我们没有限制用户使用特定的实现,而是将默认生成器设置为标准C库中的随机函数,然后导出一个接口函数,以便高级用户自定义生成器:
typedef长(*randfunc_t)();
无效集randfunc(randfunc_t randfunch);
命令行uShuffle工具是uShuff库的最小前端,它演示了库的典型用法。它有以下四个选项:
-
s<string>指定输入序列,
-
n<number>指定要生成的随机序列数,
-
k<number>指定let大小,
-
seed<number>指定随机数生成器的种子。
Java小程序
uShuffle程序被移植到Java编程语言。除了有一个库和命令行工具外,uShuffle程序的Java版本还可以在web浏览器中作为小程序运行。我们参考图2uShuffle Java小程序的屏幕截图1:小程序的界面很小,由三部分组成:顶部的输入文本区域、底部的输出文本区域和中间的控制面板。控制面板包含两个文本字段和一个按钮。最大出租规模k个和数字n个的输出序列可以在两个文本字段中设置。单击“Shuffle”按钮时,小程序从输入文本字段获取输入序列,去掉空白,生成n个保留k个-让我们计数,然后在输出文本区域中输出序列。当n个>1:每个输出序列前面都有一个注释行,其中包含从1到n个.
uShuffle Java小程序将所有输出序列保存在内存中,以便在输出文本区域中显示。当数字n个输出序列和输入序列长度我例如,过大,n个=100000000和我=100,则保存输出序列所需的总内存可能超过Java虚拟机(JVM)的最大堆大小,并且小程序可能会挂起。这不是我们程序中的错误,而是由于JVM的限制;尽管如此,我们还是准备了一个网页来指导用户如何增加JVM的最大堆大小。
C#/Perl/Python版本
uShuffle程序也被移植到C#编程语言。Perl和Python是用于生物信息学的流行编程语言;它们允许与用C编写的程序轻松集成。我们没有在源代码级别将uShuffle程序移植到Perl和Python,而是准备了两个网页来指导用户如何使用uShuff库扩展Perl和Cython环境。