MENU

KMP(3)

July 3, 2018 • Read: 3203 • 算法阅读设置

例 2 题目链接:HDOJ1711

这个题目的大意就是给你一个整数数组 A=[A1, A2, … AN] 和一个整数数组 B=[B1, B2, … BM],让你找到一个位置 K 使得从 A [K] 开始的 M 个整数与 [B1, B2, … BM] 相等。如果没有满足条件的位置 K 就输出 - 1

这其实就是一个整数版的 KMP。我们之前讲 KMP 的背景都是字符串匹配,其实也可以拿来做数组序列匹配。只要把整数相等对应成字符相等就可以了

  • #include <cstring>
  • #include <cstdio>
  • using namespace std;
  • int nxt[1000001];
  • int ans,t;
  • int p[1000001],s[1000001];
  • int n,m;
  • int main()
  • {
  • scanf("%d",&t);
  • while(t--)
  • {
  • ans = -1;
  • scanf("%d %d",&n,&m);
  • for(int i = 1;i <= n;i++)
  • scanf("%d",&s[i]);
  • for(int i = 1;i <= m;i++)
  • scanf("%d",&p[i]);
  • nxt[0] = -1;
  • int j = -1;
  • for(int i = 1;i <= m;i++)
  • {
  • while(j >= 0 && p[j + 1] != p[i])
  • j = nxt[j];
  • nxt[i] = ++j;
  • }
  • j = 0;
  • for(int i = 1;i <= n;i++)
  • {
  • while(j >= 0 && p[j + 1] != s[i])
  • j = nxt[j];
  • if(++j == m)
  • {
  • ans = i - m + 1;
  • break;
  • }
  • }
  • printf("%d\n",ans);
  • }
  • return 0;
  • }

例 3 题目链接:HDOJ2087

这道题目的大意是给定两个字符串 P 和 S。问 P 能从 S 中剪出来多少次。这道题看上去与上一节 hihoCoder #1015 题类似。但是其实是有一个关键条件不一样,这道题不允许剪出来 P 在 S 中重叠。比如在之前的题目中 ADA 在 ADADADA 中出现了 3 次,本题中就只能剪出来 2 次。这道题的样例也告诉你,从 aaaaaa 中最多剪出来 3 个 aa

这道题有两种思路。第一种思路是先不管三七二十一,用 hihoCoder #1015 中的匹配方法,把所有出现 P 的位置都求出来,存在一个 vector<int> start_pos 里:

  • for(int i = 1;i <= n;i++)
  • {
  • while(j >= 0 && p[j + 1] != s[i])
  • j = nxt[j];
  • if(++j == m)
  • {
  • start_pos.push_back(i - m + 1);
  • j = nxt[j];
  • }
  • }

假如输入是 aaaaaa 和 aa,那么 start_pos 中保存的就是 [1, 2, 3, 4, 5]。现在我们的问题就变成要从 start_pos 中选出尽量多的整数,并且保证相邻的两个整数差不小于 P.len。比如上面的例子中就应该选出 [1, 3, 5]

这个问题是可以贪心选的。大致思路就是一定选第一个最小的整数,然后从小到大检查每一个整数,如果能选(与上一个差不小于 P.len)就一定选,不能选再看下一个。伪代码如下:

  • Last_Pick = -P.len
  • Cnt = 0
  • For i:start_Pos
  • If i - Last_Pos >= P.len
  • Last_Pick = i
  • Cnt++
  • Print Cnt

第二种思路。这种思路是建立在贪心算法基础上的。假设 S [L..L+P.len-1] 是 P 第一次出现的位置。根据贪心的思路,这个 S [L..L+P.len-1] 是一定要被剪出来的。在之前的题目中,找到一个完整匹配 j==m 的时候是令 j = next [j] (也就是 j=next [m]),让 S [i] 与 P [next [m]+1] 继续匹配。这里 S [i] 就是 S [L+P.len]

; 本题要避免剪出重叠的 P,要把 S [L..L+P.len-1]” 剪掉 “,显然剪掉之后 S [L+P.len] 只能从 P [1] 开始匹配起了,而不能从 P [next [m]+1] 开始了。所以我们直接把 j = next [j] 改成 j = 0 即可让 KMP 按我们的要求避免重叠

  • #include <iostream>
  • #include <cstring>
  • #include <cstdio>
  • using namespace std;
  • int nxt[1000001];
  • int ans,n,m;
  • char p[1000001],s[1000001];
  • int main()
  • {
  • scanf("%s",s + 1);
  • while(strcmp(s + 1,"#") != 0)
  • {
  • scanf("%s",p + 1);
  • n = strlen(s + 1);
  • m = strlen(p + 1);
  • ans = 0;
  • nxt[0] = -1;
  • int j = -1;
  • for(int i = 1;i <= m;i++)
  • {
  • while(j >= 0 && p[j + 1] != p[i])
  • j = nxt[j];
  • nxt[i] = ++j;
  • }
  • j = 0;
  • for(int i = 1;i <= n;i++)
  • {
  • while(j >= 0 && p[j + 1] != s[i])
  • j = nxt[j];
  • if(++j == m)
  • {
  • ans++;
  • j = 0;
  • }
  • }
  • cout << ans << endl;
  • scanf("%s",s + 1);
  • }
  • return 0;
  • }

