前言
突然间发现自己好久没写有关算法题的博客了,今儿来一道很有意思的算法题,它可以使用三种完全不同的思路来处理,话不多说,直接上题。
题目描述
给你一个整数 n,请返回长度为 n 、仅由元音 (a, e, i, o, u) 组成且按 字典序排列 的字符串数量。
字符串 s 按 字典序排列 需要满足:对于所有有效的 i,s[i] 在字母表中的位置总是与 s[i+1] 相同或在 s[i+1] 之前。
例如:
输入:n=2
输出:15
解释:仅由元音组成的 15 个字典序字符串为
[“aa”,“ae”,“ai”,“ao”,“au”,“ee”,“ei”,“eo”,“eu”,“ii”,“io”,“iu”,“oo”,“ou”,“uu”]
注意,“ea” 不是符合题意的字符串,因为 ‘e’ 在字母表中的位置比 ‘a’ 靠后
这道题目来自力扣,
原题链接:https://leetcode-cn.com/problems/count-sorted-vowel-strings/
回溯算法
显然,这道题目最直接的思路就是使用回溯算法,这道题也是一道很标准的回溯算法题,难度不大。
直接上代码:
function countVowelStrings(n: number): number {
const map = ['a', 'e', 'i', 'o', 'u'];
//const res = [];
let count = 0;
// index用于记录下一轮回溯的开始位置,list用于保存当前回溯的数据状态
const dfs = (index: number, list: string[]) => {
if (list.length === n) {
count++;
// res.push(list.join(''));
return;
}
for (let i = index; i < map.length; i++) {
list.push(map[i]);
dfs(i, list);
list.pop();
}
}
dfs(0, []);
return count;
};
使用回溯算法的优势是可以把每一种组合的具体结果都记录下来(即代码中所注释掉的code),
但是由于我们不需要具体的结果只需要总共的结果数,并且使用回溯的时间复杂度高达O(2n),
所以我们考虑是否能将算法进行进一步优化。
不难发现,f(n)的结果是与其子问题f(n-1)的结果有直接关联,所以接下来我们使用动态规划来解决此问题。
动态规划
创建一个二维dp,行代表元音数组,列代表组合字符串的长度,
那么dp[i][j]表示以s[j]元音字母开头长度为i的组合总数。
以n=3为例可得到如下的二维数组表:
/*
* a e i o u sum
* 1 1 1 1 1 1 5
* 2 5 4 3 2 1 15
* 3 15 10 6 3 1 35
*/
从表中,我们不难得出其状态转移方程:dp[i][j] = dp[i-1][j]+dp[i][j+1]。
代码如下:
function countVowelStrings(n: number): number {
const dp = new Array(n).fill(0).map(() => new Array(5).fill(1));
for (let i = 1; i < n; i++) {
//由于状态转移方程的特性,所以此处采取倒序遍历
for (let j = 3; j >= 0; j--) {
dp[i][j] = dp[i - 1][j] + dp[i][j + 1];
}
}
let sum = 0;
for (let j = 0; j < 5; j++) {
sum += dp[n - 1][j];
}
return sum;
};
``
此算法的时间复杂度为O(n)(因为元音数组的长度是已知的)。
到此,O(n)的时间复杂度还能再进一步优化吗?答案是当然可以的,因为数学,yyds!
数学(盒子放球模型)
长度为5的元音数组组合成长度为n的字符串种数转换为数学问题就等价于有n个小球放到5个盒子里(盒子可以为空),有多少方式?
首先我解释一下为何能如此等价,因为题目条件的限制使得每一种确定的放置方式能且仅能得到一种组合,比如一个’a’一个’e’两个’o’,它只能得到一个结果,那就是’aeoo’。
然后我们来解决数学问题,这是一道高中的经典排列组合问题(组合数学里面也有),先看它的一般问题:
n个小球放到m个盒子里(盒子可以为空),有多少方式?
由于盒子可以为空,貌似不容易解决,我们可以尝试将其进行等价变形处理:
n+m个小球放到m个盒子里(每个盒子至少有一个小球),有多少方式?
第二种方式其实就是增加m个小球然后给每个盒子都默认放置一个小球即可。
那么如何处理第二种问题呢?接下来就要引入数学的神奇手法–“挡板”,我尝试用另外一种更容易理解的方式来描述这个“挡板”。
m个盒子有m-1个间隙,n+m个小球含有n+m-1个间隙,将n+m个小球放到m个盒子相当于在小球的n+m-1个间隙中选取m-1个间隙来放置挡板,进而将小球分成m个部分也就是m个盒子,那么总共就有C(n + m - 1, m - 1)种方式。回到本题,那么答案就是C(n + 4, 4)。
所以使用此种方式,那么只需要一行代码:
function countVowelStrings(n: number): number {
return (n + 4) * (n + 3) * (n + 2) * (n + 1) / 24;
};
此算法时间复杂度为O(1)!
结语
终究,数学才是最强大的算法源泉,没有之一!