题目
核心思路
题目的含义还是挺好理解的,作为周赛的第二题也还算中规中矩。我们需要找到长度为n
的、按字典序排列的、仅由元音字母组成的字符串数量。其中最重要的就是要满足按字典序排列,也就是示例二所说的"ea" 不是符合题意的字符串,因为 'e' 在字母表中的位置比 'a' 靠后。
暴力法(深搜、宽搜)
理解了题意,直接使用深搜或者宽搜模拟拼接的过程就可以了,可以先确定第一个字符为c
(c表示一个元音字符),那么他后边能接的只有大于或等于他的字符,接着来处理他后边的那个字符,形成了一个递归的过程,就可以很容易得出答案了。而递归出口,就是字符串的长度为目标值n
即可。
注意:因为实际上我们只需要拼接字符串的最后一个字符,所以直接对字符而非字符串递归即可,这样可以节省很多时间。
暴力法代码
class Solution {
char[] chars = {'a', 'e', 'i', 'o', 'u'};
public int countVowelStrings(int n) {
int res = 0;
for(char c : chars){
res += solve(c, n);
}
return res;
}
public int solve(char c, int n){
if(n == 1) return 1;
int idx = 5;
int res = 0;
for(int i = 0; i < chars.length; i++){
if(chars[i] == c){
idx = i;
}
if(i >= idx){
res += solve(chars[i], n - 1);
}
}
return res;
}
}
这里使用递归的方式实现,迭代方式思想也是一样的,就不再过多赘述。
动态规划法
观察暴力法,在函数参数c, n
相同的情况下,返回的结果也是肯定相同的,而在函数调用中可能会多次调用同一个函数,因此不妨直接记录dp[c][n] 表示以字符c开头,长度为n的满足条件的字符串的数量
。当然直接当做记忆化递归完善上边代码倒也是可以的,不过由于只有5个字符,完全可以迭代一层循环解决,会简便不少。
我们考虑下边的例子:n = 5
如果对所有结果分类的话,可以分为:
- 以
a
开头的长度为n
字符串数量 - 以
e
开头的长度为n
字符串数量 - 以
i
开头的长度为n
字符串数量 - 以
o
开头的长度为n
字符串数量 - 以
u
开头的长度为n
字符串数量
那么以 a
开头的字符串又有什么规律呢?它的后边5种字符都可以接,也就是按上述递归,包含以 'a' 开头长度为 'n - 1'的字符串数量、以 'e'开头长度为 ‘n - 1' 的字符串的数量……
最终,也就是长度为一的字符串的数量,均为1,可以直接初始化。
因此就可以得到如下的递推公式:
dp[4][i] = dp[4][i - 1];
dp[3][i] = dp[3][i - 1] + dp[4][i];
dp[2][i] = dp[2][i - 1] + dp[3][i];
dp[1][i] = dp[1][i - 1] + dp[2][i];
dp[0][i] = dp[0][i - 1] + dp[1][i];
其中的dp[0]、dp[1]、dp[2]......分别对应了字符a, e, i, o, u
开头,因为按照倒序u, o, i, e, a
的方式遍历,避免了中间重复加的数值,稍微简化了点代码。
动态规划代码
class Solution {
public int countVowelStrings(int n) {
int[][] dp = new int[5][n];
dp[0][0] = dp[1][0] = dp[2][0] = dp[3][0] = dp[4][0] = 1;
for(int i = 1; i < n; i++){
dp[4][i] = dp[4][i - 1];
dp[3][i] = dp[3][i - 1] + dp[4][i];
dp[2][i] = dp[2][i - 1] + dp[3][i];
dp[1][i] = dp[1][i - 1] + dp[2][i];
dp[0][i] = dp[0][i - 1] + dp[1][i];
}
return dp[0][n - 1] + dp[1][n - 1] + dp[2][n - 1] + dp[3][n - 1] + dp[4][n - 1];
}
}
这样避免了字符串的拼接以及重复的函数调用(记忆化递归结果也是相同的),时间复杂度O(N),效率大大提升。
排列组合法
感谢零神的题解,使用排列组合的隔板法,直接计算最终结果即可。
不过由于隔板法要保证两个隔板中间含有元素,而在本题中是可以不含元素的,所以可以给所有的n
个位置中添加5个虚位置,来保证至少有一个元素,因为是虚位置,插入板之后要删掉,这样就使得两个隔板中间可以没有元素了。
因此总共就有n + 5
个位置,中间的间隔可以插板的位置就有n + 4
个,由于最终要划分为5种元音字母,因此需要插的隔板有4
个。也就是在n + 4
个位置中取4
个位置,结果也就是:
那么直接求出这个公式的结果返回即可。
排列组合法代码
class Solution {
public int countVowelStrings(int n) {
return (n + 4) * (n + 3) * (n + 2) * (n + 1) / (4 * 3 * 2);
}
}
这一次周赛的数学问题真的很多,虽然其他方法也可以解决,总归还是感觉差那么点事儿,数学终归还是算法的归处啊。
文章有写的不对的地方还请指出,感恩相遇~