上面的代码同上一节 hihoCoder #1015 题除了处理输入部分之外,就只有 27 行不一样。这道题的代码 27 是直接 j = 0,从而强制下一个字符 S [i] 从 P [1] 开始匹配。第二个思路需要对 KMP 的理解更加清楚,才能更加灵活的运用

例 4 题目链接:hihoCoder1625

题目大意是给定两个字符串 A 和 B,请你求出字符串 A 最少重复几次才能使得 B 是 A 的子串。例如 A="hiho",B="hohihohi"。则 A 重复 3 次之后变为 "hihohihohiho",这时 B 是 A 的子串。如果没有解输出 - 1

这题降低时间复杂度的关键是一次性得到足够长的 S,进行 KMP;而不能从一个比较短的 S 开始尝试 KMP,匹配不成就在 S 后面接一个 A,再匹配不成再接一个 A……

于是问题来了,S 多长才是足够长?这个问题的答案是 A.len+B.len。证明比较简单,留给大家自己思考。于是我们有一个伪代码:

  • S = 空串
  • While S.len < A.len + B.len
  • S = S + A//+这里是字符串连接
  • L = KMP(B,S)
  • If L > 0
  • Print L + B.len - 2 / A.len + 1//长度L + B.len - 1需要几个A拼起来
  • Else
  • Print -1

不过这道题目还有一种更简单的实现方法。我们可以并不真的拼出一个字符串 S。而是 “假装” 有一个字符串 S。当我们 KMP 的过程中需要用的 S [i] 的时候,就用 A [(i-1) % A.len+1] 代替。代码如下:

  • #include <iostream>
  • #include <cstring>
  • #include <cstdio>
  • using namespace std;
  • int nxt[1000001];
  • int ans,n,m,t;
  • char p[1000001],s[1000001];
  • int main()
  • {
  • scanf("%d",&t);
  • while(t--)
  • {
  • scanf("%s%s",s + 1,p + 1);
  • n = strlen(s + 1);
  • m = strlen(p + 1);
  • ans = -1;
  • nxt[0] = -1;
  • int j = -1;
  • for(int i = 1;i <= m;i++)
  • {
  • while(j >= 0 && p[j + 1] != p[i])
  • j = nxt[j];
  • nxt[i] = ++j;
  • }
  • j = 0;
  • for(int i = 1;i <= n + m;i++)
  • {
  • char c = s[(i - 1) % n + 1];
  • while(j >= 0 && p[j + 1] != c)
  • j = nxt[j];
  • if(++j == m)
  • {
  • ans = (i - 1) / n + 1;
  • break;
  • }
  • }
  • cout << ans << endl;
  • }
  • return 0;
  • }

注意第 28 行这里的 s 实际上是题目中的 A。我们实际上并没有 S [i],而是用 A 中的对应字符代替

例 5 题目链接:HDOJ2594

这道题目的大意是给定两个字符串 P 和 S,让你找一个最长的字符串 T 满足 T 是 P 的前缀,也是 S 的后缀

如果你对 KMP 的过程非常清楚的话,你会发现 KMP 用 P 去匹配 S 的过程中,如果 S [i] 匹配上了 P [j] 那就说明 P [1..j] 是 S [1..i] 的最长后缀,同时 P [1..j] 显然是 P 的前缀。所以我们只要找到第一个匹配上 S [n] 的 P [j] 的即可,这时 j 就是答案

  • #include <iostream>
  • #include <cstring>
  • #include <cstdio>
  • using namespace std;
  • int nxt[1000001];
  • int ans,n,m,t;
  • char p[1000001],s[1000001];
  • int main()
  • {
  • while(scanf("%s%s",p + 1,s + 1) != EOF)
  • {
  • n = strlen(s + 1);
  • m = strlen(p + 1);
  • nxt[0] = -1;
  • int j = -1;
  • for(int i = 1;i <= m;i++)
  • {
  • while(j >= 0 && p[j + 1] != p[i])
  • j = nxt[j];
  • nxt[i] = ++j;
  • }
  • j = 0;
  • for(int i = 1;i <= n;i++)
  • {
  • char c = s[(i - 1) % n + 1];
  • while(j >= 0 && p[j + 1] != c)
  • j = nxt[j];
  • if(++j == m)
  • {
  • if(i == n)
  • break;
  • j = nxt[j];
  • }
  • }
  • if(j == 0)
  • cout << 0 << endl;
  • else
  • {
  • p[j + 1] = 0;
  • cout << p + 1 << ' ' << j << endl;
  • }
  • }
  • return 0;
  • }
Last Modified: February 8, 2020
Archives Tip
QR Code for this page
Tipping QR Code
Leave a Comment