统计元音字母序列的数目

177 阅读4分钟

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

给你一个整数 n,请你帮忙统计一下我们可以按下述规则形成多少个长度为 n 的字符串:

  • 字符串中
  • 的每个字符都应当是小写元音字母('a', 'e', 'i', 'o', 'u'

由于答案可能会很大,所以请你返回 模 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

这道题我最开始的想法是把每个数都遍历出来, 但是看到需要取模之后

首先我先判断0和1的两种情况

if (n == 0) return 0
if (n == 1) return 5

然后我将n=1的五种情况列出来,同时也是需要进行计算的五个基础字符,并将其存入map中,初始数值都是1个。

let a = ["a", "e", "i", "o", "u"]
  let map = new Map()
  for (let i = 0; i < a.length; i++) {
    map.set(a[i], 1)
  }

接下来就是递归遍历的阶段。

首先确定终止条件,是n==0 时递归终止

同时我们将map进行一次拷贝,用于计算数量

let tem = []
map.forEach(v => {
  tem.push(v)
})

递归时我们遍历map的五个字符,每遇到一个字符,就去掉其数量,并对其对应应该添加的字符的个数加上当前字符的个数。

for (const m of map) {
  switch (m[0]) {
    case "a":
      map.set("e", map.get('e') + 1 * tem[0] % mod)
      map.set("a", map.get('a') - 1 * tem[0] % mod)
      break;
    case "e":
      res = res + 1 * tem[1] % mod
      map.set("i", map.get('i') + 1 * tem[1] % mod)
      map.set("a", map.get('a') + 1 * tem[1] % mod)
      map.set("e", map.get('e') - 1 * tem[1] % mod)
      break;
    case "i":
      res = res + 3 * tem[2] % mod
      map.set("i", map.get('i') - 1 * tem[2] % mod)
      map.set("a", map.get('a') + 1 * tem[2] % mod)
      map.set("e", map.get('e') + 1 * tem[2] % mod)
      map.set("o", map.get('o') + 1 * tem[2] % mod)
      map.set("u", map.get('u') + 1 * tem[2] % mod)
      break;
    case "o":
      res = res + 1 * tem[3] % mod
      map.set("i", map.get('i') + 1 * tem[3] % mod)
      map.set("u", map.get('u') + 1 * tem[3] % mod)
      map.set("o", map.get('o') - 1 * tem[3] % mod)
      break;
    case "u":
      map.set("u", map.get('u') - 1 * tem[4] % mod)
      map.set("a", map.get('a') + 1 * tem[4] % mod)
      break;
    default:
      break;
  }
}

总结

var countVowelPermutation = function (n) {
  if (n == 0) return 0
  if (n == 1) return 5
  let mod = 1000000007
  let a = ["a", "e", "i", "o", "u"]
  let map = new Map()
  for (let i = 0; i < a.length; i++) {
    map.set(a[i], 1)
  }
  let res = 5
  dp(n - 1)
  return res % mod
  function dp (n) {
    if (n == 0) return
    let tem = []
    map.forEach(v => {
      tem.push(v)
    })
    for (const m of map) {
      switch (m[0]) {
        case "a":
          map.set("e", map.get('e') + 1 * tem[0] % mod)
          map.set("a", map.get('a') - 1 * tem[0] % mod)
          break;
        case "e":
          res = res + 1 * tem[1] % mod
          map.set("i", map.get('i') + 1 * tem[1] % mod)
          map.set("a", map.get('a') + 1 * tem[1] % mod)
          map.set("e", map.get('e') - 1 * tem[1] % mod)
          break;
        case "i":
          res = res + 3 * tem[2] % mod
          map.set("i", map.get('i') - 1 * tem[2] % mod)
          map.set("a", map.get('a') + 1 * tem[2] % mod)
          map.set("e", map.get('e') + 1 * tem[2] % mod)
          map.set("o", map.get('o') + 1 * tem[2] % mod)
          map.set("u", map.get('u') + 1 * tem[2] % mod)
          break;
        case "o":
          res = res + 1 * tem[3] % mod
          map.set("i", map.get('i') + 1 * tem[3] % mod)
          map.set("u", map.get('u') + 1 * tem[3] % mod)
          map.set("o", map.get('o') - 1 * tem[3] % mod)
          break;
        case "u":
          map.set("u", map.get('u') - 1 * tem[4] % mod)
          map.set("a", map.get('a') + 1 * tem[4] % mod)
          break;
        default:
          break;
      }
    }
    n--
    dp(n)
  }
};

上述代码时间复杂度过高。

所以我们还是使用动态规划的方案

题目中给定的字符的下一个字符的规则如下:

  • 字符串中的每个字符都应当是小写元音字母 (‘a’,‘e’,‘i’,‘o’,‘u’)
  • 每个元音'a' 后面都只能跟着 'e'
  • 每个元音'e' 后面只能跟着 'a' 或者是 'i'
  • 每个元音 'i' 后面 不能 再跟着另一个 'i'
  • 每个元音 'o' 后面只能跟着 'i' 或者是 'u'
  • 每个元音 'u' 后面只能跟着 'a'

以上等价于每个字符的前一个字符的规则如下:

  • 每个元音'a' 前面都只能跟着 'e''i''u'
  • 每个元音'e' 前面只能跟着 'a' 或者是 'i'
  • 每个元音 'i' 前面只能着 'e''o'
  • 每个元音 'o' 前面只能跟着 'i'
  • 每个元音 'u' 前面只能跟着 'o''i'

我们设 dp[i][j] 代表当前长度为 i 且以字符 j 为结尾的字符串的数目,其中在此 j=0,1,2,3,4 分别代表元音字'a', 'e', 'i', 'o', 'u')。

实际计算过程中只需要保留前一个状态即可推导出后一个状态,计算长度为 iii 的状态只需要长度为 i−1 的中间变量即可,i−1 之前的状态全部都可以丢弃掉。计算过程中答案需要取模

var countVowelPermutation = function(n) {
    const mod = 1000000007;
    const dp = new Array(5).fill(0);
    const ndp = new Array(5).fill(0);
    for (let i = 0; i < 5; ++i) {
        dp[i] = 1;
    }
    for (let i = 2; i <= n; ++i) {
        /* a前面可以为e,u,i */
        ndp[0] = (dp[1] + dp[2] + dp[4]) % mod;
        /* e前面可以为a,i */
        ndp[1] = (dp[0] + dp[2]) % mod;
        /* i前面可以为e,o */
        ndp[2] = (dp[1] + dp[3]) % mod;
        /* o前面可以为i */
        ndp[3] = dp[2];
        /* u前面可以为i,o */
        ndp[4] = (dp[2] + dp[3]) % mod;
        dp.splice(0, 5, ...ndp);
    }
    let ans = 0;
    for (let i = 0; i < 5; ++i) {
        ans = (ans + dp[i]) % mod;
    }
    return ans;
};