KMP-next数组的花式使用

kmp算法,是用来优化字符串的模式匹配(源串s中是否存在模式串p),可以把暴力匹配的复杂度O(n²)降低到O(m+n)。

通过对模式串生成前缀数组(next数组),来跳跃式的进行模式匹配,解决如HDU 1711(http://acm.hdu.edu.cn/showproblem.php?pid=1711)这样的问题。我们一般使用的是把next[0]标记为负数(-1)的修正版kmp。

但是kmp算法中最具价值的部分并不是模式匹配,而是next数组本身。next数组的一般定义是“next[n] = 0-n串中最长相同前后缀的长度”。定义是这样没错,但是这一定义却可以用另外一种方式来理解:next[n] = 当前前缀上一次出现的索引。

这一理解是由定义和一定的脑洞推出的,当我们把next数组生成目标串的起始索引标为1而不是0的时候生效(这就是脑洞部分…..)。

举个例子:

   a b a b a b
-1 0 0 1 2 3 4 

上方是模式串,索引从1开始标记。从头开始看,next[1]的值是0,代表这个前缀第一次出现。而next[3] = 1,代表着当前位置的前缀在串中不是第一次出现,上一次出现的位置是索引1,即“a”。

而next[4] = 2,也代表着不是第一次出现,上一次出现的位置是索引2,即“ab”。

根据这一变换定义,我们也可以写出生成next数组的代码:

    public static void setPrefix(int m) {
        int i = 1;
        while (i < m) {
            if (p[i] == p[next[i - 1]]) {
                next[i] = next[i - 1] + 1;
            }
            i++;
        }
    }

以上代码是未修正版的next数组,next值是错位的,仅做演示证明结论的正确性,不建议使用。我们一般使用的模板是:

    public static void setPrefix(int m) {
        int j = 0, k = -1;
        next[0] = -1;
        while (j < m) {
            if (k == -1 || p[j] == p[k]) {
                j++;
                k++;
                next[j] = k;
            } else
                k = next[k];
        }
    }

两者的结果除了错位以外完全相同,“next[n] = 当前前缀上一次出现的索引”的脑洞正确。

通过这一脑洞,我们可以解决一些有趣的非模式匹配但是有关前后缀的问题。首先以POJ2752(http://poj.org/problem?id=2752)为例。这一题要你求所有“同时是前缀又是后缀”的子串的长度。比如样例“aaaa”的结果就是“1 2 3 4 5”。

我们可以用上面提到的next数组脑洞版定义来求解这一题。

首先生成next数组,然后倒着利用数组的索引性来进行穿梭,最后正着输出答案。

int count = 0;
for (int i = in.length(); i >= 1;) {
    if (next[i] == 0) {
        break;
    }
    ans[count++] = next[i];
    i = next[i];// 跳到上一次出现的位置
}

for (int i = count - 1; i >= 0; i--) {
    out.print(ans[i] + " ");
}
out.println(in.length());// 最后加上自己

由于后缀的定义“从任意字母开始到最后的子串”,才可以直接从最后开始穿梭。

除了纯粹的穿梭操作以外,还可以利用这一性质数出每个前缀重复出现的次数,举例HDU3336(http://acm.hdu.edu.cn/showproblem.php?pid=3336)。

这一题让你求每个前缀出现次数的和再%10007。

我们求出next数组后可以写出这样的代码:

// zhouge = 10007, 洲哥是一位值得膜的大人物,连续%可以增加ac率
int ans = 0;
for (int i = 1; i <= n; i++) {
    int tmp = next[i];
    while (tmp != -1) {
        ans = (ans + 1) % zhouge;
        tmp = next[tmp];
    }
}
out.println(ans);

每能够穿梭一次,就代表前缀出现了一次,+1记录就好。我们来人肉跑一遍,以样例“abab”为例,生成的next数组是:

   a b a b
-1 0 0 1 2 

从索引1开始。
next[1] = 0,代表前缀”a”第一次出现,+1
next[2] = 0,前缀”ab”第一次出现,+1
next[3] = 1,首先前缀”aba”第一次出现,+1,然后因为next数组不为0,穿梭,原来是前缀”a”第二次出现了,+1
next[4] = 2,前缀”abab”第一次出现,+1,穿梭,前缀”ab”第二次出现,+1
结果ans = 6

画外音:
Q:为什么next[4] = 2不是”b”第二次出现?
A: 因为b不是前缀啊!!!只有博主这种zz才会在做题时脑子短路出现这种疑问。

通过人肉cpu我们发现,每一个前缀至少会出现一次,因此我们可以进行一个小优化:

int ans = len;// 串的长度
for (int i = 1; i <= n; i++) {
    int tmp = next[i];
    while (tmp != 0) {
        ans = (ans + 1) % zhouge;
        tmp = next[tmp];
    }
}
out.println(ans);

直接在一开始就把这部分加进去,并避免不必要的穿梭。

索引性质的利用到这就说完了,接下来提一下其他的一些操作。举例华工赛2018-E(之前博客发过)和HDU2594(http://acm.hdu.edu.cn/showproblem.php?pid=2594)
华工赛那题太长了,以HDU2594为例。
让你求同时s1串的前缀和s2串后缀的最长子串。比如样例“riemann”和“marjorie”答案是“rie”。

呃,操作就是把两串拼起来跑一遍next数组,然后输出最后一位,原理上面已经提过了。其实这是扩展kmp的extend数组解决的问题,即“定义母串S,和字串T,设S的长度为n,T的长度为m,求T与S的每一个后缀的最长公共前缀”。然而还是能用next数组水过去。

值得一提的是拼接时要在中间加一个俩串中都不会出现的特殊符号,例如“*”。如果不这么做的话输入“abcabc”和“abc”,或是“abc”和“abcabc”就会出bug,当然不加的话加个条件判断也ok。

String get = reader.next() + "*" + reader.next();
setPrefix(get);
int ans = next[get.length()];
if (ans == 0) {
    out.println(0);
} else {
    out.println(get.substring(0, ans) + " " + ans);
}

然后是循环子串的问题,直接丢万能定理:假设S的长度为len,则S存在循环子串,当且仅当,len可以被len - next[len]整除,最短循环子串为S[len - next[len]]。

原理大佬比我讲得好:https://blog.csdn.net/dnf1990/article/details/8010384

举例POJ2406(https://blog.csdn.net/dnf1990/article/details/8010384),求循环子串的长度。

int c = len - next[len];
if(len%c==0){
    out.println(len/c);
}else{
    out.println(1); 
}

通过len%0==0保证存在循环子串,然后输出就行了。

以上是这几天休闲打kmp的成果,以后有再补。去研究莫队了。

你可能感兴趣的:(ACM,字符串)