Given a length n, return the number of strings of length n that can be made up of the letters 'a', 'b', and 'c', where there can only be a maximum of 1 'b' and can only have up to two consecutive 'c's.
Example:
findStrings(3) returns 19
since the possible combinations are:
aaa,aab,aac,aba,abc,aca,acb,baa,bac,bca,caa,cab,cac,cba,cbc,acc,bcc,cca,ccb and the invalid combinations are:
abb,bab,bba,bbb,bbc,bcb,cbb,ccc
給出一個長度n,求只包含字母a、b、c,且最多只能有1個b,不包含連續的超過兩個的c的,長度為n的字符串的數目。
1. 詢問
首先要完全明白題意。根據題目的意思,a和b很好理解,c的約束條件則意味著ccacc這樣的是滿足的。
2. 分析
暴力破解
把所有abc構成的長度為n的字符串都造出來,然后剔除掉不符合的。復雜度O(3^n)。
為何低效
有在明知道前面已經不可能的情況下還繼續去試,這就是暴力破解低效的原因。
假如使用DFS遞歸,則每一步都需要知道之前的狀態,如果每一步再檢查整個字符串一次過于低效,于是選擇把狀態放入參數里面一起帶著傳下去。什么參數很重要?有沒有使用過'b',和末尾'c'的數目。根據這兩個參數,就可以知道如何添加字母。
這是一種解法,但時間復雜度不是很直觀。雖然知道比起暴力破解肯定效率提高了很多,但具體多少?
而受到這種思路的啟發,可以用DP來解決這個問題。
DP解法
和上面的思路類似,構造三維dp矩陣,dp[i][j][k],i代表對應字符串的長度,j可選0或1,0表示沒有使用b,1表示使用過;k可選012,表示末尾連續c的長度。
因此,在已知上一個dp[i]的情況下,下一個dp[i+1]也可以推導出來:
- dp[i+1][0][0] = dp[i][0]:這表示在之前沒有使用'b'的字符串后新添加一個'a',那么無論之前后面有多少'c',被'a'分隔之后,現在的末尾連續'c'都是0了。
- dp[i+1][1][0] = dp[i]:假如之前使用過'b',那么末尾添加'a',否則添加'b'。
- dp[i+1][0][1] = dp[i][0][0]:要求添加完成之后末尾只能有一個'c',那么只能在最后加'c',也就是說之前末尾不是'c'。'b'的使用情況繼承。
- dp[i+1][1][1] = dp[i][1][0]:同上。
- dp[i+1][0][2] = dp[i][0][1]:類似地,要有兩個'c',之前需要有一個'c'。
- dp[i+1][1][2] = dp[i][1][1]:同上。
由此,得到所有遞推公式。
確定初始邊界值:dp[1][0][0] = 1 (a); dp[1][0][1] = 1 (c); dp[1][1][0] = 1 (b); dp[1][1][1],dp[1][0][2],dp[1][1][2]=0。
時間復雜度O(n),空間復雜度O(n)。
3. 代碼
class Solution:
def findString(self, n):
if not n:
return 0
dp = [[[0] * 3 for _ in range(2)] for _ in range(n + 1)]
dp[1][0][0] = 1
dp[1][0][1] = 1
dp[1][1][0] = 1
for i in range(2, n + 1):
dp[i][0][0] = dp[i - 1][0][0] + dp[i - 1][0][1] + dp[i - 1][0][2]
dp[i][1][0] = 0
for k in range(2):
for h in range(3):
dp[i][1][0] += dp[i - 1][k][h]
dp[i][0][1] = dp[i - 1][0][0]
dp[i][1][1] = dp[i - 1][1][0]
dp[i][0][2] = dp[i - 1][0][1]
dp[i][1][2] = dp[i - 1][1][1]
ret = 0
for k in range(2):
for h in range(3):
ret += dp[-1][k][h]
return ret
4. 總結
難度Hard。DP在只求一個數值時確實是非常高效的解法。假如要列出所有符合條件的字符串,可能DFS+遞歸更加好用。