这道题,主要求一个序列中各个子序列出现的次数,子序列的长度在a到b之间,并且按照一定规则输出,为了简化问题的讨论,我们只考虑求出子序列的出现次数,而不考虑相关的输出问题。

我首先想到的最简单的方法莫过于用一个字典来保存所有的pattern,考虑每一个pattern,如果字典中不包含此pattern,则把它加入字典,并设置出现了1次,以后如果遇到相同的pattern则从字典中取出来把它的出现次数增加1,最后对字典中的pattern排序,以便做相关输出。

(最后部分放上的字典实现的java代码,字典用hashtable实现,不超时)

usaco上给出的解答用了另一种方式来实现,即把pattern看为一个2进制数,那么每个pattern就对应一个整数,可以用一个数组来保存每个整数(pattern)出现的次数,由于00和000属于不同的pattern,而他们表示的二进制是同一个数,所以可以在00和000的前面加上一个‘1’,这样这两个pattern就可以用不同的二进制数来表示了。

上面所说的两种方法,其实只能算是一种算法,只是用了不同的“字典”来实现pattern的存储与查询,第一种方法使用了hashtable来实现“字典”,而第二种方法使用了数组来实现“字典”。

对于用hashtable的方式,优点是被存储的pattern不受到限制,当题目给出的输入序列不是由0和1组成的,而是由26个字母随意组成时(如abcdefg),此时程序的时间要求与空间要求没有什么变化,而此时如果使用二进制方式就要把pattern看成一个二十六进制的数,需要开很大的数组来保存每个pattern出现的次数。

而用数组方式时也有它的好处,那就是“字典”的查询非常快,应此当pattern由少数字母类型组成且长度不算长时开点内存换速度是很值得的。

但以上两种方法都不是主角,下面我给大家介绍一种更好的算法来计算每个pattern出现的次数,它与上面所说的方法没有什么联系。

这个算法使用到了满01二叉树,如下图:
    0         1
   / \        / \
 0    1     0   1
 /\   /\    /\   /\
0 1 0 1 0 1 0 1
…………………………(等等)

这棵树有以下几个特点:
1、它有两个根节点
2、每个节点都有两个儿子,左儿子为0,右儿子为1
3、这棵树的深度为11,根节点的深度为0,共有12层(因为题目限定pattern最长为12)
4、每一层都是满的

(这棵树有什么用呢?建议读者先想想再继续看。)
当有了这棵树时,我们就可以把每个节点看成一个pattern,它由根节点到此节点路径上的点组成,比如第二排的左数第二个可以看成01,第三排的左数第五个可以看成100。因此我们可以把pattern出现的次数放在树的节点中。

等等,我们为什么要这样做呢?或者说,我们根据什么想到了用树来记录出现次数?

关键点是通过观察可以发现一个现象,比如00出现的次数等于000……(省略号表示零个或多个字符)的出现次数加上001……的出现次数再加上00出现的次数。想到这一点就会突然发现,如果知道树中一个节点的左、右儿子出现的次数,那就可以立刻得到此节点的出现次数,也就是说如果求出了叶子节点的出现次数就可以容易地求出所有节点的出现次数。因此我们就可以把精力放在求出长度为12的pattern(叶子节点)出现的次数了,而这个工作仅需要O(L)的时间,L为题目给出的序列长度。

恩,这个方法不错,但我们还需要更多的信息来帮助我们实现这个算法,他们是:

1、如何实现树?用什么实现?Node类+left、right指针?
2、当一个pattern去掉最左边字符并在右边增加一个新字符时如何快速定位到对应的节点?读者想一想。

由于满二叉树的“满”性质,我们可以用一个数组来实现这棵树,比如把节点按照如下方式进行编码:
    0            1
   /  \        /     \
 2     3    4        5 
 /\   /\     /\       /\
6 7 8 9 10 11 12 13
…………………………(等等)
这样每个节点的编码就是他在数组中的位置。

对于第2个问题,当增加一个新字符时,如果增加的是‘0’,那么就向左儿子走,否则向右儿子走。减去左边一个字符时对应先“砍掉”根,然后“丢掉”半边树得到的新树,在新树中的节点位置就是新pattern对应的位置。
这几步都可以在瞬间求出,参看相关代码。

 

 

Code满01二叉树实现

 

 

 

 

Code_hashtable字典实现
posted on 2009-01-16 21:36  刘永辉  阅读(576)  评论(0编辑  收藏  举报