「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。
每日刷题第24天 2021.1.19
统计元音字母序列的数目
- leetcode原题链接:leetcode-cn.com/problems/co…
- 难度:困难
- 方法:动态规划
题目
- 给你一个整数 n,请你帮忙统计一下我们可以按下述规则形成多少个长度为 n 的字符串:
- 字符串中的每个字符都应当是小写元音字母('a', 'e', 'i', 'o', 'u')
- 每个元音 'a' 后面都只能跟着 'e'
- 每个元音 'e' 后面只能跟着 'a' 或者是 'i'
- 每个元音 'i' 后面 不能 再跟着另一个 'i'
- 每个元音 'o' 后面只能跟着 'i' 或者是 'u'
- 每个元音 'u' 后面只能跟着 'a'
- 由于答案可能会很大,所以请你返回 模 10^9 + 7 之后的结果。
示例
- 示例1
输入: n = 1
输出: 5
解释: 所有可能的字符串分别是:"a", "e", "i" , "o" 和 "u"。
- 示例2
输入:n = 2
输出:10
解释:所有可能的字符串分别是:"ae", "ea", "ei", "ia", "ie", "io", "iu", "oi", "ou" 和 "ua"。
- 示例3
输入: n = 5
输出: 68
提示
1 <= n <= 2 * 10^4
解法
解法1:动态规划
- 思路:根据题目进行手写模拟后,发现题目中求按照规则形成多少个长度为
n的字符串,其实就是统计基于上一次的字符串的末尾元音字母,查看可以匹配的元音字母,计算出这些元音字母的总数即可。 - 也就是说:字符串末尾的元音字母一定的情况下,我们可以推算出下一次可以匹配的元音字母的总个数。
- 既然和末尾元音字母有关,那么将题目转换一下
- 前一个末尾字母为
e \ i \ u可产生下一次末尾为a的字符串 - 前一个末尾字母为
i \ a可产生下一次末尾为e的字符串 - 前一个末尾字母为
o \ e可产生下一次末尾为i的字符串 - 前一个末尾字母为
i可产生下一次末尾为o的字符串 - 前一个末尾字母为
o \ i可产生下一次末尾为u的字符串
- 前一个末尾字母为
- 那么动态规划方程:
dp[z][j] = ?z表示:长度j表示:当前字符串的末尾元音字母dp[z][j]表示:当前长度z下,末尾为j的元音字母的长度
- 结合上述分析,推理出
dp[z][a] = dp[z - 1][e] + dp[z - 1][i] + dp[z - 1][u]dp[z][e] = dp[z - 1][i] + dp[z - 1][a]dp[z][i] = dp[z - 1][e] + dp[z - 1][o]dp[z][o] = dp[z - 1][i]dp[z][u] = dp[z - 1][o] + dp[z - 1][i]
- 存储字符可能有些不太方便,因此将元音字符替换为
0,1,2,3,4来进行书写
/**
* @param {number} n
* @return {number}
*/
var countVowelPermutation = function(n) {
let dp = new Array(n);
for (let i = 0; i < n; i++) {
dp[i] = new Array(5).fill(0);
}
// 记录取模值
let mod = 1000000007;
// 初始化:长度为1时,以各元音字母结尾出现的次数
dp[0][0] = 1;
dp[0][1] = 1;
dp[0][2] = 1;
dp[0][3] = 1;
dp[0][4] = 1;
for (let i = 1; i < n; i++) {
dp[i][0] = (dp[i - 1][4] + dp[i - 1][2] + dp[i - 1][1]) % mod;
dp[i][1] = (dp[i - 1][0] + dp[i - 1][2]) % mod;
dp[i][2] = (dp[i - 1][1] + dp[i - 1][3]) % mod;
dp[i][3] = (dp[i - 1][2]) % mod;
dp[i][4] = (dp[i - 1][2] + dp[i - 1][3]) % mod;
}
// console.log('shuchu',dp);
let sum = (dp[n - 1][0] + dp[n - 1][1] + dp[n - 1][2] + dp[n - 1][3] + dp[n - 1][4]) % mod;
return sum;
};
- 上述解法可行,但是提交后发现内存消耗过大(如下图所示),因此对解法进行了优化,请看下面👇解法二
解法2:(优化后)动态规划
- 思考🤔优化方法的时候,发现
for循环从2~n的遍历中,不需要将每个长度的数据都记录下来,有用的只有上一步的数据和当前的数据,因此使用两个常数级数组进行优化。 - 优化后的复杂度
- 时间复杂度:
O(C×n),其中n是给定,C表示元音字母的数量,在本题中C=5。 - 空间复杂度:
O(C),我们只需要常数个空间存储不同组的数目。
- 时间复杂度:
- 代码展示
// 优化:使用两个数组
let dpPre = new Array(5).fill(1);
let dpCur = new Array(5).fill(1);
// 设置取模数值
let mod = 1000000007;
// 循环遍历 数组中的数
for (let i = 2; i <= n; i++) {
dpCur[0] = (dpPre[4] + dpPre[2] + dpPre[1]) % mod;
dpCur[1] = (dpPre[0] + dpPre[2]) % mod;
dpCur[2] = (dpPre[1] + dpPre[3]) % mod;
dpCur[3] = (dpPre[2]) % mod;
dpCur[4] = (dpPre[2] + dpPre[3]) % mod;
dpPre.splice(0,5,...dpCur);
}
return (dpCur[0] + dpCur[1] + dpCur[2] + dpCur[3] + dpCur[4]) % mod;
附录
- 动态规划的题目还需多练,再接再厉!