字符串操作 + 经典动态规划问题。
推导dp数组分为两种状态:蓝肽相等和蓝肽不相等。
蓝肽相等:那么找到了一个子序列,当前最大蓝肽子序列为l1 和 l2的上一个蓝肽的最大子序列 + 1。dp[i][j] = dp[i - 1][j - 1] + 1
蓝肽不相等:当前最大蓝肽子序列为l1的上一个最大蓝肽子序列或l2 的上一个最大蓝肽子序列的最大值。
dp[i][j] = Math.max(dp[i - 1][j] + dp[i][j - 1])
。
public class Main {
static String m;
static String n;
static int[][] dp;
public static void main(String[] args) {
Scanner s = new Scanner(System.in);
m = s.nextLine();
n = s.nextLine();
List <String> l1 = sub(m);
List <String> l2 = sub(n);
dp = new int[l1.size() + 1][l2.size() + 1];
int ans = 0;
for(int i = 1 ; i <= l1.size() ; i ++){
for(int j = 1 ; j <= l2.size() ; j ++){
if(l1.get(i - 1).equals(l2.get(j - 1))){
dp[i][j] = dp[i - 1][j -1] + 1;
}else{
dp[i][j] = Math.max(dp[i - 1][j] , dp[i][j - 1]);
}
ans = Math.max(ans , dp[i][j]);
}
}
System.out.println(ans);
}
static List<String> sub(String s){
List <String> list = new ArrayList<>();
int start = 0;
for(int i = 1 ; i < s.length() ; i ++){
if (s.charAt(i) - 'A' >= 0 && s.charAt(i) - 'A' <= 26) {
list.add(s.substring(start , i));
start = i;
}
if(i == s.length() - 1){
list.add(s.substring(start , i + 1));
}
}
return list;
}
}
题目 | |
---|---|
28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode) | |
459. 重复的子字符串 - 力扣(LeetCode) |
KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
要想知道一部分之前匹配过的文本内容,就需要使用前缀表。因为前缀表的作用就是记录了模式串与主串不匹配时,模式串从哪里开始匹配的问题(跳到之前已经匹配过的地方)。
既然前缀表这么厉害,接下来说说什么是前缀表:
前缀表记录的是下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
前缀:不包含最后一个字符的,所有以第一个字符开头的连续子串。
后缀:不包含第一个字符的,所有以最后一个字符结尾的连续子串。
前缀表的原理:由于我们失败的位置是后缀子串(aa)的后面(在f匹配失败了),那么就要找到相同的前缀的后面重新匹配。由于前缀就是从下标0开始的,所以前缀的后面就是前缀的长度,就是前缀表对应的值。
一般KMP使用next数组,其实next数组既可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。
接下来就来构造next数组:
首先定义两个指针i和j,j指向前缀末尾位置,i 指向后缀末尾位置。其实这里j也表示了最长相等前后缀的长度。
next数组初始化:这里j表示后缀末尾,所以应该是0;next[0],在0这个位置,当然也是回退到0。
但是我们实现前缀表使用全部减一的方式实现,所以j初始化为-1,next[i]表示i(包括i)之前的最长前后缀长度,所以next[0] = j。
int j = -1;
next[0] = j;
处理前后缀不同的情况,也就是s[i]与s[j + 1]不相同。这里 j + 1应该向前回退,就要找 j+1前一个元素在next数组里的值(就是next[j])。next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。
//i是表示后缀末尾,所以i应该从1开始才有效。
for (int i = 1; i < s.size(); i++) {
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
j = next[j]; // 向前回退
}
}
处理前后缀相同的情况,也就是s[i] 和 s[j + 1]相同,找到了最长前后缀。那么 j 和 i 都要向后移动一位。同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。
if (s[i] == s[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j;
KMP时间复杂度分析:
其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。
暴力的解法显而易见是O(n × m),所以KMP在字符串匹配中极大地提高了搜索的效率。
接下来看一道例题应用next数组:28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)
使用next数组进行匹配
在文本串s里 找是否出现过模式串t。
定义两个下标,j 指向模式串起始位置,i 指向文本串起始位置。
那么j初始值依然为-1,为什么呢? 依然因为next数组里记录的起始位置为-1。
i就从0开始,遍历文本串,代码如下:
for (int i = 0; i < s.size(); i++)
接下来就是 s[i] 与 t[j + 1] (因为j从-1开始的) 进行比较。
如果 s[i] 与 t[j + 1] 不相同,j就要回退到next[j]的位置。
while(j >= 0 && s[i] != t[j + 1]) {
j = next[j];
}
如果 s[i] 与 t[j + 1] 相同,那么i 和 j 同时向后移动, 代码如下:
if (s[i] == t[j + 1]) {
j++; // i的增加在for循环里
}
如何判断在文本串s里出现了模式串t呢,如果 j 指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了。
本题要在文本串字符串中找出模式串出现的第一个位置 (从0开始),所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,就是文本串字符串中出现模式串的第一个位置。
代码如下:
if (j == (t.size() - 1) ) {
return (i - t.size() + 1);
}
完整代码如下:
class Solution {
int [] next;
public int strStr(String haystack, String needle) {
next = getNext(needle);
int j = -1;
for(int i = 0 ; i < haystack.length() ; i ++){
while(j >= 0 && haystack.charAt(i) != needle.charAt(j + 1)){
j = next[j];
}
if(haystack.charAt(i) == needle.charAt(j + 1)){
j ++;
}
if(j == needle.length() - 1){
return i - needle.length() + 1;
}
}
return -1;
}
int [] getNext(String needle){
int [] next = new int [needle.length()];
int j = -1;
next[0] = j;
for(int i = 1 ; i < needle.length() ; i ++){
while(j >= 0 && needle.charAt(i) != needle.charAt(j + 1)){
j = next[j];
}
if(needle.charAt(i) == needle.charAt(j + 1)){
j++;
}
next[i] = j;
}
return next;
}
}
459. 重复的子字符串 - 力扣(LeetCode)
解法一:KMP算法
一个字符串的内部由重复的子串组成,前面有相同的子串,后面有相同的子串,用 s + s,这样组成的字符串中,后面的子串做前串,前后的子串做后串,就一定还能组成一个s。
class Solution {
public boolean repeatedSubstringPattern(String s) {
String str = s + s;
int [] next = getNum(s);
int j = -1;
//不从头尾开始查找,如果中间出现相同的字符串,就返回true。
for(int i = 1 ; i < str.length() - 1 ; i ++){
while(j >= 0 && str.charAt(i) != s.charAt(j + 1)){
j = next[j];
}
if(str.charAt(i) == s.charAt(j + 1)){
j ++;
}
if(j == s.length() - 1){
return true;
}
}
return false;
}
int [] getNum(String s){
char ch [] = s.toCharArray();
int [] next = new int [ch.length];
int j = -1;
next[0] = j;
for(int i = 1 ; i < ch.length ; i ++){
while(j >= 0 && ch[i] != ch[j + 1]){
j = next[j];
}
if(ch[i] == ch[j + 1]){
j++;
}
next[i] = j;
}
return next;
}
}