ACM解題總結———HihoCoder1403(後綴數組)

(p.s: 前段時間因爲找工作和論文的關係,很久沒有更新衰 ,今天起再次開更。。。。奮鬥 )
 

題目來源:
    HihoCoder1403 

題目要求:

    小Hi平時的一大興趣愛好就是演奏鋼琴。我們知道一個音樂旋律被表示爲長度爲 N 的數構成的數列。

    小Hi在練習過很多曲子以後發現很多作品自身包含一樣的旋律。旋律是一段連續的數列,相似的旋律在原數列可重疊。比如在1 2 3 2 3 2 1 中 2 3 2 出現了兩次。

    小Hi想知道一段旋律中出現次數至少爲K次的旋律最長是多少?

解答:
    本題要求找出一個字符串中重複出現的子串,允許多次出現的子串重疊。解法如下:

·N個字符串的最長公共前綴:
    首先,我們需要了解:給定N個字符串,如何求解其最長公共前綴的長度。這裏給出一個例子,我們有6個字符串,內容分別如下:
    {banana,na, nana, ananaana, a} 
    爲了計算最長公共前綴,我們需要將這些字符串按照字典序進行排序,對於上面的例子,排序結果如下:  
    a
    ana
    anana
    banana
    na
    nana
    我們可以將排序後的字符串分別記作:s1, s2, s3, ... sN,並用length(i,j)表示字符串sisj的最長公共前綴的長度。
    對於字典序,其比較準則爲:首先對比兩字符串第一個字符是否相同,如果不同,則比較大小,確定兩字符串的次序;如果相同,則再比較二者的第二個字符,以此類推,同時規定:空白字符排在任何可見字符之前(因此字符串a排在字符串ana的前面)。
    基於字典序的特徵,可以知道,兩個字符串的最長公共前綴越長,那麼按照字典序排序後,它們的次序就越接近,換句話說,對於已經按照字典序排好序的一組字符串,兩個字符串的距離越近,那麼它們的最長公共前綴的長度就越長。因此,我們可以得到下面的結論:
    對於任意的i,j,ki<j<k,可以得到:
        length(i, j) ≥ length(i,k)
    這意味着,將字符串按照字典序排好序後,兩個字符串的距離越遠,那麼它們的最長公共前綴的長度越短。
    接着,我們考慮另一個問題:對於任意的i,j,k,假設i<j<k,我們計算length(i,j)length(j,k),這兩個值分別表示字符串sisj,以及字符串sjsk的最長公共前綴的長度。這意味着字符串sisj的前面length(i,j)個元素是相同的,而字符串sjsk的前length(j,k)個元素是相同的,於是我們可以得到:字符串sisjsk三者的前Min{length(i,j), length(j,k)}個元素是相同的,即:sisjsk三者的最長公共前綴的長度爲:
            length(i, j, k) = Min{length(i, j), length(j, k)}  
    然後,我們定義一個新的函數height(i) (i > 1),它表示每一個字符串與它前面的字符串的最長公共前綴的長度,即:
    height(i) = length(i, i - 1)
    對於i = 2, 3, ...N,我們分別計算其height值,就可以得到一個height序列:
        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
    abcbcbcba
    ba
    bcba
    bcbcba 
    bcbcbcba  
    cba
    cbcba
    cbcbcba 
    對應的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次,說明我們的計算結果是正確的。 
    下面改變一下這些後綴串的順序如下:
    ba
    
cba 
    a
    bcba
    
cbcba
    bcbcba
    
abcbcbcba
    bcbcbcba  
  
    繼續求解height序列爲0, 0, 0, 0, 0, 0, 0,並計算所有長度爲2的序列對應的length值,然後得到的最大值也爲0。此時得到的答案是錯誤的,這也就說明了字符串的排序會直接影響求解結果的正確性。前文中我們要將字符串按照字典序排列,這樣就可以保證相似度高的字符串被排在靠近的位置,這樣才能保證計算結果的正確。

·原始次序和字典次序
    接下來求解的思路就比較清晰了。計算得到字符串的後綴數組,求得height序列後,在其中找出所有的長度爲K-1的子串,計算得到每個子串對應的length值,找到最大值即可。
    在算法實現的過程中,我們對後綴串用到了2種排序方式——後綴串在原字符串中的原始次序以及按照字典序排列後的字典次序。對於上文中的例子,字符串"abcbcbcba",它的所有後綴串按照原始次序排序結果爲:
    1:abcbcbcba
    2:bcbcbcba
    3:cbcbcba
    4:bcbcba
    5:cbcba
    6:bcba
    7:cba
    8:ba
    9:a
    按照字典序排序的結果則是:
    1:a
    2:abcbcbcba
    3:ba
    4:bcba
    5:bcbcba 
    6:bcbcbcba  
    7:cba
    8:cbcba
    9:cbcbcba
    這裏我們定義2個函數來實現原始次序和字典次序的轉化,用rank[i]來表示原始次序爲i的字符串的字典次序,而用sa[i]表示字典序爲i的字符串的原始次序。對於上面的例子,rank[1] = 2, rank[5] = 8, sa[1] = 9, sa[5] = 4
    需要說明的是,字典序的排序中我們允許並列的次序,即如果有兩個字符串完全相同,那麼它們的字典次序也是相同的,下文中可以看到,rank函數還可用作數據數值化的過程,對於這一點,允許並列的規則很重要。

·基於基數排序的後綴數組生成算法:
    接下來,我們利用一種基於基數排序的思路來生成一個字符串的後綴數組。
    首先,將字符串的每一個字符視作一個長度爲1的子串,按照字典序排列,排序主要採用桶排序的方式進行,排序完畢後,更新sarank記錄。如下圖:
     
    從第二輪開始,我們採用雙關鍵字基數排序的方式。基本思想是:利用排好序的長度爲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個字符串,原始次序爲ii-1的字符串,我們將其分別記爲ab,以及字典序中位於字符串b前面的字符串,我們記作c,此時height[rank[i]]表示ac的最長公共前綴的長度,假設字符串a和c的內容分別是:
    b = {b1, b2, ... bm}
    c = {c1, c2, ... cn}
    於是我們可以得到:
a = {a2, a3, ... am}
    由於bc的前height[rank[i - 1]]個元素是相同的,因此,如果將字符串c的第一個元素刪去,我們將得到的字符串記作d,那麼da就有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值,就是本題的答案。 

輸入輸出格式:
    輸入:

第一行兩個整數 NK1≤N≤20000 1≤K≤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);
    }
}


發佈了75 篇原創文章 · 獲贊 80 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章