1220. 统计元音字母序列的数目(动态规划)

291 阅读4分钟

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。

每日刷题第24天 2021.1.19

统计元音字母序列的数目

题目

  • 给你一个整数 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;
};
  • 上述解法可行,但是提交后发现内存消耗过大(如下图所示),因此对解法进行了优化,请看下面👇解法二 image.png

解法2:(优化后)动态规划

  • 思考🤔优化方法的时候,发现for循环从2~n的遍历中,不需要将每个长度的数据都记录下来,有用的只有上一步的数据和当前的数据,因此使用两个常数级数组进行优化。 image.png
  • 优化后的复杂度
    • 时间复杂度: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;

附录

  • 动态规划的题目还需多练,再接再厉!