1641-统计字典序元音字符串的数目-排列组合巧解

题目

核心思路

题目的含义还是挺好理解的,作为周赛的第二题也还算中规中矩。我们需要找到长度为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);
    }
}

这一次周赛的数学问题真的很多,虽然其他方法也可以解决,总归还是感觉差那么点事儿,数学终归还是算法的归处啊。
文章有写的不对的地方还请指出,感恩相遇~

你可能感兴趣的:(1641-统计字典序元音字符串的数目-排列组合巧解)