1641-統(tǒng)計字典序元音字符串的數(shù)目-排列組合巧解

題目

核心思路

題目的含義還是挺好理解的,作為周賽的第二題也還算中規(guī)中矩。我們需要找到長度為n的、按字典序排列的、僅由元音字母組成的字符串數(shù)量。其中最重要的就是要滿足按字典序排列,也就是示例二所說的"ea" 不是符合題意的字符串,因為 'e' 在字母表中的位置比 'a' 靠后。

暴力法(深搜、寬搜)

理解了題意,直接使用深搜或者寬搜模擬拼接的過程就可以了,可以先確定第一個字符為c(c表示一個元音字符),那么他后邊能接的只有大于或等于他的字符,接著來處理他后邊的那個字符,形成了一個遞歸的過程,就可以很容易得出答案了。而遞歸出口,就是字符串的長度為目標值n即可。
注意:因為實際上我們只需要拼接字符串的最后一個字符,所以直接對字符而非字符串遞歸即可,這樣可以節(jié)省很多時間。

暴力法代碼
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;
    }
}

這里使用遞歸的方式實現(xiàn),迭代方式思想也是一樣的,就不再過多贅述。

動態(tài)規(guī)劃法

觀察暴力法,在函數(shù)參數(shù)c, n相同的情況下,返回的結(jié)果也是肯定相同的,而在函數(shù)調(diào)用中可能會多次調(diào)用同一個函數(shù),因此不妨直接記錄dp[c][n] 表示以字符c開頭,長度為n的滿足條件的字符串的數(shù)量。當然直接當做記憶化遞歸完善上邊代碼倒也是可以的,不過由于只有5個字符,完全可以迭代一層循環(huán)解決,會簡便不少。
我們考慮下邊的例子:n = 5
如果對所有結(jié)果分類的話,可以分為:

  • a 開頭的長度為n字符串數(shù)量
  • e 開頭的長度為n字符串數(shù)量
  • i 開頭的長度為n字符串數(shù)量
  • o 開頭的長度為n字符串數(shù)量
  • u 開頭的長度為n字符串數(shù)量

那么以 a 開頭的字符串又有什么規(guī)律呢?它的后邊5種字符都可以接,也就是按上述遞歸,包含以 'a' 開頭長度為 'n - 1'的字符串數(shù)量、以 'e'開頭長度為 ‘n - 1' 的字符串的數(shù)量……最終,也就是長度為一的字符串的數(shù)量,均為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的方式遍歷,避免了中間重復加的數(shù)值,稍微簡化了點代碼。

動態(tài)規(guī)劃代碼
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];
    }
}

這樣避免了字符串的拼接以及重復的函數(shù)調(diào)用(記憶化遞歸結(jié)果也是相同的),時間復雜度O(N),效率大大提升。

排列組合法

感謝零神的題解,使用排列組合的隔板法,直接計算最終結(jié)果即可。
不過由于隔板法要保證兩個隔板中間含有元素,而在本題中是可以不含元素的,所以可以給所有的n個位置中添加5個虛位置,來保證至少有一個元素,因為是虛位置,插入板之后要刪掉,這樣就使得兩個隔板中間可以沒有元素了。
因此總共就有n + 5個位置,中間的間隔可以插板的位置就有n + 4個,由于最終要劃分為5種元音字母,因此需要插的隔板有4個。也就是在n + 4個位置中取4個位置,結(jié)果也就是:


那么直接求出這個公式的結(jié)果返回即可。

排列組合法代碼
class Solution {
    public int countVowelStrings(int n) {
        return (n + 4) * (n + 3) * (n + 2) * (n + 1) / (4 * 3 * 2);
    }
}

這一次周賽的數(shù)學問題真的很多,雖然其他方法也可以解決,總歸還是感覺差那么點事兒,數(shù)學終歸還是算法的歸處啊。
文章有寫的不對的地方還請指出,感恩相遇~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容