之前因为一些事中断了打卡现在开始重启!
理解了kmp的原理,但是代码怎么写还是没怎么研究清楚特别是next这个数组的构建,后面参考了labuladong的知乎,链接如下
我觉得这个通过 动态规划中的状态的思想来解决KMP问题思路很清晰,先考虑pat串也就是pat
的匹配就是状态的转移。比如当 pat = "ABABC":KMP 算法最关键的步骤就是构造这个状态转移图。要确定状态转移的行为,得明确两个变量,一个是当前的匹配状态,另一个是遇到的字符;
根据我们这个 dp 数组的定义和刚才状态转移的过程,我们可以先写出 KMP 算法的 search 函数代码:
public int search(String txt) {
int M = pat.length();
int N = txt.length();
// pat 的初始态为 0
int j = 0;
for (int i = 0; i < N; i++) {
// 当前是状态 j,遇到字符 txt[i],
// pat 应该转移到哪个状态?
j = dp[j][txt.charAt(i)];
// 如果达到终止态,返回匹配开头的索引
if (j == M) return i - M + 1;
}
// 没到达终止态,匹配失败
return -1;
}
如果字符 c
和 pat[j]
不匹配的话,状态就要回退(或者原地不动),我们不妨称这种情况为状态重启:
那么,如何得知在哪个状态重启呢?解答这个问题之前,我们再定义一个名字:影子状态(我编的名字),用变量 X
表示。所谓影子状态,就是和当前状态具有相同的前缀。比如下面这种情况:
当前状态 j = 4
,其影子状态为 X = 2
,它们都有相同的前缀 "AB"。因为状态 X
和状态 j
存在相同的前缀,所以当状态 j
准备进行状态重启的时候(遇到的字符 c
和 pat[j]
不匹配),可以通过 X
的状态转移图来获得最近的重启位置。
比如说刚才的情况,如果状态 j
遇到一个字符 "A",应该转移到哪里呢?首先只有遇到 "C" 才能推进状态,遇到 "A" 显然只能进行状态重启。状态 j
会把这个字符委托给状态 X
处理,也就是 dp[j]['A'] = dp[X]['A']
:
为什么这样可以呢?因为:既然 j
这边已经确定字符 "A" 无法推进状态,只能回退,而且 KMP 就是要尽可能少的回退,以免多余的计算。那么 j
就可以去问问和自己具有相同前缀的 X
,如果 X
遇见 "A" 可以进行「状态推进」,那就转移过去,因为这样回退最少。
当然,如果遇到的字符是 "B",状态 X
也不能进行「状态推进」,只能回退,j
只要跟着 X
指引的方向回退就行了:
你也许会问,这个 X
怎么知道遇到字符 "B" 要回退到状态 0 呢?因为 X
永远跟在 j
的身后,状态 X
如何转移,在之前就已经算出来了。动态规划算法不就是利用过去的结果解决现在的问题吗?
这样,我们就细化一下刚才的框架代码:
int X # 影子状态
for 0 <= j < M:
for 0 <= c < 256:
if c == pat[j]:
# 状态推进
dp[j][c] = j + 1
else:
# 状态重启
# 委托 X 计算重启位置
dp[j][c] = dp[X][c]
下面这行代码是影子状态更新的关键点因为影子状态的X初始为0,而j初始为1,只有又遇上跟一开始的A影子状态才会移动也就是相同前缀
这行代码是 base case,只有遇到 pat[0] 这个字符才能使状态从 0 转移到 1,遇到其它字符的话还是停留在状态 0(Java 默认初始化数组全为 0)。
// base case
dp[0][pat.charAt(0)] = 1;
另外,构建 dp 数组是根据 base case dp[0][..]
向后推演。这就是我认为 KMP 算法就是一种动态规划算法的原因。
下面来看一下状态转移图的完整构造过程,你就能理解状态 X
作用之精妙了:
下面这个图非常有用!!!!!!!
class Solution {
public int[][] dp;
public int strStr(String txt, String pat) {
KMP(pat);
int M = pat.length();
int N = txt.length();
// pat 的初始态为 0
int j = 0;
for (int i = 0; i < N; i++) {
// 计算 pat 的下一个状态
j = dp[j][txt.charAt(i)];
// 到达终止态,返回结果
if (j == M) return i - M + 1;
}
// 没到达终止态,匹配失败
return -1;
}
public void KMP(String pat) {
int M = pat.length();
// dp[状态][字符] = 下个状态
dp = new int[M][256];
// base case
dp[0][pat.charAt(0)] = 1;
// 影子状态 X 初始为 0
int X = 0;
// 构建状态转移图(稍改的更紧凑了)
for (int j = 1; j < M; j++) {
for (int c = 0; c < 256; c++)
dp[j][c] = dp[X][c];
dp[j][pat.charAt(j)] = j + 1;
// 更新影子状态
X = dp[X][pat.charAt(j)];
}
}
}