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