【4.5】(蓝桥备战)动态规划、KMP算法

文章目录

    • 蓝肽子序列
    • KMP

蓝肽子序列

  • 蓝肽子序列 - 蓝桥云课 (lanqiao.cn)

字符串操作 + 经典动态规划问题。
推导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;
    }
}

KMP

题目
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;
        }
    }
    

你可能感兴趣的:(日更计划,算法,动态规划,leetcode)