182 竞赛
找到所有好字符串
给你两个长度为 n 的字符串 s1 和 s2 ,以及一个字符串 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"。最后,还有一个以 'd' 开头的好字符串:"da"。
示例 2:
输入:n = 8, s1 = "leetcode", s2 = "leetgoes", evil = "leet"
输出:0
解释:所有字典序大于等于 s1 且小于等于 s2 的字符串都以 evil 字符串 "leet" 开头。所以没有好字符串。
示例 3:
输入:n = 2, s1 = "gx", s2 = "gz", evil = "x"
输出:2
提示:
s1.length == n
s2.length == n
s1 <= s2
1 <= n <= 500
1 <= evil.length <= 50
所有字符串都只包含小写英文字母。
/** * @param {number} n * @param {string} s1 * @param {string} s2 * @param {string} evil * @return {number} * 单个字母的字典序就是排在字母表前面的字母小,排在字母表后面的字母大。 * 字符串的字典序是从第一个字符开始往后一一对比,如果两个字符相等就比较打下一个; * 直到两个字符不等或其中一个字符串没有更多字母,这时长度大的字符串大。 */ const mod = 10 ** 9 + 7 let cache = null var findGoodStrings = function(n, s1, s2, evil) { /** * 9个bit,生成的字符串s最长为500个字符, 需要 2 ** 9 > 500 * 6个bit,evil串最长长度为50, 需要 2 ** 6 > 50 * leftBound 1个bit * rightBound 1个bit * 总共17个bit,记录生成的s串长度为mainTraver、匹配evil的长度为evilMatch、s中最后一个字符 * 与下、上界是否接壤的的每种情况下生成的合法字符串的数目 * 深度优先遍历的特点是,遍历是从第1层开始的,生成结果却是从最后一层开始倒着生成的 * 所以cache记录的是所有已生成的完整的s串中, 前缀长度为mainTraver、evil匹配长度为evilMatch、 * 以s的前缀长度mainTraver、evil已匹配的长度evilMatch、s的前缀最后一个字符接壤s1、s2的下标为mainTravel - 1的字符做上下界的情况 * 四个维度做category分类统计的s合法串的数目 * 所以最后的答案就是s串前缀长度mainTravel为0、evil已匹配长度为0、s串前缀最有后一个字符 * 上下界都接壤的类目下合法s串的数目 */ cache = new Array(1 << 17).fill(-1) return dfs(0, 0, n, s1, s2, evil, true, true, computeNext(evil)) }; /** * 深度优先遍历: * 解法是从s1和s2的第1个字符开始从左到右遍历到最后一个字符 * 每遍历一个位置i(i的范围是[0, n], 0表示还没开始遍历,是初始条件,n表示遍历完了), * 当前位置的字符可以从from变化到to,from由 * i - 1选择的字符是否等于s1[i - 1]和s1[i]决定,同理to由i - 1选择 * 的字符是否等于s2[i -1]和s2[i]决定。 * i位置每选择一个字符就要确定遍历到i位置为止生成的字符串中evil能匹配的长度。如果evilMatch * 灯evil.length,说明当前生成的字符串和以这个字符串为前缀的字符串都是非法的,直接返回0 * 如果s1已经遍历到第n个字符,那么说明生成了一个完整的长度为n的合法的字符,返回1. * 初始条件是s1中遍历到第0个字符、evil匹配长度是0、上一个选择的字符既挨着上界又挨着下界(因为true && 任意值的话真假值由后面条件决定) * mainTravel 上一次dfs已生成的s串的长度,初始传入0,表示初始条件是一个字符都没有生成 * evilMatch evil中与上一次已生成的s串匹配的长度,初始值同样是0 * n * s1 * s2 * evil, 这四个参数不解释,不是猪就不用说 * leftBound 上一个位置选择的字符是否挨着下界, 1表示挨下界 0不挨下界 * rightBound 上一个位置选择的字符是否挨着上界 1挨上界 * next, 如果选择的当前字符与evil中当前待比较的那个字符不相等, 下一个我要用evil中哪个下标的字符 * 来跟当前选择的字符做比较,实际上也就得到了选择某个字符延长生成的s串后evil与新生成的串匹配的长度 */ function dfs(mainTravel, evilMatch, n, s1, s2, evil, leftBound, rightBound, next) { // 如果上一次dfs已生成的s串, evil全部能匹配到, 那么已这个s串为前缀的串都非法, // 直接返回0,这个分支也不用再继续遍历了下去了 if(evilMatch === evil.length) return 0 // 这表示生成了一个完整的s串并且是合法的,dfs遍历到最后生成初始结果 if(mainTravel === n) return 1 let key = generateKey(mainTravel, evilMatch, leftBound, rightBound) // 因为四个类目的任意组合只会出现一次,不会有重复,所以如果已经统计好了这个类目下的数据 // 那就可以直接复用以前的结果 if(cache[key] !== -1) return cache[key] let from = leftBound ? s1.charCodeAt(mainTravel) : 97 let to = rightBound ? s2.charCodeAt(mainTravel) : 122 let res = 0 for(let i = from; i <= to; i++) { let c = String.fromCharCode(i) /** * 找到evil与当前生成的字符串能匹配的长度 * 实际上这道题目里evilMatch只会变长或保持不变、不会变短 */ let j = evilMatch while((j > 0) && (evil[j] !== c)) j = next[j - 1] if(evil[j] === c) j++ res += dfs(mainTravel + 1, j, n, s1, s2, evil, leftBound && (i === from), rightBound && (i === to), next) res %= mod } cache[key] = res return res } // 生成cache的key function generateKey(mainTravel, evilMatch, leftBound, rightBound) { return (mainTravel << 8) | (evilMatch << 2) | ((leftBound ? 1 : 0 ) << 1) | (rightBound ? 1 : 0) } /** * 到某一个下标为止, 和某个后缀相等的前缀的长度的最大值(自己等于自己不算, * 也就是长度最大值是下标,不是下标 + 1)。 * 也就是如果evil当前下标的指示的字符与主串中当前字符不相等, evil要往右 * 滑动多少个位置。也就是下一步evil用哪个下标指示的值去与主串中的当前位置的字符做比较 */ function computeNext(evil) { let n = evil.length let arr = new Array(n).fill(0) arr[0] = 0 let j = 0 for(let i = 1; i < n; i++) { while((j > 0) && (evil[i] !== evil[j])) j = arr[j - 1] if(evil[i] === evil[j]) arr[i] = ++j } return arr }
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-all-good-strings
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。