这道题,主要求一个序列中各个子序列出现的次数,子序列的长度在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二叉树实现
/**//*
ID: sdjllyh1
PROG: contact
LANG: JAVA
complete date: 2009/1/16
author: LiuYongHui From GuiZhou University Of China
more articles: www.cnblogs.com/sdjls
*/
import java.io.*;
import java.util.*;
public class contact
{
private static int a, b, n;
private static String sequence;//输入文件给出的序列
private static int[] tree = new int[8190];//tree[index]表示节点index出现的次数,节点深度小于12时(即pattern长度最大为12时)树中最多有8190个节点
private static int[] power = new int[] { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192 };//power[dep]表示2的dep次方
public static void main(String[] args) throws IOException
{
init();
run();
output();
System.exit(0);
}
private static void run()
{
int length = sequence.length();//获得输入序列的长度
int start;//处理pattern的开始位置
int end;//处理pattern的结束位置
int index;//处理的pattern对应树中的节点位置(编号)
int dep;//处理的pattern的对应书中的节点深度
//初始化第一个pattern的相关值
start = 0;
end = Math.min(start + 12, length);
index = getFirstIndex(end);
dep = end - 1;
tree[index] = 1;
//依次处理其它pattern
for (start = 1; start < length; start++)
{
//求出当前pattern的结束位置
end = Math.min(start + 12, length);
//获得去掉根的新位置
index = cutRoot(index, dep, sequence.charAt(start - 1));
//如果当前pattern长度不变
if (end == start + 12)
{
//根据pattern的最后一个字符判断往哪边走
if (sequence.charAt(end - 1) == '0')
{
index = left(index);
}
else
{
index = right(index);
}
}
else//否则长度减少了1
{
//pattern在树中的深度减1
dep--;
}
//记录此pattern又出现1次
tree[index]++;
}
//计算深度小于11的pattern出现的次数,4093表示深度小于11的节点的最大编号
for (index = 4093; index >= 0; index--)
{
tree[index] += tree[left(index)] + tree[right(index)];
}
}
//计算index的左儿子
private static int left(int index)
{
return (index + 1) << 1;
}
//计算index的右儿子
private static int right(int index)
{
return ((index + 1) << 1) + 1;
}
//计算去掉顶层后index节点所在子树中的新位置,dep为当前节点的深度,root为此节点的根(或为‘0’,或为‘1’)
private static int cutRoot(int index, int dep, char root)
{
if (root == '1')
{
dep++;
}
return index - power[dep];
}
//获得第一个pattern的节点编号,len为此pattern的长度
private static int getFirstIndex(int len)
{
int index;
if (sequence.charAt(0) == '0')
{
index = 0;
}
else
{
index = 1;
}
for (int i = 1; i < len; i++)
{
if (sequence.charAt(i) == '0')
{
index = left(index);
}
else
{
index = right(index);
}
}
return index;
}
private static void init() throws IOException
{
BufferedReader f = new BufferedReader(new FileReader("contact.in"));
StringTokenizer st = new StringTokenizer(f.readLine());
a = Integer.parseInt(st.nextToken());
b = Integer.parseInt(st.nextToken());
n = Integer.parseInt(st.nextToken());
StringBuffer sb = new StringBuffer();
String readline = f.readLine();
while (readline != null)
{
sb.append(readline);
readline = f.readLine();
}
sequence = sb.toString();
f.close();
}
private static void output() throws IOException
{
//相关输出略.
}
}
Code_hashtable字典实现
/**//*
ID: sdjllyh1
PROG: contact
LANG: JAVA
complete date: 2009/1/15
author: LiuYongHui From GuiZhou University Of China
more articles: www.cnblogs.com/sdjls
*/
import java.io.*;
import java.util.*;
public class contact
{
private static int a, b, n;
private static String sequence;//输入文件给出的序列
private static Dictionary patternsDic = new Hashtable();//保存pattern的字典(哈希表),用于根据关键字查找pattern
private static List patternsList = new ArrayList();//与patternsDic保存相同的pattern,用于排序。
public static void main(String[] args) throws IOException
{
init();
run();
output();
System.exit(0);
}
private static void run()
{
int length = sequence.length();
//枚举pattern的起始位置
for (int start = 0; start < length; start++)
{
//枚举pattern的长度
for (int len = a; len <= b; len++)
{
//如果没有超出边界
if (start + len <= length)
{
//获得当前pattern
String subsequence = sequence.substring(start,start+len);
pattern p = (pattern)patternsDic.get(subsequence);
//如果这个pattern不在字典中,即是第一次出现
if (p == null)
{
//把此pattern加入字典与列表
p = new pattern(subsequence);
p.occurrence = 1;
patternsDic.put(subsequence,p);
patternsList.add(p);
}
else
{
//否则此pattern的出现次数加一
p.occurrence++;
}
}
}
}
//给pattern们排序
Collections.sort(patternsList);
/**//*
* 排完序后,在output函数中进行输出,按照题目要求进行输出,
* 输出规则有点麻烦,output不注释了。
*/
}
private static void init() throws IOException
{
BufferedReader f = new BufferedReader(new FileReader("contact.in"));
StringTokenizer st = new StringTokenizer(f.readLine());
a = Integer.parseInt(st.nextToken());
b = Integer.parseInt(st.nextToken());
n = Integer.parseInt(st.nextToken());
StringBuffer sb = new StringBuffer();
String readline = f.readLine();
while (readline != null)
{
sb.append(readline);
readline = f.readLine();
}
sequence = sb.toString();
f.close();
}
private static void output() throws IOException
{
PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("contact.out")));
Iterator it = patternsList.iterator();
pattern p = (pattern)it.next();
int occurrence = -1;
int count = 0;
while ((n>0) && (p != null))
{
if (occurrence != -1)
{
out.println();
}
occurrence = p.occurrence;
out.println(occurrence);
count = 1;
out.print(p.sequence);
n--;
if (it.hasNext())
{
p = (pattern)it.next();
}
else
{
p = null;
}
while ((p != null) && (p.occurrence == occurrence))
{
if (count % 6 ==0)
{
out.println();
out.print(p.sequence);
}
else
{
out.print(' ' + p.sequence);
}
count++;
p = (pattern)it.next();
}
}
out.println();
out.close();
}
}
class pattern implements Comparable
{
public String sequence;//pattern的序列
public int occurrence;//出现次数
public pattern(String sequence)
{
this.sequence = sequence;
this.occurrence = 0;
}
public int compareTo(Object p)
{
pattern compareP = (pattern)p;
if (this.occurrence > compareP.occurrence)
{
return -1;
}
if (this.occurrence < compareP.occurrence)
{
return 1;
}
if (this.sequence.length() < compareP.sequence.length())
{
return -1;
}
if (this.sequence.length() > compareP.sequence.length())
{
return 1;
}
return this.sequence.compareTo(compareP.sequence);
}
}