題目一:
思路:
作為動態規劃的第一道題,這個題很有代表性且很簡單,適合入門
先理解題意,很簡單,就是斐波那契數列的加強版,從前兩個數變為前三個數
算法原理:
這五步可以說是所有動態規劃的通用步驟,基本按照這個邏輯步驟,正確地完成每一個步驟,最后就能得到正確結果
1.狀態表示:
基本所有的動態規劃都要先創建一個dp表,也就是一維數組或者二維數組,而這個數組中的每一個空對應的含義就是狀態表示
像本道題,我們要創建一個一維數組,而數組下標n對應的值就應該為第n個泰波那契數,這個狀態表示就是我們所定義的,這個是最重要的一步,跟之前遞歸專題的定義遞歸函數功能是一樣的,你想怎么定義就怎么定義,這道題很明顯這么定義就很符合第一反應,因此這就完成了動態規劃的第一步,狀態表示
2.狀態轉移方程
用人話來說就是靠這個方程將dp表所有給填滿,而這一步就是動態規劃最難的一步,這道題題目已經給出了狀態轉移方程,那就是dp[n]=dp[n-1]+dp[n-2]+dp[n-3],所以我們就是根據這個狀態轉移方程去填這個dp表,當然其他動態規劃題就不會像本題一樣簡單,就需要自己去推導狀態轉移方程
3.初始化
其實就跟數獨一樣,題目得先給你一些存在的數,你才能去填其他的空,不能什么都不給,就讓你填,而初始化就是先填上一部分dp表的空,然后狀態轉移方程通過初始化的值去填剩下的空
本題就是給了前三個泰波那契數0,1,1,這樣我們才能通過狀態轉移方程往后推出更多的泰波那契數
同時也這一步也用于避免填表時越界的問題,比如當n<=2時,根據狀態轉移方程,就會出現訪問-1,-2下標的值,就越界了,所以我們需要對這些進行特判
4.填表順序
其實就是從左往右填,還是從右往左填,這道題很明顯是根據前面三個泰波那契數推導當前的泰波那契數,根據狀態表示,也就是數組靠前的是前面的泰波那契數,因此就是從左往右填
5.返回值
dp表填完后,那就該返回答案了,根據狀態表示,確定返回值,比如本題狀態表示是下標為n的代表第n個泰波那契數,那么題目要求我們返回第n個泰波那契數,那么我們就返回下標為n的就可以了,這時題目也就解決了
代碼1:
class Solution {public int tribonacci(int n) {//初始化加特判if(n<=2){if(n==0){return 0;}else{return 1;}}int[] dp=new int[n+1];dp[0]=0;dp[1]=1;dp[2]=1;//狀態轉移方程并填表for(int i=3;i<=n;i++){dp[i]=dp[i-1]+dp[i-2]+dp[i-3];}//返回值return dp[n];}
}
而這道題和后面的背包問題可以使用空間優化的方法,也就是滾動數組
我們可以發現有些數用完其實就沒用了,就像一次性筷子一樣,那么用完就該扔了,同理我們的數組其實就沒必要繼續保留那些用過的值了
像我們這道題,就只用4個變量,就可以解決了,abc代表前三個泰波那契數,d表示當前的泰波那契數,然后到下一個泰波納鍥數時,通過滾動的操作,將上一回的bcd賦給abc,然后再求出d,如此類推
這樣我們的時間復雜度就從原來的O(N)變為O(1),所以這種辦法可以降下一個指數級別的復雜度
代碼2(空間優化):
class Solution {public int tribonacci(int n) {//初始化加特判if(n<=2){if(n==0){return 0;}else{return 1;}}int a=0,b=1,c=1,d=0;for(int i=3;i<=n;i++){//狀態轉移方程d=a+b+c;//滾動數組a=b;b=c;c=d;}//返回值return d;}
}
題目二:
思路:
題意很簡單,就是每次可以有3種跳臺階的選擇,然后返回到某一階臺階有多少種跳法?
以n=3為例,小孩從第0階為出發點,可以選擇(1,2,3)每次跳1步到達第3階,也可以選擇(1,3)第一次跳1步,第二次跳2步到達第3階,也可以選擇(2,3)第一次跳2步,第二次跳1步到達第3階,還可以選擇直接跳3步到第3階
總共4種跳法,所以返回4
算法原理:
還是按照動態規劃的五個步驟走
1.狀態表示:
dp表的第i個下標表示到達第i階有多少種跳法
2.狀態轉移方程:
因為有3種跳法,所以如果要到第i階,只能從i-3階,i-2階,i-1階分別跳3步,2步,1步才能到達第i階,而dp[i-3],dp[i-2],dp[i-1]記錄著到達對應臺階的跳法,因此狀態轉移方程為dp[i]=dp[i-1]+dp[i-2]+dp[i-3]
3.初始化:
因為有三種跳法,所以至少要初始化前3個臺階,易知dp[1]=1,dp[2]=2,dp[3]=4,所以初始化這三個值然后并進行特判
4.填表順序:
從低臺階跳到高臺階,而dp表前面的值記錄的低臺階的跳法,后面的就記錄更高臺階的跳法,所以填表順序是從左往右填
5.返回值:
因為狀態表示中dp[i]表示第i階有多少種跳法,所以第n階有多少種跳法要返回dp[n],問題就解決了
代碼:
class Solution {public int waysToStep(int n) {//初始化并且特判if(n==1||n==2){return n;}if(n==3){return 4;}//求模數,e代表科學計數法,且默認為double類型int mod=(int)1e9+7;int[] dp=new int[n+1];dp[1]=1;dp[2]=2;dp[3]=4;//狀態轉移方程for(int i=4;i<=n;i++){dp[i]=((dp[i-1]+dp[i-2])%mod+dp[i-3])%mod;}//返回值return dp[n];}
}
其實這道題跟上面那道題幾乎一模一樣,因此也可以用滾動數組進行空間優化,就不多說了
題目三:
思路:
題意也很簡單,就是可以選擇跳1步還是跳2步,然后每次跳完要給錢,找到最小花費的值,唯一需要注意的是終點并不是最后一個元素,而是最后一個元素的后面,也就是數組越界的位置
算法原理:
?還是五個步驟
1.狀態表示
一般都是以dp表的第i個位置,表示題目的要求,本題那就是dp[i]就表示,到達第i階時所需要的最少花費
2.狀態轉移方程
一般根據dp表之前的填空情況來推導當前填表的答案,dp[i]表示第i階時所需要的最少花費,那么如果要到第i階,那么就只能從第i-1階或者i-2階開始跳,如果要求第i階時所需要的最少花費,那么就從第i-1階或者i-2階的最少花費再加上對應的花費選較少的那個,而第i-1階或者i-2階的最少花費又是由dp[i-1]和dp[i-2]記錄,因此就形成了閉環
所以狀態轉移方程:dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])
3.初始化
題目說可以從0下標和1下標開始跳,那么所以dp[0]=dp[1]=0,避免了當i過小時數組越界
4.填表順序
還是從下往上跳,那么dp表左邊的指向低臺階,dp表右邊指向高臺階,高臺階的最少花費由低臺階的最少花費決定,所以填表順序是從左往右
5.返回值
因為最后要跳出數組,所以要返回dp[n],因此dp表在創建的時候要多創一個位置new int[n+1]
問題就解決了
代碼1:
class Solution {public int minCostClimbingStairs(int[] cost) {//初始化int n=cost.length;int[] dp=new int[n+1];dp[0]=dp[1]=0;//狀態轉移方程for(int i=2;i<=n;i++){dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);}//返回值return dp[n];}
}
同理也可以逆向思考
剛剛的狀態表示是dp[i]為到第i階的最少花費,那么也可以反過來,dp[i]為從第i階為起點,到頂的最少花費,那么初始化也是先初始dp[n-1],dp[n-2],填表順序也變為從右往左,返回值則是dp[0]和dp[1]的最小值
代碼2:
class Solution {public int minCostClimbingStairs(int[] cost) {//初始化int n=cost.length;int[] dp=new int[n];dp[n-1]=cost[n-1];dp[n-2]=cost[n-2];//狀態轉移方程for(int i=n-3;i>=0;i--){dp[i]=Math.min(dp[i+1],dp[i+2])+cost[i];}//返回值return Math.min(dp[0],dp[1]);}
}
題目四:
?思路:
題意不是很難理解,就是返回一段字符串有多少種解碼方法
需要注意的就是6是合法解碼方式,06不是合法解碼方式,最大解碼為26,如果有一處出現了無法解碼,那么整條解碼方法就為0
算法原理:
還是五個步驟:
1.狀態表示?
一般都是以某一個位置為起點或結尾+題目要求,本題就以i位置為結尾,有多少種解碼數
2.狀態轉移方程
根據題意我們可以知道,要么是單獨解碼,要么是連續兩個一起解碼
這時就開始分類討論
如果是單獨解碼,如果不為0,那么此時解碼數就加上前一個位置的解碼數,如果為0,那就不能單獨解碼,就不加上前一個位置的解碼數,但也不能直接就認為解碼失敗,因為還有連續解碼的情況,比如10,20
然后是連續解碼,如果與前一個位置連起來在10-26之間,那么就說明能被連續解碼,這時前一個位置和當前位置就成為了一個整體,因此加上的是i-2位置的解碼數,反之,不在10-26之間就不加i-2位置的解碼數
因為我們狀態表示是以i位置為結尾,所以我們只用分析與前一個位置的連續解碼情況,而不用管后面的位置
所以總體來說狀態轉移方程為
dp[i]+=dp[i-1](單獨解碼如果成功)+dp[i-2](連續解碼如果成功)
3.初始化
為了避免越界情況,所以至少要先初始化dp[0]和dp[1]
dp[0]前面沒有元素,那么只能單獨解碼,所以如果是0就為0,不是0就為1
dp[1]前面有dp[0],所以可以單獨解碼也可以連續解碼,同理單獨解碼時如果是0就不加,不是0就加1,連續解碼時如果為10-26就加1,不是就不加
4.填表順序
因為是以i位置為結尾,所以是根據前面填好的情況來填后面的空,因此是從左往右
5.返回值
因為要返回整個解碼總數,所以是以n-1位置為結尾有多少解碼方法,而狀態表示以i位置為結尾,有多少種解碼數,所以就返回dp[n-1]
代碼1:
class Solution {public int numDecodings(String s) {//初始化int n = s.length();int[] dp = new int[n];char[] ch = s.toCharArray();//特判dp[0]和dp[1]if (ch[0] != '0') {dp[0] = 1;}if (n == 1) {return dp[0];}//單獨解碼if (ch[0] != '0' && ch[1] != '0') {dp[1] += 1;}//連續解碼int num = (ch[0] - '0') * 10 + (ch[1] - '0');if (num >= 10 && num <= 26) {dp[1] += 1;}//狀態轉移方程for (int i = 2; i < n; i++) {//單獨解碼if (ch[i] != '0') {dp[i] += dp[i - 1];}//連續解碼int num = (ch[i - 1] - '0') * 10 + (ch[i] - '0');if (num >= 10 && num <= 26) {dp[i] += dp[i - 2];}}//返回值return dp[n - 1];}
}
可以發現在初始化特判的時候非常冗雜,甚至比狀態轉移方程寫的還多,而其中dp[1]的初始化流程和狀態轉移方程幾乎一致,因此能不能直接將dp[1]的初始化流程放到狀態轉移方程中完成,當然是可以的
這里就用到了有些動態規劃題目可以采用虛擬節點的方法進行初始化
就是多創建一個空,然后所有往后移動一位
dp[1]就是原來的dp[0],那么還是只有單獨解碼的可能,因此是0就為0,不為0就為1,然后dp[2]就是原來的dp[1],根據狀態轉移方程?
dp[i] = dp[i-1](單獨解碼如果成功)+dp[i-2](連續解碼如果成功)
所以此時dp[2-2]應該設置為1,因為如果能連續解碼,那么就加1,因此虛擬節點就初始化為1,如果連續解碼不成功,那么就根本用不到dp[2-2],無所謂dp[2-2]的值,所以綜上要將dp[0]初始化為1
代碼2:
class Solution {public int numDecodings(String s) {//初始化int n = s.length();int[] dp = new int[n+1];char[] ch = s.toCharArray();//dp[0]和dp[1]dp[0]=1;if (ch[0] != '0') {dp[1] = 1;}//狀態轉移方程for (int i = 2; i <= n; i++) {//單獨解碼if (ch[i-1] != '0') {dp[i] += dp[i - 1];}//連續解碼int num = (ch[i - 2] - '0') * 10 + (ch[i-1] - '0');if (num >= 10 && num <= 26) {dp[i] += dp[i - 2];}}//返回值return dp[n];}
}
題目五:
思路:
之前在記憶化搜索的章節做過這道題,因為記憶化搜索和動態規劃其實大差不差,因此也可以用動態規劃做
算法原理:
這道題因為涉及了二維表,與之前一維表有些不一樣,但步驟都是那五步
1.狀態表示
?因為是二維表,那么就是以dp[ i ][ j ]為結尾,加上題目要求,到達ij位置時的路徑和
2.狀態轉移方程
因為只能向右或向下,所以到達該位置時,只能從上面到下面,從左邊到右邊,因此狀態轉移方程
dp[ i ] [ j?]=dp[i-1][ j-1]+dp[ i ][ j-1]
3.初始化
因為處于最左邊和最上邊的邊界格子,在狀態轉移方程中會越界,所以要對此進行特殊處理,而在一維中是采用多開一個位置,即虛擬節點,而在二維中,往往會在左邊多開一列,上邊多開一行,那么邊界情況就全部解決了,不會越界,而在起點dp[1][1]應該為1,所以可以直接給dp[1][1]賦值為1,也可以在dp[0][1]或者dp[1][0]賦值為1,然后dp[1][1]再通過狀態轉移方程賦值為1,都是可以的
4.填表順序
因為是根據上邊和左邊來決定當前位置,因此應該是從上往下,且從左往右填表的
5.返回值
dp[ i ][ j ]表示為當前位置的路徑和,題目要求返回到達終點有多少種不同路徑,那么我們就返回dp[m][n]即可
代碼:
class Solution {public int uniquePaths(int m, int n) {//初始化int[][] dp=new int[m+1][n+1];dp[0][1]=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][n];}
}
題目六:
思路:
跟上一道題幾乎一模一樣,就是多了障礙物這個設置
算法原理:?
還是五個步驟,除了狀態轉移方程與上一題不太一樣,其他都一樣
因為出現了障礙物,所以這個格子的dp值一定為0,因此只需要在填表時判斷一下是否有障礙物,如果有就直接設為0,如果不是障礙物,則繼續套用上一題的狀態轉移方程
代碼:
class Solution {public int uniquePathsWithObstacles(int[][] obstacleGrid) {//初始化int row=obstacleGrid.length;int col=obstacleGrid[0].length;int[][] dp=new int[row+1][col+1];dp[0][1]=1;//狀態轉移方程for(int i=1;i<=row;i++){for(int j=1;j<=col;j++){if(obstacleGrid[i-1][j-1]==1){dp[i][j]=0;}else{dp[i][j]=dp[i-1][j]+dp[i][j-1]; }}}//返回值return dp[row][col];}
}
題目七:
思路:
還是路徑問題,依然可以采用動態規劃,題意很簡單,就是從左上角為起點,右下角為終點,移動方向只能向下或向右,然后返回路徑上的最大值
算法原理:
?依舊是五個步驟
1.狀態表示
以i j位置為終點,到達這里的路徑最大值
2.狀態轉移方程
因為只有向下和向右兩個方向,那么能到達[i][ j ]位置的只能是[i-1][ j ],或者[i][ j-1] ,而又要找到當前位置路徑最大值,那么就選擇這兩格的最大值,再加上當前位置的值就是當前位置的最大值,因此狀態轉移方程是
dp[i][ j ]=Math.max(dp[i-1][ j ],[i][ j-1]) + frame[i][ j ]
3.初始化
依舊多加一行多加一列,方便填表時不越界
4.填表順序
根據移動方向可知,填表順序是從上往下,從左往右
5.返回值
根據狀態表示,返回dp[m][n]
代碼:
class Solution {public int jewelleryValue(int[][] frame) {//初始化int row=frame.length;int col=frame[0].length;int[][] dp=new int[row+1][col+1];//狀態轉移方程for(int i=1;i<=row;i++){for(int j=1;j<=col;j++){dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1])+frame[i-1][j-1];}}//返回值return dp[row][col];}
}
題目八:
思路:
依舊是路徑問題,只是這回移動方向變成了只能向下,且左右可以對角移動,最后返回最小下降路徑的值
算法原理:
依舊是五個步驟
1.狀態表示
以i j位置為終點,表示到達此位置時的最小下降路徑和
2.狀態轉移方程
因為方向多了對角線,且只能從上一行下來,因此為[i-1][ j-1 ],[i-1][ j?],[i-1][ j+1 ]
而要找的是最小的,所以就是狀態轉移方程為
dp[i][ j?]=Math.min(dp[i-1][ j-1 ],Math.min(dp[i-1][ j?],dp[i-1][ j+1 ]))
3.初始化
為了避免越界問題,所以依舊要擴容來將原數組包圍,所以就是多加一行,但是這回就要多加兩列,因為左邊要有一列,右邊要有一列,這樣才能全部包圍住,不會越界
因為要找最小的,所以擴容的位置的值不能初始化為0,不然會影響到真正的結果,因此要初始化為最大值
4.填表順序
根據移動方向順序,還是從上到下,從左往右
5.返回值
最后都會到達最后一行,所以只用返回最后一行的最小dp值即可
代碼:
class Solution {public int minFallingPathSum(int[][] matrix) {//初始化int row=matrix.length;int col=matrix[0].length;int[][] dp=new int[row+1][col+2];for(int i=1;i<=row;i++){dp[i][0]=dp[i][col+1]=Integer.MAX_VALUE;}//狀態轉移方程for(int i=1;i<=row;i++){for(int j=1;j<=col;j++){dp[i][j]=Math.min(dp[i-1][j-1],Math.min(dp[i-1][j],dp[i-1][j+1]))+matrix[i-1][j-1];}}//返回值int ret=dp[row][1];for(int j=2;j<=col;j++){ret=ret>dp[row][j]?dp[row][j]:ret;}return ret;}
}
題目九:
思路:
還是路徑問題,只是變成要求最小值,其他的要求和之前珠寶那道題目幾乎一樣
算法原理:
依舊是五步:
1.狀態表示
以i j位置為終點,表示到達此位置時的最小路徑和
2.狀態轉移方程
因為只有向下和向右兩個方向,那么能到達[i][ j ]位置的只能是[i-1][ j ],或者[i][ j-1] ,而又要找到當前位置路徑最小值,那么就選擇這兩格的最小值,再加上當前位置的值就是當前位置的最小值,因此狀態轉移方程是
dp[i][ j ]=Math.min(dp[i-1][ j ],[i][ j-1]) + grid[i][ j ]
3.初始化
依舊多加一行多加一列,方便填表時不越界,只是這里要求最小值,所以擴容的位置要初始化為MAX,而從起點開始,因此它的上一格或者左一格任選一個初始化為0即可
4.填表順序
根據移動方向可知,填表順序是從上往下,從左往右
5.返回值
根據狀態表示,返回dp[m][n]
代碼:
class Solution {public int minPathSum(int[][] grid) {//初始化int row=grid.length;int col=grid[0].length;int[][] dp=new int[row+1][col+1];for(int j=2;j<=col;j++){dp[0][j]=Integer.MAX_VALUE;}for(int i=1;i<=row;i++){dp[i][0]=Integer.MAX_VALUE;}//狀態轉移方程for(int i=1;i<=row;i++){for(int j=1;j<=col;j++){dp[i][j]=Math.min(dp[i-1][j],dp[i][j-1])+grid[i-1][j-1];}}//返回值return dp[row][col];}
}
題目十:
?思路:
依舊是路徑,題意也很好理解,跟游戲中打怪boss一樣,正數表示為補給包,負數代表有怪物,0表示為空房間,每次只能選擇進入右邊的房間或者下邊的房間,從左上角出發,到右下角結束,但過程中要保持有血量(路徑和)要大于等于1,問一開始起點初始的最低血量
以示例1為例子,按照該路徑走可以算出起始最低血量為7,但不要認為就是-2+-3+3+1+-5=-6,所以直接-6取反再加1就可以了,這里就容易出現一個誤區,那就是這個-6是所有路徑上所需血量的最小值,這道題剛好是最后一格是所有路徑上的所需血量的最小值,所以是-6
如果還有疑惑,舉個游戲中的例子就明白,你進入一個房間,遇到超級大怪獸,假設打倒需要10000血,然后打完這個怪獸后面有個房間有9999血的補給包,那這里你能直接
10000-9999+x(初始血量)>=1,x=2,能這樣算嘛,你都打不過那個怪獸,后面補給包不可能拿到的
初始最低血量應該為10001滴血
算法原理:
依舊是路徑問題,大致步驟也是一樣的
但是從第一步就容易出錯,根據之前的題目,我們狀態表示都是以某個位置為結尾,怎么這么樣,而這道題用以結尾這種狀態表示則是錯誤的,因為具有“有后效性”
可以簡單去嘗試下,就是填dp表時,剛根據上左dp值得出當前dp值,但是后面依然會影響當前dp值
如上圖,假設設置狀態表示為到達ij位置時,所需最低初始健康點數
那么-2這個格根據上左擴容的dp就應該為3,到了-3這個格子,發現最低應該是6,3這個格子為6,然后到3這個格子,你就不知道怎么填了,因為你不能6-3就填3吧,那就犯了一開始的誤區,也不能不管仍然填6吧,那后面就完全亂套了,最后返回-5這個格子的dp值肯定是錯的,這就是被后面的狀態給影響了,也就是“有后效性”
簡單來說還是被后面的血包影響了,以為你能欠血后面再還,實際是全程都不能低于1
因此就需要改變狀態表示,反正不是以結尾,就是以起點
1.狀態表示
以i j為起點,所需最低初始健康點數
2.狀態轉移方程
因為是以起點,那么就需要根據后面的dp值來填,即右格和下格的dp值
因為要找最低的,所以是用右格和下格的min,然后再減去當前格子dungeon的值,也就是dp值
當然因為dungeon有可能為正數(血包),所以減去后有可能為負數,此時又犯了欠血的誤區,因此要if判斷一下,如果為負數,那么就直接修改為1,因為至少需要活著
所以狀態轉移方程為
dp[i][j]=Math.min(dp[i+1][j],dp[i][j+1])-dungeon[i][j];
? ? ? ? ? ? ? ? if(dp[i][j]<=0){
? ? ? ? ? ? ? ? ? ? dp[i][j]=1;
? ? ? ? ? ? ? ? }
3.初始化
為了避免越界問題,所以要將原數組包圍起來,因此要多加一行多加一列,但只用包越界的那邊,因為這次是向右和向下,所以圍的是右邊和下邊
因為要找最小值,所以應該初始化為MAX,因為終點下右的都是初始化的,所以要任選一個更改為1,表示最低需要1滴血
4.填表順序
如果狀態表示是以起點,那么填表順序就反過來了,從下往上,從右往左
5.返回值
根據狀態表示返回第一格的dp就是
代碼:
class Solution {public int calculateMinimumHP(int[][] dungeon) {//初始化int row=dungeon.length;int col=dungeon[0].length;int[][] dp=new int[row+1][col+1];for(int i=0;i<=row;i++){dp[i][col]=Integer.MAX_VALUE;}for(int j=0;j<=col;j++){dp[row][j]=Integer.MAX_VALUE;}dp[row-1][col]=1;//狀態轉移方程for(int i=row-1;i>=0;i--){for(int j=col-1;j>=0;j--){dp[i][j]=Math.min(dp[i+1][j],dp[i][j+1])-dungeon[i][j];if(dp[i][j]<=0){dp[i][j]=1;}}}//返回值return dp[0][0];}
}