Hard
题目描述
给你字符串 s1 和 s2,它们的长度都为 n,以及一个字符串 evil,请你返回 好字符串 的数目。
好字符串 的定义如下:
- 它的长度为 n
- 它按字典序大于等于 s1
- 它按字典序小于等于 s2
- 它不包含 evil 作为子字符串
由于答案可能很大,请返回答案对 10^9 + 7 取模的结果。
示例 1:
输入:n = 2, s1 = "aa", s2 = "da", evil = "b"
输出:51
解释:总共有 25 个以 'a' 开头的好字符串:"aa","ac","ad",...,"az"。
还有 25 个以 'c' 开头的好字符串:"ca","cc","cd",...,"cz"。
最后还有 1 个以 'd' 开头的好字符串:"da"。
示例 2:
输入:n = 8, s1 = "leetcode", s2 = "leetgoes", evil = "leet"
输出:0
解释:所有字典序在 s1 和 s2 之间的字符串都以前缀 "leet" 开头,因此没有好字符串。
示例 3:
输入:n = 2, s1 = "gx", s2 = "gz", evil = "x"
输出:2
提示:
s1.length == ns2.length == ns1 <= s21 <= n <= 5001 <= evil.length <= 50- 所有字符串都只包含小写英文字母
解题思路
这是一道典型的数位动态规划与字符串匹配结合的题目。我们需要解决三个核心问题:
- 范围限制:生成的字符串必须在 s1 和 s2 的字典序范围内
- 子串匹配:避免包含 evil 作为子字符串
- 计数统计:统计满足条件的字符串数量
解题思路:
使用四维动态规划状态 dp[pos][evil_pos][is_s1_prefix][is_s2_prefix]:
pos:当前构建到第几个字符位置evil_pos:当前字符串与 evil 的最长公共后缀长度(KMP 中的失配位置)is_s1_prefix:当前字符串是否仍等于 s1 的前缀is_s2_prefix:当前字符串是否仍等于 s2 的前缀
关键技术:
- 使用 KMP 算法 预处理 evil 字符串的失配函数,用于高效地跟踪匹配状态
- 当
evil_pos == evil.length()时,说明完全匹配到了 evil,这种情况需要跳过 - 通过
is_s1_prefix和is_s2_prefix控制字符选择范围,确保结果在指定区间内
状态转移时,对于每个可选字符,更新 evil 的匹配位置,并根据边界条件更新前缀标志。
代码实现
class Solution {
public:
int findGoodStrings(int n, string s1, string s2, string evil) {
const int MOD = 1e9 + 7;
int m = evil.length();
// KMP preprocessing
vector<int> failure(m, 0);
for (int i = 1; i < m; i++) {
int j = failure[i-1];
while (j > 0 && evil[i] != evil[j]) {
j = failure[j-1];
}
if (evil[i] == evil[j]) {
j++;
}
failure[i] = j;
}
// dp[pos][evil_pos][is_s1_prefix][is_s2_prefix]
vector<vector<vector<vector<int>>>> dp(n+1, vector<vector<vector<int>>>(m+1,
vector<vector<int>>(2, vector<int>(2, -1))));
function<int(int, int, bool, bool)> solve = [&](int pos, int evil_pos, bool is_s1_prefix, bool is_s2_prefix) -> int {
if (evil_pos == m) return 0; // contains evil as substring
if (pos == n) return 1; // valid string
int& res = dp[pos][evil_pos][is_s1_prefix][is_s2_prefix];
if (res != -1) return res;
res = 0;
char start = is_s1_prefix ? s1[pos] : 'a';
char end = is_s2_prefix ? s2[pos] : 'z';
for (char c = start; c <= end; c++) {
// Update evil_pos using KMP
int new_evil_pos = evil_pos;
while (new_evil_pos > 0 && evil[new_evil_pos] != c) {
new_evil_pos = failure[new_evil_pos - 1];
}
if (evil[new_evil_pos] == c) {
new_evil_pos++;
}
bool new_is_s1_prefix = is_s1_prefix && (c == s1[pos]);
bool new_is_s2_prefix = is_s2_prefix && (c == s2[pos]);
res = (res + solve(pos + 1, new_evil_pos, new_is_s1_prefix, new_is_s2_prefix)) % MOD;
}
return res;
};
return solve(0, 0, true, true);
}
};
class Solution:
def findGoodStrings(self, n: int, s1: str, s2: str, evil: str) -> int:
MOD = 10**9 + 7
m = len(evil)
# KMP preprocessing
failure = [0] * m
for i in range(1, m):
j = failure[i-1]
while j > 0 and evil[i] != evil[j]:
j = failure[j-1]
if evil[i] == evil[j]:
j += 1
failure[i] = j
from functools import lru_cache
@lru_cache(None)
def solve(pos, evil_pos, is_s1_prefix, is_s2_prefix):
if evil_pos == m:
return 0 # contains evil as substring
if pos == n:
return 1 # valid string
result = 0
start = ord(s1[pos]) if is_s1_prefix else ord('a')
end = ord(s2[pos]) if is_s2_prefix else ord('z')
for c_ord in range(start, end + 1):
c = chr(c_ord)
# Update evil_pos using KMP
new_evil_pos = evil_pos
while new_evil_pos > 0 and evil[new_evil_pos] != c:
new_evil_pos = failure[new_evil_pos - 1]
if evil[new_evil_pos] == c:
new_evil_pos += 1
new_is_s1_prefix = is_s1_prefix and (c == s1[pos])
new_is_s2_prefix = is_s2_prefix and (c == s2[pos])
result = (result + solve(pos + 1, new_evil_pos, new_is_s1_prefix, new_is_s2_prefix)) % MOD
return result
return solve(0, 0, True, True)
public class Solution {
private const int MOD = 1000000007;
private int[,,,] dp;
private int[] failure;
private string evil;
private string s1, s2;
private int n, m;
public int FindGoodStrings(int n, string s1, string s2, string evil) {
this.n = n;
this.s1 = s1;
this.s2 = s2;
this.evil = evil;
this.m = evil.Length;
// KMP preprocessing
failure = new int[m];
for (int i = 1; i < m; i++) {
int j = failure[i-1];
while (j > 0 && evil[i] != evil[j]) {
j = failure[j-1];
}
if (evil[i] == evil[j]) {
j++;
}
failure[i] = j;
}
dp = new int[n+1, m+1, 2, 2];
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
for (int k = 0; k < 2; k++) {
for (int l = 0; l < 2; l++) {
dp[i,j,k,l] = -1;
}
}
}
}
return Solve(0, 0, 1, 1);
}
private int Solve(int pos, int evilPos, int isS1Prefix, int isS2Prefix) {
if (evilPos == m) return 0;
if (pos == n) return 1;
if (dp[pos, evilPos, isS1Prefix, isS2Prefix] != -1) {
return dp[pos, evilPos, isS1Prefix, isS2Prefix];
}
int result = 0;
char start = isS1Prefix == 1 ? s1[pos] : 'a';
char end = isS2Prefix == 1 ? s2[pos] : 'z';
for (char c = start; c <= end; c++) {
int newEvilPos = evilPos;
while (newEvilPos > 0 && evil[newEvilPos] != c) {
newEvilPos = failure[newEvilPos - 1];
}
if (evil[newEvilPos] == c) {
newEvilPos++;
}
int newIsS1Prefix = (isS1Prefix == 1 && c == s1[pos]) ? 1 : 0;
int newIsS2Prefix = (isS2Prefix == 1 && c == s2[pos]) ? 1 : 0;
result = (result + Solve(pos + 1, newEvilPos, newIsS1Prefix, newIsS2Prefix)) % MOD;
}
return dp[pos, evilPos, isS1Prefix, isS2Prefix] = result;
}
}
var findGoodStrings = function(n, s1, s2, evil) {
const MOD = 1000000007;
const m = evil.length;
// KMP preprocessing
const failure = new Array(m).fill(0);
for (let i = 1; i < m; i++) {
let j = failure[i-1];
while (j > 0 && evil[i] !== evil[j]) {
j = failure[j-1];
}
if (evil[i]
复杂度分析
| 复杂度类型 | 分析 |
|---|---|
| 时间复杂度 | O(n × m × 26) |
| 空间复杂度 | O(n × m) |
其中 n 是字符串长度,m 是 evil 字符串长度。时间复杂度中的 26 来自于每个位置最多尝试 26 个字符。空间复杂度主要来自记忆化搜索的状态存储。