動態規劃解題步驟
- 確定dp陣列(dp table)以及下標的含義
- 確定遞推公式
- dp陣列如何初始化
- 確定遍歷順序
- 舉例推導dp陣列
[62. 不同路徑](https://leetcode.cn/problems/fibonacci-number/)
動態規劃的解法還是很好理解的。按照解題步驟來。
- 確定dp陣列及其下標的含義。
dp[i][j]
代表到達[i, j]
位置處的路徑數 - 確定遞推公式。這裡最重要的點就是
[i,j]
點只能從其上或右側的點到達。因此dp[i][j] = dp[i-1][j] + dp[i][j-1]
。 dp
初值的確定。顯然,第一行和第一列的所有節點都只有1種到達方式。- 按從上到下從左到右去遍歷即可。
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for(int i = 0; i < m; i++) dp[i][0] = 1;
for(int i = 0; i < n; i++) dp[0][i] = 1;
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
class Solution { //滾動陣列最佳化時間複雜度
public int uniquePaths(int m, int n) {
int[] dp = new int[n];
Arrays.fill(dp, 1);
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
dp[j] += dp[j-1];
}
}
return dp[n-1];
}
}
這道題還有數學的解法,其實我最開始也是這麼想的,但可能是排列組合忘太久了,想到的思路並不正確。只能看到題解後,哦哦哦,原來是這樣。其實就是組合數,從起點走到終點一共有m+n-2
步,其中一定有 m-1
步向下走(等價於一定有 n-1
步向下走),因此就是組合數$C_{m+n-2}^{m-1}$。這裡需要注意的就是可能出現越界的問題,所以在計算組合數時需要邊乘邊除。貼上程式碼,自己理解吧。
class Solution {
public int uniquePaths(int m, int n) {
long ans = 1;
for (int x = n, y = 1; y < m; ++x, ++y) {
ans = ans * x / y;
}
return (int) ans;
}
}
63. 不同路徑 II
這道題就不能使用數學的方式了。只能使用 dp
的方法來做。基本上與上一題是一致的,但是需要處理遇到石頭時的情況。這道題還可以使用 滾動陣列
最佳化,第一種方式熟練之後,很容易寫出第二種。
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1;
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
if(obstacleGrid[i][j] == 1) continue;
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
class Solution { //滾動陣列最佳化空間複雜度
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int[] dp = new int[n];
for(int j = 0; j < n && obstacleGrid[0][j] != 1; j++) dp[j] = 1;
for(int i = 1; i < m; i++)
for(int j = 0; j < n; j++)
if(obstacleGrid[i][j] == 1) dp[j] = 0;
else if(j != 0) dp[j] += dp[j-1];
return dp[n-1];
}
}
343. 整數拆分
這道題的思路真的太牛逼,照著這個思路誰都能寫出來,但這個思路想不到就是寄。對於一個數字,要麼分成兩個數字的乘積,要麼分成多個數字的乘積,最大值一定是這兩種情況之一,因此考慮用動態規劃。但是怎麼表示多個數字的乘積呢。這裡我就直接搬來題解中的 dp
陣列的定義了。
dp
陣列的定義:dp[i]
代表數字i
使得乘積最大的劃分方式得到的乘積。- 遞推公式:對於一個數字
n
,顯然他能拆分成(1 * n-1), (2 * n-2), (3 * n-3) ...
,這是對應於拆分成兩個數字的乘積的部分,怎麼表示其拆分成多個數字的乘積呢。這裡就是最重要的部分了,數字n的拆分成多個數字乘積可以這樣表示(1 * dp[n-1]), (2 * dp[n-2]), (3, dp[n-3])...
class Solution {
public int integerBreak(int n) {
int[] dp = new int[n+1];
dp[2] = 1;
for(int i = 3; i <= n; i++)
for(int j = 1; j <= i/2; j++)
dp[i] = Math.max(dp[i], Math.max(j * (i-j), j * dp[i-j])); //別忘了還要和dp[i]自己比較,留存最優結果
return dp[n];
}
}
96. 不同的二叉搜尋樹
和上一題的思路基本一致,只有在遞推關係式部分有所不同。
dp
陣列的含義:dp[i]
代表了i個節點的二叉搜尋樹的個數。- 遞推公式:對於包含了n個節點的搜尋樹,顯然,根節點一定是必不可少的,其餘節點分配在兩個子樹上。以左子樹為準,可以包含
0 ~ n-1
個節點,對應的右子樹就包含了n-1 ~ 0
個節點,所以dp[左子樹節點個數] * dp[右子樹節點個數]
的累加和就是總的數量。
class Solution {
public int numTrees(int n) {
int[] dp = new int[n+1];
dp[0] = 1; dp[1] = 1;
for(int i = 2; i <= n; i++){
for(int j = 0; j < i; j++){ //j代表左子樹上節點個數
dp[i] += dp[j] * dp[i-1-j];
}
}
return dp[n];
}
}