(p.s: 前段时间因为找工作和论文的关系,很久没有更新 ,今天起再次开更。。。。 )
题目来源:
HihoCoder1403 题目来源:
题目要求:
小Hi平时的一大兴趣爱好就是演奏钢琴。我们知道一个音乐旋律被表示为长度为 N 的数构成的数列。
小Hi在练习过很多曲子以后发现很多作品自身包含一样的旋律。旋律是一段连续的数列,相似的旋律在原数列可重叠。比如在1 2 3 2 3 2 1 中 2 3 2 出现了两次。
小Hi想知道一段旋律中出现次数至少为K次的旋律最长是多少?解答:
本题要求找出一个字符串中重复出现的子串,允许多次出现的子串重叠。解法如下:
·N个字符串的最长公共前缀:
首先,我们需要了解:给定N个字符串,如何求解其最长公共前缀的长度。这里给出一个例子,我们有6个字符串,内容分别如下:
{banana,na, nana, anana, ana, a}
为了计算最长公共前缀,我们需要将这些字符串按照字典序进行排序,对于上面的例子,排序结果如下: 我们可以将排序后的字符串分别记作:s1, s2, s3, ... sN,并用length(i,j)表示字符串si和sj的最长公共前缀的长度。aanaananabananananana
对于字典序,其比较准则为:首先对比两字符串第一个字符是否相同,如果不同,则比较大小,确定两字符串的次序;如果相同,则再比较二者的第二个字符,以此类推,同时规定:空白字符排在任何可见字符之前(因此字符串a排在字符串ana的前面)。
基于字典序的特征,可以知道,两个字符串的最长公共前缀越长,那么按照字典序排序后,它们的次序就越接近,换句话说,对于已经按照字典序排好序的一组字符串,两个字符串的距离越近,那么它们的最长公共前缀的长度就越长。因此,我们可以得到下面的结论:
对于任意的i,j,k,i<j<k,可以得到:
length(i, j) ≥ length(i,k)
这意味着,将字符串按照字典序排好序后,两个字符串的距离越远,那么它们的最长公共前缀的长度越短。接着,我们考虑另一个问题:对于任意的i,j,k,假设i<j<k,我们计算length(i,j)和length(j,k),这两个值分别表示字符串si和sj,以及字符串sj和sk的最长公共前缀的长度。这意味着字符串si和sj的前面length(i,j)个元素是相同的,而字符串sj和sk的前length(j,k)个元素是相同的,于是我们可以得到:字符串si、sj、sk三者的前Min{length(i,j), length(j,k)}个元素是相同的,即:si, sj, sk三者的最长公共前缀的长度为:
length(i, j, k) = Min{length(i, j), length(j, k)}
然后,我们定义一个新的函数height(i) (i > 1),它表示每一个字符串与它前面的字符串的最长公共前缀的长度,即:
height(i) = length(i, i - 1)
height(2), height(3), height(4), ... height(N)
对于上面的例子,对应的height序列为:1, 3, 4, 0, 2。 根据上文中的结论,我们可以得到对于任意的字符串组:si, si+1, si+2, ... si+k,它们的最长公共前缀的长度就是:
length(i, i+1, i+2, ..., i+k) = Min{height(i+1), height(i+2), ..., height(i+k)}
这就是求解N个字符串的最长公共前缀的方法,总结如下;①首先将N个字符串按照字典序排列。这里需要说明的是:及时不进行步骤①,直接进行步骤②③,同样可以得到正确的结果。即:我们即使不对字符串按字典序排列,也不会影响这里的计算结果,但是,对于求解本题来说,按照字典序排序则是必须的,关于这一点,下文中会有说明。
②计算height(2), height(3)... height(n),得到height序列。
③最后,height序列中的最小元素的值,就是N个字符串的最长公共前缀的长度。
·后缀数组:
通过列举一个字符串的所有后缀串,可以得到这个字符串的后缀数组,也就是说,后缀数组就是一个字符串所有后缀序列的集合。所谓“后缀串”,是指从字符串任意位置开始,到字符串末端的子串,例如上文中的例子,就是字符串"banana"的后缀数组。
height序列不仅可以找到N个字符串的最长公共前缀的长度,还可以得到这些公共前缀的出现次数。
对于任意2个相邻的元素height(i)和height(i+1),根据前面的我们可以得到字符串si-1, si, si+1的最长公共前缀长度为Min{height(i),height(i+1)},同时也表明,对于这三个字符串,它们的前 Min{height(i),height(i+1)}个元素是相同的,这也就说明,这个共同的前缀,出现了3次。
以此类推,对于height序列中任意的子序列:height(i), height(i+1), height(i+2), ..., height(i+k),它们的最长公共前缀的长度是Min{height(i), height(i+1), height(i+2), ..., height(i+k)},同时也表明这个公共的前缀串出现了k+1次。又因为这里参与计算的所有字符串均是同一个字符串的后缀序列,因此也就表明了源字符串中,这个共同的前缀序列出现了至少k+1次。
于是这里,我们就可以得到求解本题的方法:要找到字符串中出现了K的子串,就是要找到它的后缀数组对应的height序列中的所有长度为K-1的子序列,然后计算这些子序列对应字符串的最长公共前缀值,再找出一个最大值,就是本题的结果。
为了更充分地说明,我们给出另外一个例子,假设字符串为:"abcbcbcba",同时K = 3,此时对应的后缀数组为:
a
abcbcbcbababcbabcbcbabcbcbcbacba
cbcbacbcbcba
对应的height序列为:1, 0, 1, 3, 5, 0, 2, 4。找到其中所有的长度为2的子序列,计算对应的length值,如下:
下面改变一下这些后缀串的顺序如下:
1, 0 -------------> length = 0
0, 1 -------------> length = 0
1, 3 -------------> length = 1
3, 5 -------------> length = 3
5, 0 -------------> length = 0
0, 2 -------------> length = 0
2, 4 -------------> length = 2
上面的这些length值中,最大值是3,说明原字符串中出现次数至少3次的子串的最大长度是3,通过查看原字符串,可以看到子串"bcb"出现了3次,说明我们的计算结果是正确的。 0, 1 -------------> length = 0
1, 3 -------------> length = 1
3, 5 -------------> length = 3
5, 0 -------------> length = 0
0, 2 -------------> length = 0
2, 4 -------------> length = 2
下面改变一下这些后缀串的顺序如下:
ba
cbaabcba
cbcbabcbcba
abcbcbcbabcbcbcba
继续求解height序列为0, 0, 0, 0, 0, 0, 0,并计算所有长度为2的序列对应的length值,然后得到的最大值也为0。此时得到的答案是错误的,这也就说明了字符串的排序会直接影响求解结果的正确性。前文中我们要将字符串按照字典序排列,这样就可以保证相似度高的字符串被排在靠近的位置,这样才能保证计算结果的正确。
·原始次序和字典次序
接下来求解的思路就比较清晰了。计算得到字符串的后缀数组,求得height序列后,在其中找出所有的长度为K-1的子串,计算得到每个子串对应的length值,找到最大值即可。
在算法实现的过程中,我们对后缀串用到了2种排序方式——后缀串在原字符串中的原始次序以及按照字典序排列后的字典次序。对于上文中的例子,字符串"abcbcbcba",它的所有后缀串按照原始次序排序结果为:
·原始次序和字典次序
接下来求解的思路就比较清晰了。计算得到字符串的后缀数组,求得height序列后,在其中找出所有的长度为K-1的子串,计算得到每个子串对应的length值,找到最大值即可。
在算法实现的过程中,我们对后缀串用到了2种排序方式——后缀串在原字符串中的原始次序以及按照字典序排列后的字典次序。对于上文中的例子,字符串"abcbcbcba",它的所有后缀串按照原始次序排序结果为:
1:abcbcbcba2:bcbcbcba3:cbcbcba4:bcbcba5:cbcba6:bcba7:cba8:ba9:a
按照字典序排序的结果则是:
1:a
2:abcbcbcba3:ba4:bcba5:bcbcba6:bcbcbcba7:cba
8:cbcba9:cbcbcba
这里我们定义2个函数来实现原始次序和字典次序的转化,用rank[i]来表示原始次序为i的字符串的字典次序,而用sa[i]表示字典序为i的字符串的原始次序。对于上面的例子,rank[1] = 2, rank[5] = 8, sa[1] = 9, sa[5] = 4。
需要说明的是,字典序的排序中我们允许并列的次序,即如果有两个字符串完全相同,那么它们的字典次序也是相同的,下文中可以看到,rank函数还可用作数据数值化的过程,对于这一点,允许并列的规则很重要。
·基于基数排序的后缀数组生成算法:
接下来,我们利用一种基于基数排序的思路来生成一个字符串的后缀数组。
首先,将字符串的每一个字符视作一个长度为1的子串,按照字典序排列,排序主要采用桶排序的方式进行,排序完毕后,更新sa和rank记录。如下图:
需要说明的是,字典序的排序中我们允许并列的次序,即如果有两个字符串完全相同,那么它们的字典次序也是相同的,下文中可以看到,rank函数还可用作数据数值化的过程,对于这一点,允许并列的规则很重要。
·基于基数排序的后缀数组生成算法:
接下来,我们利用一种基于基数排序的思路来生成一个字符串的后缀数组。
首先,将字符串的每一个字符视作一个长度为1的子串,按照字典序排列,排序主要采用桶排序的方式进行,排序完毕后,更新sa和rank记录。如下图:
从第二轮开始,我们采用双关键字基数排序的方式。基本思想是:利用排好序的长度为L/2的序列完成长度为L的序列的排序。对于第二轮,我们通过排好序的长度为1的子串,完成所有长度为2的子串的排列。可以看到,rank的值是允许并列的,即如果两个对象在排序过程中被认为是相等,那么它们拥有相同的rank值。因此,对于中间的步骤,rank记录并不是原始序列到字典序列的转换,而是一种对象数值化的表示,因为这里的排序使基于桶排序的,因此需要将待排序对象转化为数值,作为各个“桶”的标识。
对于第二轮的排序,我们完成原字符串中所有的长度为2的子串的排序,这里用第一轮排序得到的rank值作为关键字,首先基于低位关键字进行排序,然后再根据高位关键字排序,我们用A[i]和B[i]分别记录每个子串的低位和高位的关键字,此时:
A[i] = rank[i]B[i] = rank[i + 1]
另外,对于字符串的最后一个字符,已它为开始的长度为2的子串是不存在的,这里我们是做它的低位为“空串”, 并规定rank值为0,对于我们的例子,B[8] = 0。此时第二轮排序的结果如下:
排序完成后,我们就得到了所有长度为2的子串的字典序排序结果,然后我们更新rank的值,用于下一轮的迭代。由于字符串已经排序,因此,rank值相同的字符串一定是相邻的,初始的rank的值为0,然后比较每个字符串和它前面的字符串,如果二者对应的A[i]和B[i]都相同,说明该字符串和前一个字符串的排列次序是相同的,否则,当前字符串排在前一个字符串之后。
之后的迭代则和上文中的描述大同小异,每轮迭代通过排好序的L/2子串来为长度为L的子串进行排序,当L的值为字符串的总长度时,参与排序的所有子串均为原字符串的后缀,就得到了后缀数组。此时任意2个字符串的rank值均不相同,rank记录表示字符串的原始次序到字典次序的转换。
·height序列求解优化:
得到后缀数组后,我们下一步工作就是求得height序列,为了使height序列的求解尽可能简便,我们用到了下面的一个结论:
对于任意的i值:height[rank[i]] ≥ height[rank[i-1]]。
这个式子中涉及到了3个字符串,原始次序为i和i-1的字符串,我们将其分别记为a和b,以及字典序中位于字符串b前面的字符串,我们记作c,此时height[rank[i]]表示a和c的最长公共前缀的长度,假设字符串a和c的内容分别是:
基于这样的结论,我们首先求得height[rank[0]],然后对于height[rank[i]],我们就可以借用height[rank[i - 1]]的值来进行计算,值检查第height[rank[i - 1]] - 1个字符之后的字符是否相同即可。这样求解height序列的过程就可以简化。
最后,在height序列中,找到所有的长度为K-1的子序列,找到最大的length值,就是本题的答案。
之后的迭代则和上文中的描述大同小异,每轮迭代通过排好序的L/2子串来为长度为L的子串进行排序,当L的值为字符串的总长度时,参与排序的所有子串均为原字符串的后缀,就得到了后缀数组。此时任意2个字符串的rank值均不相同,rank记录表示字符串的原始次序到字典次序的转换。
·height序列求解优化:
得到后缀数组后,我们下一步工作就是求得height序列,为了使height序列的求解尽可能简便,我们用到了下面的一个结论:
对于任意的i值:height[rank[i]] ≥ height[rank[i-1]]。
这个式子中涉及到了3个字符串,原始次序为i和i-1的字符串,我们将其分别记为a和b,以及字典序中位于字符串b前面的字符串,我们记作c,此时height[rank[i]]表示a和c的最长公共前缀的长度,假设字符串a和c的内容分别是:
b = {b1, b2, ... bm}
c = {c1, c2, ... cn}
于是我们可以得到:
c = {c1, c2, ... cn}
a = {a2, a3, ... am}
由于b与c的前height[rank[i - 1]]个元素是相同的,因此,如果将字符串c的第一个元素删去,我们将得到的字符串记作d,那么d与a就有height[rank[i - 1]] - 1个元素是相同的。这说明我们找到了一个字符串{c2, c3, ..., cn}使得它与a的前height[rank[i - 1]] - 1个元素是相同的,由于字典序中,c排在b之前,因此d也一定排在a之前,因此height[rank[i]]的值至少为height[rank[i - 1]] - 1。基于这样的结论,我们首先求得height[rank[0]],然后对于height[rank[i]],我们就可以借用height[rank[i - 1]]的值来进行计算,值检查第height[rank[i - 1]] - 1个字符之后的字符是否相同即可。这样求解height序列的过程就可以简化。
最后,在height序列中,找到所有的长度为K-1的子序列,找到最大的length值,就是本题的答案。
输入输出格式:
程序代码:
输入:
输出:第一行两个整数 N和K。1≤N≤20000 1≤K≤N
接下来有 N 个整数,表示每个音的数字。1≤数字≤100
一行一个整数,表示答案。
程序代码:
import java.util.Scanner;
/**
* This is the ACM problem solving program for hihoCoder 1403.
*
* @version 2016-11-22
* @author Zhang Yufei
*/
public class Main {
/**
* The input data.
*/
private static int N, K;
/**
* The node data list.
*/
private static int[] node;
/**
* The suffix array list, sorted on dictionary.
*/
private static int[] sa;
/**
* The rank[i] means the order of the suffix[i]
* by dictionary sort.
*/
private static int[] rank;
/**
* Record the longest common prefix of the
* suffix[sa[i]] and suffix[sa[i-1]].
*/
private static int[] height;
/**
* The main program.
*
* @param args
* The command line parameters list.
*/
public static void main(String[] args) {
// Input data.
Scanner scan = new Scanner(System.in);
N = scan.nextInt();
K = scan.nextInt();
K--;
node = new int[N];
sa = new int[N];
height = new int[N];
rank = new int[N];
for (int i = 0; i < N; i++) {
node[i] = scan.nextInt();
node[i]--;
}
scan.close();
// Sorted the suffix array.
sort();
// Compute the result.
getHeight();
compute();
}
/**
* Sort the suffix arrays according to dictionary.
*/
private static void sort() {
int rankCnt = N >= 101 ? N + 1 : 101;
int[] count = new int[rankCnt];
// Init
for (int i = 0; i < rankCnt; i++) {
count[i] = 0;
}
for (int i = 0; i < N; i++) {
count[node[i]]++;
}
for (int i = 1; i < rankCnt; i++) {
count[i] += count[i - 1];
}
for (int i = N - 1; i >= 0; i--) {
sa[count[node[i]] - 1] = i;
count[node[i]]--;
}
for (int i = 0; i < rankCnt; i++) {
count[i] = 0;
}
rank[sa[0]] = 1;
rankCnt = 2;
for (int i = 1; i < N; i++) {
rank[sa[i]] = rank[sa[i - 1]];
if (node[sa[i]] != node[sa[i - 1]]) {
rank[sa[i]]++;
rankCnt++;
}
}
// Sort the len subsequences according to len/2 subsequences.
int[] tsa = new int[N];
for (int l = 1; rank[sa[N - 1]] < N; l *= 2) {
int[] A = new int[N];
int[] B = new int[N];
for (int i = 0; i < N; i++) {
A[i] = rank[i];
if (i + l < N) {
B[i] = rank[i + l];
} else {
B[i] = 0;
}
}
// Sort according to low key.
for (int i = 0; i < N; i++) {
count[B[i]]++;
}
for (int i = 1; i < rankCnt; i++) {
count[i] += count[i - 1];
}
for (int i = N - 1; i >= 0; i--) {
tsa[count[B[i]] - 1] = i;
count[B[i]]--;
}
for (int i = 0; i < rankCnt; i++) {
count[i] = 0;
}
// Sort according to high key.
for (int i = 0; i < N; i++) {
count[A[i]]++;
}
for (int i = 1; i < rankCnt; i++) {
count[i] += count[i - 1];
}
for (int i = N - 1; i >= 0; i--) {
sa[count[A[tsa[i]]] - 1] = tsa[i];
count[A[tsa[i]]]--;
}
for (int i = 0; i < rankCnt; i++) {
count[i] = 0;
}
// Update rank array value.
rank[sa[0]] = 1;
rankCnt = 2;
for (int i = 1; i < N; i++) {
rank[sa[i]] = rank[sa[i - 1]];
if (A[sa[i]] != A[sa[i - 1]] || B[sa[i]] != B[sa[i - 1]]) {
rank[sa[i]]++;
rankCnt++;
}
}
}
}
/**
* Compute the height array.
*/
private static void getHeight() {
for(int i = 0; i < N; i++) {
rank[i]--;
}
if(rank[0] == 0) {
height[rank[0]] = 0;
} else {
int j = 0;
int k = sa[rank[0] - 1];
int h = 0;
while(j < N && k < N) {
if(node[j] != node[k]) {
break;
}
j++;
k++;
h++;
}
height[rank[0]] = h;
}
for(int i = 1; i < N; i++) {
if(rank[i] == 0) {
height[rank[i]] = 0;
continue;
}
int h = height[rank[i - 1]] - 1;
if(h < 0) h = 0;
int j = i + h;
int k = sa[rank[i] - 1] + h;
while(j < N && k < N) {
if(node[j] != node[k]) {
break;
}
j++;
k++;
h++;
}
height[rank[i]] = h;
}
}
/**
* This function computes the result of this problem.
*/
private static void compute() {
if(K == 0) {
System.out.println(N);
return;
}
int max = -1;
for(int i = 0; i <= N - K; i++) {
int min = -1;
for(int j = 0; j < K; j++) {
if(min == -1 || min > height[i + j]) {
min = height[i + j];
}
}
if(max == -1 || max < min) {
max = min;
}
}
System.out.println(max);
}
}