題目來源:
題目要求:
小Hi平時的一大興趣愛好就是演奏鋼琴。我們知道一個音樂旋律被表示爲長度爲 N 的數構成的數列。
小Hi在練習過很多曲子以後發現很多作品自身包含一樣的旋律。旋律是一段連續的數列,相似的旋律在原數列可重疊。比如在1 2 3 2 3 2 1 中 2 3 2 出現了兩次。
小Hi想知道一段旋律中出現次數至少爲K次的旋律最長是多少?解答:
本題要求找出一個字符串中重複出現的子串,允許多次出現的子串重疊。解法如下:
·N個字符串的最長公共前綴:
首先,我們需要了解:給定N個字符串,如何求解其最長公共前綴的長度。這裏給出一個例子,我們有6個字符串,內容分別如下:
我們可以將排序後的字符串分別記作:s1, s2, s3, ... sN,並用length(i,j)表示字符串si和sj的最長公共前綴的長度。aanaananabananananana
對於字典序,其比較準則爲:首先對比兩字符串第一個字符是否相同,如果不同,則比較大小,確定兩字符串的次序;如果相同,則再比較二者的第二個字符,以此類推,同時規定:空白字符排在任何可見字符之前(因此字符串a排在字符串ana的前面)。
基於字典序的特徵,可以知道,兩個字符串的最長公共前綴越長,那麼按照字典序排序後,它們的次序就越接近,換句話說,對於已經按照字典序排好序的一組字符串,兩個字符串的距離越近,那麼它們的最長公共前綴的長度就越長。因此,我們可以得到下面的結論:
對於任意的i,j,k,i<j<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三者的最長公共前綴的長度爲:
根據上文中的結論,我們可以得到對於任意的字符串組:si, si+1, si+2, ... si+k,它們的最長公共前綴的長度就是:
①首先將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,此時對應的後綴數組爲:
aabcbcbcbababcbabcbcbabcbcbcbacbacbcbacbcbcba
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序列後,在其中找出所有的長度爲K-1的子串,計算得到每個子串對應的length值,找到最大值即可。
在算法實現的過程中,我們對後綴串用到了2種排序方式——後綴串在原字符串中的原始次序以及按照字典序排列後的字典次序。對於上文中的例子,字符串"abcbcbcba",它的所有後綴串按照原始次序排序結果爲:
1:abcbcbcba2:bcbcbcba3:cbcbcba4:bcbcba5:cbcba6:bcba7:cba8:ba9:a
1:a2:abcbcbcba3:ba4:bcba5:bcbcba6:bcbcbcba7:cba8:cbcba9:cbcbcba
需要說明的是,字典序的排序中我們允許並列的次序,即如果有兩個字符串完全相同,那麼它們的字典次序也是相同的,下文中可以看到,rank函數還可用作數據數值化的過程,對於這一點,允許並列的規則很重要。
·基於基數排序的後綴數組生成算法:
接下來,我們利用一種基於基數排序的思路來生成一個字符串的後綴數組。
首先,將字符串的每一個字符視作一個長度爲1的子串,按照字典序排列,排序主要採用桶排序的方式進行,排序完畢後,更新sa和rank記錄。如下圖:
A[i] = rank[i]B[i] = rank[i + 1]
之後的迭代則和上文中的描述大同小異,每輪迭代通過排好序的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的內容分別是:
c = {c1, c2, ... cn}
基於這樣的結論,我們首先求得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);
}
}