目錄
兩個數組dp問題
1.最長公共子序列
2.不同的子序列
3.通配符匹配
????????本文旨在通過對力扣上三道題進行講解來讓大家對使用動態規劃解決兩個數組的dp問題有一定思路,培養大家對狀態定義,以及狀態方程書寫的思維。
順序:
題目鏈接-》算法思路-》代碼呈現
兩個數組dp問題
動態規劃類題目解題步驟:
- 依據題目進行狀態表示(dp[i]的含義)
- 寫出狀態轉移方程(類似于dp[i]=dp[i-1]+dp[i-2])
- 為防止填表時數組越界,對dp表進行初始化(dp[0]=dp[1]=1)
- 搞清楚填表順序(從前往后或者從后往前)
- 利用dp表返回問題答案
1.最長公共子序列
題目鏈接:
https://leetcode.cn/problems/longest-common-subsequence/
算法思路:
1. 狀態表?:
????????對于兩個數組的動態規劃,我們的定義狀態表?的經驗就是:
- 選取第?個數組 [0, i] 區間以及第?個數組 [0, j] 區間作為研究對象;
- 結合題?要求,定義狀態表?。
在這道題中,我們根據定義狀態表?為:
dp[i][j] 表?: s1 的 [0, i] 區間以及 s2 的 [0, j] 區間內的所有的?序列中,最?公共?序列的?度。
2. 狀態轉移?程:
????????分析狀態轉移?程的經驗就是根據「最后?個位置」的狀況,分情況討論。
對于 dp[i][j] ,我們可以根據 s1[i] 與 s2[j] 的字符分情況討論:
1.? 兩個字符相同, s1[i] = s2[j] :那么最?公共?序列就在 s1 的 [0, i - 1] 及 s2 的 [0, j - 1] 區間上找到?個最?的,然后再加上 s1[i] 即可。因此dp[i][j] = dp[i - 1][j - 1] + 1 ;
2. 兩個字符不相同, s1[i] != s2[j] :那么最?公共?序列?定不會同時以 s1[i]和 s2[j] 結尾。那么我們找最?公共?序列時,有下?三種策略:
- 去 s1 的 [0, i - 1] 以及 s2 的 [0, j] 區間內找:此時最??度為 dp[i - 1][j] ;
- 去 s1 的 [0, i] 以及 s2 的 [0, j - 1] 區間內找:此時最??度為 dp[i ] [j - 1] ;
- 去 s1 的 [0, i - 1] 以及 s2 的 [0, j - 1] 區間內找:此時最??度為dp[i - 1][j - 1] 。
我們要三者的最?值即可。但是我們細細觀察會發現,第三種包含在第?種和第?種情況??,但是我們求的是最?值,并不影響最終結果。因此只需求前兩種情況下的最?值即可。
綜上,狀態轉移?程為:
if(s1[i] == s2[j]) dp[i][j] = dp[i - 1][j - 1] + 1 ;
if(s1[i] != s2[j]) dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) 。
3. 初始化:
- 「空串」是有研究意義的,因此我們將原始 dp 表的規模多加上??和?列,表?空串。
- 引?空串后,??的?便我們的初始化。
- 但也要注意「下標的映射關系」,以及??的值要「保證后續填表是正確的」。
當 s1 為空時,沒有?度,同理 s2 也是。因此第??和第?列??的值初始化為 0 即可保證后續填表是正確的。
4. 填表順序:
????????根據「狀態轉移?程」得:從上往下填寫每??,每??從左往右。
5. 返回值:
????????根據「狀態表?」得:返回 dp[m][n] 。
代碼呈現:
class Solution {public int longestCommonSubsequence(String text1, String text2) {int re=0;char[] s=text1.toCharArray();char[] s1=text2.toCharArray();int m=s.length,n=s1.length;int[][] dp=new int[m+1][n+1];for(int i=1;i<=m;i++){for(int j=1;j<=n;j++){if(s[i-1]==s1[j-1]){dp[i][j]=dp[i-1][j-1]+1;}else{dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);}}}return dp[m][n];}
}
2.不同的子序列
題目鏈接:
https://leetcode.cn/problems/distinct-subsequences/
算法思路:
1. 狀態表?:
????????對于兩個字符串之間的 dp 問題,我們?般的思考?式如下:
- 選取第?個字符串的 [0, i] 區間以及第?個字符串的 [0, j] 區間當成研究對象,結合題?的要求來定義「狀態表?」;
- 然后根據兩個區間上「最后?個位置的字符」,來進?「分類討論」,從?確定「狀態轉移?程」。
我們可以根據上?的策略,解決?部分關于兩個字符串之間的 dp 問題。
dp[i][j] 表?:在字符串 s 的 [0, j] 區間內的所有?序列中,有多少個 t 字符串 [0,i] 區間內的?串。
2. 狀態轉移?程:
?????????規矩,根據「最后?個位置」的元素,結合題?要求,分情況討論:
1. 當 t[i] == s[j] 的時候,此時的?序列有兩種選擇:
- ?種選擇是:?序列選擇 s[j] 作為結尾,此時相當于在狀態 dp[i - 1][j - 1]中的所有符合要求的?序列的后?,再加上?個字符 s[j] (請?家結合狀態表?,好好理解這句話),此時 dp[i][j] = dp[i - 1][j - 1] ;
- 另?種選擇是:我就是任性,我就不選擇 s[j] 作為結尾。此時相當于選擇了狀態dp[i][j - 1] 中所有符合要求的?序列。我們也可以理解為繼承了上個狀態??的求得的?序列。此時 dp[i][j] = dp[i][j - 1] ;
兩種情況加起來,就是 t[i] == s[j] 時的結果。
2. 當 t[i] != s[j] 的時候,此時的?序列只能從 dp[i][j - 1] 中選擇所有符合要求的?序列。只能繼承上個狀態??求得的?序列, dp[i][j] = dp[i][j - 1] ;
綜上所述,狀態轉移?程為:
- 所有情況下都可以繼承上?次的結果: dp[i][j] = dp[i][j - 1] ;
- 當 t[i] == s[j] 時,可以多選擇?種情況: dp[i][j] += dp[i - 1][j - 1]
3. 初始化:
- 「空串」是有研究意義的,因此我們將原始 dp 表的規模多加上??和?列,表?空串。
- 引?空串后,??的?便我們的初始化。
- 但也要注意「下標的映射關系」,以及??的值要「保證后續填表是正確的」。
當 s 為空時, t 的?串中有?個空串和它?樣,因此初始化第??全部為 1 。
4. 填表順序:
????????「從上往下」填每??,每??「從左往右」。
5. 返回值:
????????根據「狀態表?」,返回 dp[m][n] 的值。
本題有?個巨惡?的地?,題?上說結果不會超過 int 的最?值,但是實際在計算過程會會超。為
了避免報錯,我們選擇 double 存儲結果。
代碼呈現:
class Solution {public int numDistinct(String s, String t) {int MOD=1000000007;s=" "+s;t=" "+t;char[] s1=s.toCharArray();char[] s2=t.toCharArray();int n=s1.length;int m=s2.length;int[][] dp=new int[n][m];for(int i=0;i<n;i++){dp[i][0]=1;}for(int i=1;i<n;i++){for(int j=1;j<m;j++){dp[i][j]=dp[i-1][j];if(s1[i]==s2[j]){dp[i][j]+=dp[i-1][j-1];}}}return dp[n-1][m-1];}
}
3.通配符匹配
題目鏈接:
https://leetcode.cn/problems/wildcard-matching/
算法思路:
1. 狀態表?:
????????對于兩個字符串之間的 dp 問題,我們?般的思考?式如下:
- 選取第?個字符串的 [0, i] 區間以及第?個字符串的 [0, j] 區間當成研究對象,結合題?的要求來定義「狀態表?」;
- 然后根據兩個區間上「最后?個位置的字符」,來進?「分類討論」,從?確定「狀態轉移?程」。
我們可以根據上?的策略,解決?部分關于兩個字符串之間的 dp 問題。
因此,我們定義狀態表?為:dp[i][j] 表?: p 字符串 [0, j] 區間內的?串能否匹配字符串 s 的 [0, i] 區間內的?串。
2. 狀態轉移?程:
?????????規矩,根據最后?個位置的元素,結合題?要求,分情況討論:
1. 當 s[i] == p[j] 或 p[j] == '?' 的時候,此時兩個字符串匹配上了當前的?個字符,只能從 dp[i - 1][j - 1] 中看當前字符前?的兩個?串是否匹配。只能繼承上個狀態中的匹配結果, dp[i][j] = dp[i][j - 1] ;
2. 當 p[j] == '*' 的時候,此時匹配策略有兩種選擇:
- ?種選擇是: * 匹配空字符串,此時相當于它匹配了?個寂寞,直接繼承狀態 dp[i][j - 1] ,此時 dp[i][j] = dp[i][j - 1] ;
- 另?種選擇是: * 向前匹配 1 ~ n 個字符,直?匹配上整個 s1 串。此時相當于從 dp[k][j - 1] (0 <= k <= i) 中所有匹配情況中,選擇性繼承可以成功的情況。此時 dp[i][j] = dp[k][j - 1] (0 <= k <= i) ;
3. 當? p[j] 不是特殊字符,且不與 s[i] 相等時,?法匹配。三種情況加起來,就是所有可能的匹配結果。
綜上所述,狀態轉移?程為:
- 當 s[i] == p[j] 或 p[j] == '?' 時: dp[i][j] = dp[i][j - 1] ;
- 當 p[j] == '*' 時,有多種情況需要討論: dp[i][j] = dp[k][j - 1] (0 <=k <= i) ;
優化:當我們發現,計算?個狀態的時候,需要?個循環才能搞定的時候,我們要想到去優化。優
化的?向就是??個或者兩個狀態來表?這?堆的狀態。通常就是把它寫下來,然后?數學的?式
做?下等價替換:
當 p[j] == '*' 時,狀態轉移?程為:
dp[i][j] = dp[i][j - 1] || dp[i - 1][j - 1] || dp[i - 2][j - 1]
......
我們發現 i 是有規律的減?的,因此我們去看看 dp[i - 1][j] :
dp[i - 1][j] = dp[i - 1][j - 1] || dp[i - 2][j - 1] || dp[i - 3][j - 1] ...... 我們驚奇的發現, dp[i][j] 的狀態轉移?程??除了第?項以外,其余的都可以? dp[i - 1][j] 替代。因此,我們優化我們的狀態轉移?程為: dp[i][j] = dp[i - 1][j] || dp[i][j - 1] 。
3. 初始化:
????????由于 dp 數組的值設置為是否匹配,為了不與答案值混淆,我們需要將整個數組初始化為false 。
由于需要?到前??和前?列的狀態,我們初始化第??、第?列即可。
- dp[0][0] 表?兩個空串能否匹配,答案是顯然的, 初始化為 true 。
- 第??表? s 是?個空串, p 串和空串只有?種匹配可能,即 p 串表?為 "***" ,此時 也相當于空串匹配上空串。所以,我們可以遍歷 p 串,把所有前導為 "*" 的 p ?串和空串 的 dp 值設為 true 。
- 第?列表? p 是?個空串,不可能匹配上 s 串,跟隨數組初始化即可。
4. 填表順序:
????????從上往下填每??,每??從左往右。
5. 返回值:
????????根據狀態表?,返回 dp[m][n] 的值。
代碼呈現:
class Solution {public boolean isMatch(String s, String p) {s=" "+s;p=" "+p; char[] s1=s.toCharArray();char[] s2=p.toCharArray();int n=s1.length;int m=s2.length;boolean[][] dp=new boolean[n][m];dp[0][0]=true;for(int i=1;i<m;i++){if(s2[i]=='*'){dp[0][i]=dp[0][i-1];}}for(int i=1;i<n;i++){for(int j=1;j<m;j++){if(s2[j]=='?'||s1[i]==s2[j]){dp[i][j]=dp[i-1][j-1];}if(s2[j]=='*'){dp[i][j]=dp[i][j-1]||dp[i-1][j];}}}return dp[n-1][m-1];}
}