leetcode
leetcode 1301 ~ 1350
找到所有好字符串

找到所有好字符串

难度:

标签:

题目描述

代码结果

运行时间: 206 ms, 内存: 21.1 MB


/*
 * 思路:
 * 1. 使用递归方法并结合Stream进行动态规划处理。
 * 2. 定义一个递归函数来处理每一个字符位置,同时使用流的特性简化代码。
 * 3. 在递归过程中更新状态,判断是否符合好字符串的定义。
 */

import java.util.stream.Stream;

public class GoodStringCounterStream {
    private static final int MOD = 1000000007;
    private String s1, s2, evil;
    private int n, m;
    private Integer[][][] memo;

    public int countGoodStrings(int n, String s1, String s2, String evil) {
        this.s1 = s1;
        this.s2 = s2;
        this.evil = evil;
        this.n = n;
        this.m = evil.length();
        this.memo = new Integer[n][s1.length() + 1][m + 1];

        return dfs(0, 0, 0);
    }

    private int dfs(int i, int j, int k) {
        if (k == m) return 0;
        if (i == n) return 1;
        if (memo[i][j][k] != null) return memo[i][j][k];

        char start = i < s1.length() ? s1.charAt(i) : 'a';
        char end = i < s2.length() ? s2.charAt(i) : 'z';

        int result = Stream.iterate(start, c -> (char) (c + 1)).limit(end - start + 1)
                .mapToInt(c -> {
                    boolean matchS1 = (j < s1.length() && c == s1.charAt(j));
                    boolean matchS2 = (j < s2.length() && c == s2.charAt(j));
                    int newJ = j + (matchS1 ? 1 : 0) - (matchS2 ? 1 : 0);
                    int newK = (evil.charAt(k) == c) ? k + 1 : 0;
                    return dfs(i + 1, newJ, newK);
                })
                .reduce(0, (a, b) -> (a + b) % MOD);

        memo[i][j][k] = result;
        return result;
    }
}

解释

方法:

这道题的解法使用了动态规划和KMP算法的思想。首先通过KMP算法的前缀函数计算出evil字符串的前缀函数,然后基于前缀函数构建一个dp数组,dp[i][j]表示evil字符串的前i个字符与某个字符串匹配,且当前匹配到字符j时,evil字符串的最大匹配长度。接下来使用记忆化搜索,枚举满足条件的字符串,搜索过程中维护当前枚举到的位置i,当前匹配的evil字符串的长度match,以及是否受到s1和s2的字典序限制。搜索到字符串末尾时,如果match小于evil字符串的长度,说明当前字符串是好字符串,累加答案。

时间复杂度:

O(n × m × 26)

空间复杂度:

O(n × m + m × 26)

代码细节讲解

🦆
在计算KMP算法的前缀函数时,为什么需要检查当前字符与j指向的字符不相同时,将j更新为pi[j - 1]?
在KMP算法中,前缀函数被用来表示字符串的最长相同前后缀的长度。当计算前缀函数时,如果当前字符与j指向的字符不匹配,这意味着当前的匹配失败。因此,我们需要找到一个更短的有效匹配,这可以通过跳转到pi[j - 1]来实现,即跳到当前已匹配前缀的下一个最长相同前后缀的末尾。这样做可以避免重复检查已知不匹配的字符,从而提高匹配效率。
🦆
dp数组中dp[i][j]的定义是基于匹配到字符j时evil字符串的最大匹配长度,那么dp数组的初始状态如何确定,尤其是除了dp[0][ord(pattern[0]) - 97] = 1之外的其他值?
dp数组的每个元素dp[i][j]表示在字符串pattern中,以i结尾的子串能够匹配的最大长度,当下一个字符为j(ASCII码减去97得到字符'a'到'z'的索引)时的情况。对于dp[0][ord(pattern[0]) - 97] = 1是因为第一个字符匹配时,匹配长度为1。对于其他值,当字符不匹配时,我们需要回溯到前一个匹配的状态,这由前缀函数pi给出。因此,对于dp数组中的其他元素,我们根据前缀函数计算得到的最长可匹配后缀的继续位置来填充,这保证了在非直接匹配的情况下,能找到最长的可能的匹配状态。
🦆
dfs函数中的limit_low和limit_high参数是如何控制字符串的字典序界限的?具体是如何通过这两个参数确保生成的字符串在s1和s2的字典序范围内?
在dfs函数中,参数limit_low和limit_high用于控制生成字符串的字典序边界以确保其在s1和s2的范围内。当limit_low为true时,表示当前位置的字符不能小于s1在同一位置的字符。类似地,当limit_high为true时,表示当前位置的字符不能大于s2在同一位置的字符。这两个参数在递归过程中动态更新:如果当前字符恰好等于s1(或s2)的对应字符,相应的限制参数保持为true,否则设为false。这种机制确保了生成的字符串在给定的字典序范围内递归搜索。
🦆
在dfs递归函数中,如果当前的match已经等于或超过了evil字符串的长度,为什么直接返回0,这是基于什么样的考虑?
在dfs函数中,如果match等于或超过evil字符串的长度,这意味着在生成的字符串中已经完全包含了evil字符串作为子串。题目要求找到不包含evil字符串的好字符串。因此,一旦发现evil字符串已经被完全匹配,当前分支的搜索可以终止,直接返回0,表示这条路径不产生任何有效的好字符串,这是为了满足问题的约束条件而进行的优化处理。

相关问题