2012/09/06

双队列实现Trie

Trie的每个节点的含义是从root走到当前节点所搜集的边是一个词的一部分,或者是一个合法的词。所以每到一个节点,往下走的可能性最多和字母表里字母的个数相同。最简单的Trie实现是在节点里保持N个指针,N是字母表的大小,当然有些指针是空的。

这种实现可以给节点加一个标志位,用来标识当前节点是一个叶子节点,用于识别查找输入是一个词的一部分而不是整个词的情况。


用双队列实现Trie


Trie可以看成是一个有限状态机,其中的状态是词汇表里的任何一个词的一部分或者整个词,比如假设there在词汇表里,那么th就是一个状态,这个过度函数f满足f(th, e)=the,f(th, z)=th。

如果我们想把一个trie存在一个array里,最直接的想法如下图,例子里字母表为"abcd",假设所有可能的字符串都在trie里。



数组里元素是下一个状态的base index,对于任何一个当前状态,下一个可能状态个数等于字母表的size,这里是4,所以要分配一个size为4的连续block表达,base index就是这个block的起始位置的index,存在代表当前状态的数组元素里,比如所有可能状态bx(其中x=a,b,c,d)的base index已经标注在图里了。

这样存储不能满足不满的trie,比如如果bx状态只有"ba","bb","bc"没有"bd",我们想节省存储空间,那么不给状态bx分配size为4的blcok,分一个size为3的,这会带来一个问题,那么原来应该在"bd"位置存储的就不是"bdx"的base index,而是"cax"的base index。



而这两种情况下那个元素的意义可以这样区分,如果是bd,那么pre-state是xxxxb,如果是ca,那么pre-state是xxxxxc。所以解决的办法是另外弄一个array记录当前状态的pre-state。



查找算法,如果要找bdc
1. base index of state "": base[0]=0

2. state change form "" to "b":

index of state "b" is (i = base[0] + 'b' - 'a')

if( check[i] == "" ) bi = base[i] //(base index of bx)
else failed

3. state change from "b" to "bd":

index of state "bd" is (i = base[bi] + 'd' - 'a')

if( check[i] == "b" ) bi = base[i] //(base index of bdx)
else failed

第三步会返回failed,也就是不在trie里。

区别完整word和前缀的方法是,对最后一个输入字符,check成功,但是base[i]是非法的,比如base队列里初始化的时候为0,如果base[i]==0且i!=0就是非法的。

base队列每个元素有双重意义,元素的index代表state,元素的值代表直接后续状态的base index。所以上面代码里面的check[i] == "b"里面的"b"应该写为base的index,因为"b"的含义是一个state,state和base的index是等价的。改为check[i] == base[0] + 'b' - 'a',例如第3步应改为:

3. state change from "b" to "bd":

pre_i = i // keep the index of state "b"

index of state "bd" is (i = base[bi] + 'd' - 'a')

if( check[i] == pre_i ) bi = base[i] //(base index of bdx)
else failed




构建trie


构建是不断插入字符串的过程,会遇到要插入的位置已经被占了,例如上例中插入"bdc",当到状态"bd"的时候发现位置已经被"ca"占了,现在有两个选择,要么把整个bx block移动到队列最后,要么把cx block移动。谁小就移谁。

移动b block到位置p

base[p + i] = base[b + i]
check[p + i] = check[b + i] //==""

//update check[] of state trade "b" as pre-state

state p+i的后续state block的base index bi=base[p + i],所以base[bi] + j(假设这个index合法)的pre-state变为p+i,也就是check[base[bi] + j] = p+i

// free b block
check[b + i] = 0

2012/08/30

欧拉回路的回朔算法



首先算法不会给出所有的回路,当算法结束的时候,得到的仅仅是一个回路,算法检查边的顺序不同会造成结果的回路不同。把所有的边编号,其中的变量的含义g[node][i]是从node出发第i条边到达的节点。

search可以看成是检查当前节点node有没有未用过的边,如果没有就会返回,调用堆栈往上一层,search( g[node][i] )返回说明,边i到达节点g[node][i],但是这个节点无边可用了,我们把i记下来(回朔),然后看看从node出发的其他边能不能前进,如果假设node出来就一条边,那么算法会再往上返回,上面一层会认为node无边可用了,把探索的时候通向node的边也记下来。

分析算法的运行过程,算法一开始从一个节点开始,只要发现没用过的边就往前走,称这时候为探索模式,一直到没有边可用,注意这时候并不代表所有的边都用过了,还可能是当前节点的连接的边都用过了。没有边可用的时候算法开始回朔,回朔的同时记录边,回朔到某个节点,又有没用过的边,算法又开始往前,直到又没有边可用,开始回朔。最后所有的边都用完的时候,算法会一直到回朔到起点,最后记录的是从起点出来的边。

任何边在算法运行过程中只会被用一次,回朔开始的时候必然是到达了一个以前访问过的节点,因为如果到达一个节点,没被访问过,并且又无边可用,这个节点的度为1,不存在欧拉回路。

第一次回朔是什么时候开始的呢?回朔发生以前,站在任何一个节点看,算法必然是进去出来交替发生,对任何一个非起点节点,一进一出我们把节点的度减掉2,可以看出对于非起点节点是不可能发生进去出不来的情况,因为他们的度都是偶数,所以回朔第一次发生必然是在起点,因为起点是直接出来,度只需要减1。

还有一点就是,虽然第一次回朔发生在起点,在回朔的过程中还是会发现有的节点的degree没用完,算法可能再次进入探索模式。也就是当前节点有边可用进入探索模式,无边可用进入回朔模式。