動態規劃超清晰易懂的教程

題目描述

https://leetcode.com/problems/distinct-subsequences/算法

這道題的分類是動態規劃,動態規劃很難掌握,對於沒搞過信息學競賽的人來講更是難上加難,接下來我會用一個接地氣的方法教你們如何推導出這道題的動態規劃遞推公式。數組

題目大意是給定一個字符串S和子串T,請問S中有多少個T的字串?緩存

Given a string S and a string T, count the number of distinct subsequences of S which equals T.函數

A subsequence of a string is a new string which is formed from the original string by deleting some (can be none) of the characters without disturbing the relative positions of the remaining characters. (ie, "ACE" is a subsequence of "ABCDE" while "AEC" is not).優化

例子1: Input: S = "rabbbit", T = "rabbit" Output: 3 Explanation: ​ As shown below, there are 3 ways you can generate "rabbit" from S. (The caret symbol ^ means the chosen letters) ​ rabbbit ^^^^ ^^ rabbbit ^^ ^^^^ rabbbit ^^^ ^^^ 例子2: Input: S = "babgbag", T = "bag" Output: 5 Explanation: ​ As shown below, there are 5 ways you can generate "bag" from S. (The caret symbol ^ means the chosen letters) ​ babgbag ^^ ^ babgbag ^^    ^ babgbag ^    ^^ babgbag ^  ^^ babgbag ^^^

 

 

回溯法

這題的標籤是動態規劃,可是我對動態規劃還不是很熟練,而動態規劃是回溯的優化版本,有位大師說過一切動態規劃問題均可以由回溯問題轉換而來。因爲我對回溯更加熟練,所以我決定先實現一個回溯版本的題解。spa

思路

從左到右遍歷S的每一個字符,若是T的字符在S中出現了,則這兩個字符對碰消掉,接着考慮這兩個字符不存在的狀況。code

在S遍歷的循環中須要考慮兩個狀況:orm

  • 若是S當前的的字符等於T的第一個字符,則刪除S和T中的這兩個對應的字符,接着遞歸調用。例如func("abc","ac"),兩個字串的第一個字母相等,下一次遞歸調用的入參則應該是func("bc","c")對象

  • 若是S當前的字符不等於T的第一個字符,則繼續遍歷S。blog

函數遞歸的中止條件爲T串爲空,意味着子串T的全部字符都被S的字符抵消掉,所以是一個子串組合。

每一個遞歸的盡頭意味着是一個子串組合的解,所以咱們將全部的遞歸函數的返回值累加起來便可獲得全部可能的組合數量。

代碼實現

咱們對函數添加入參,表示字符串S和字符串T的開始下標,以此達到刪除某個字符的目的,而不用頻繁建立字符串對象,例如調用substring()。

class Solution { public static void main(String[] args) { System.out.println(new Solution().numDistinct("babgbag","bag")); } public int numDistinct(String s, String t) { return find(s, 0, t, 0); } ​ private int find(String s, int sIndex, String t, int tIndex) { System.out.println("find("+sIndex+","+tIndex+")"); if (s.length() == 0 && t.length() == 0) { return 1; } if (tIndex == t.length()) { return 1; } int sum = 0; for (int i = sIndex; i < s.length(); i++) { if (s.charAt(i) == t.charAt(tIndex)) { sum += find(s, i + 1, t, tIndex + 1); } } return sum; } }

 

輸出結果

find(0,0) find(1,1) find(2,2) find(4,3) find(7,3) find(6,2) find(7,3) find(3,1) find(6,2) find(7,3) find(5,1) find(6,2) find(7,3) 5

 

缺點

這個解法超時了,從輸出結果中能夠看出,find方法有很多重複的調用,例如find(7,3)調用了4次,可是得到的結果應該沒什麼不一樣,畢竟入參都同樣,所以咱們須要緩存已經算出來的結果。

咱們使用備忘錄法,將每次find()調用的結果保存起來存在一個二維數組中,遞歸調用以前先查詢是否已經計算過,若是計算過則跳過沒必要要的重複計算,這個方法已是動態規劃的雛形了,具體實現再也不贅述。

動態規劃法

從回溯法怎麼優化爲動態規劃法呢?從上述回溯法的解法中咱們能夠知道,find(0,0)的返回值就是答案,即從S串、T串的下標0開始逐個字符匹配,咱們用一個二維數組dp表明find()的計算結果。dp[0] [0]則意味着字符串S從下標0開始,子串T從下標0開始,S有多少種T的子串。

初始化

觀察回溯函數find()的終止條件,可知當T的下標等於T的長度時,獲得一個子串組合,此時爲一個解。因爲T的下標會等於T的長度,所以所以dp數組的長度須要加1。初始化爲

int[][] dp=new int[S.length()+1][T.length()+1]; for(int i=0;i<=S.length;i++) dp[i][T.length()]=1;

 

從回溯的角度分析,上述初始化對應的回溯函數的入參爲find("ab...bc",""),即子串T爲空。

遞推式

接下來思考遞推式

若是S的某個字符等於T的某個字符,則將這兩個字符同時消掉,當作不存在,所以有:

  • dp[i] [j]=dp[i+1] [j+1] if S.charAt(i) == T.charAt(j)

或者只消掉S的對應字符,至關於忽略S的這個字符,看看S後面還有沒有可能出現這個字符,組成一個不同的子串:

  • dp[i] [j]=dp[i+1] [j] if S.charAt(i) == T.charAt(j)

若是S的字符不等於T的字符,說明不匹配,繼續在S的後面的字符尋找能和T匹配的字符:

  • dp[i] [j]=dp[i+1] [j] if S.charAt(i) != T.charAt(j)

代碼實現

根據上述分析,寫出通俗易懂的動態規劃算法,有理有據,使人信服。

public int dp(String s, String t) { int[][] dp = new int[s.length() + 1][t.length() + 1]; for (int i = 0; i <= s.length(); i++) { dp[i][t.length()] = 1; } for (int i = s.length() - 1; i >= 0; i--) { for (int j = t.length() - 1; j >= 0; j--) { if (s.charAt(i) == t.charAt(j)) { dp[i][j] = dp[i + 1][j + 1] + dp[i + 1][j]; } else { dp[i][j] = dp[i + 1][j]; } } } return dp[0][0]; }
相關文章
相關標籤/搜索