ACM解题总结———HihoCoder1403(后缀数组)

来源:互联网 发布:c语言联合体int的大小 编辑:程序博客网 时间:2024/05/21 09:50
(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);    }}


0 0
原创粉丝点击