动态规划(浅层基础)
引言
在我看來動態規劃和遞歸有很大的相似性,可以說動態規劃就是剪枝后的遞歸,它舍去了遞歸重復的步驟,在時間復雜度上相對于遞歸有著非常大的提升,就比如說斐波那契數列(下題有)用遞歸的時間復雜度是指數級,而用動態規劃則可以將其優化為線性級,且動態規劃還可以繼續進行優化使其空間復雜度大大減小,這就是動態規劃進階的內容了;
做動態規劃類的題目有很多相似的套路和方法,開始肯定會有一些困難,題目見多了就會發現好多題有著相似的解法;
我的一般做題步驟是:
1,先找最后一步并定義一個dp[i]數組元素的含義(不一定是數組由題目而定)
2,找出關系數組間的關系式:由最后一步向前推一步,想通過哪一步(或幾步)可以走到最后一步,通過這點,找到數組元素間的關系式(轉移方程)
3,找出初始值(轉移方程算不出的):通過轉移方程來判斷,有沒有特殊情況無法使用轉移方程,單獨列出來,這就是初始值;
ps:這個方法不一定適用于所有題,一定要就題而論;
什么樣的題適合動態規劃求解呢?我總結有以下幾種:
1,求最值類
2,求幾種方法類(計數)
3,判斷是否存在滿足某個條件(一般布爾類型)
下面來幾道有點人氣的題目
斐波那契數
斐波那契數,通常用 F(n) 表示,形成的序列稱為 斐波那契數列 。該數列由 0 和 1 開始,后面的每一項數字都是前面兩項數字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
給你 n ,請計算 F(n) 。
示例 1:
輸入:2
輸出:1
解釋:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:
輸入:3
輸出:2
解釋:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:
輸入:4
輸出:3
解釋:F(4) = F(3) + F(2) = 2 + 1 = 3
提示:
0 <= n <= 30
這道題就是非常經典的動態規劃題目,也是遞歸題目,當然在有些情況下遞歸解題會時間超限,所以學會動態規劃解法還是很重要的;
解題步驟:
1,可以設一個dp[i]的數組,是第i個數,該數組表示第i個數的和;
2,由題目可以輕松找到狀態轉移方程dp[i] = dp[i - 1] + dp[i - 2];
3,通過題目可以看出當i = 0時為dp[0] = 0,而且狀態轉移方程當i小于2時并不滿足,所以單獨列出特殊情況:dp[1] = dp[2] = 1;
代碼如下:
int fib(int N) {vector<int> dp(N + 1, 0);dp[1] = dp[2] = 1;for (int i = 3; i <= N; i++)dp[i] = dp[i - 1] + dp[i - 2];return dp[N]; }我們還可以將空間復雜度從O(n)優化到O(1),該怎么做呢?我們分析可以發現,每次dp[i]加的值總是它的前兩個,所以我們只需要記錄前兩個值,并不斷更新這兩個值就可以了;
代碼如下:
class Solution { public:int fib(int n) {int a = 0, b = 1, sum;for (int i = 0; i < n; ++i) {sum = a + b;a = b;b = sum;}return a;//最后通過多走一步使sum結果值賦給a,前面就不用寫判斷了} };來到相似的題練習下:
第 N 個泰波那契數
泰波那契序列 Tn 定義如下:
T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的條件下 Tn+3 = Tn + Tn+1 + Tn+2
給你整數 n,請返回第 n 個泰波那契數 Tn 的值。
示例 1:
輸入:n = 4
輸出:4
解釋:
T_3 = 0 + 1 + 1 = 2
T_4 = 1 + 1 + 2 = 4
示例 2:
輸入:n = 25
輸出:1389537
提示: 0 <= n <= 37
答案保證是一個 32 位整數,即 answer <= 2^31 - 1。
一樣的題意,思路就不寫了直接上代碼:
class Solution { public:int tribonacci(int n) {int dp[n + 1];if (n < 3) return n == 0? 0 : 1;dp[0] = 0;dp[1] = dp[2] = 1;for (int i = 3; i <= n; ++i)dp[i] = dp[i - 3] + dp[i - 2] + dp[i - 1];return dp[n];} };優化版本
class Solution {public:int tribonacci(int n) {int sum, x = 0, y = 1, z = 1;for (int i = 0; i < n; ++i) {sum = x + y + z;x = y;y = z;z = sum;}return x;} }連續子數組的最大和
輸入一個整型數組,數組中的一個或連續多個整數組成一個子數組。求所有子數組的和的最大值。
要求時間復雜度為O(n)。
示例1:
輸入: nums = [-2,1,-3,4,-1,2,1,-5,4]
輸出: 6
解釋: 連續子數組 [4,-1,2,1] 的和最大,為 6。
提示:
1 <= arr.length <= 10^5
-100 <= arr[i] <= 100
這是一道求最值的題,非常經典的動態規劃,來分析一下題目
1,dp[i]為nums數組中第i個數的最大值;
2,dp[i] = max(dp[i - 1] + nums[i], nums[i]);當前最大值為前一個最大值加上當前nums的值與當前nums值的最大值,防止dp[i-1]為負數;
3,dp[0] = nums[0];
代碼如下:
還有其他幾種方法,思想類似,一塊放出來了
方法2
class Solution { public:int maxSubArray(vector<int>& nums) {int max = INT_MIN, sum = 0;for (int i = 0; i < nums.size(); ++i){sum += nums[i];if(max < sum)max = sum;if(sum < 0)sum = 0;}return max;} };方法3
class Solution { public:int maxSubArray(vector<int>& nums) {int res = nums[0];for (int i = 1; i < nums.size(); ++i) {nums[i] += max(0, nums[i-1]);res = max(nums[i], res);}return res;} };方法4
class Solution { public:int maxSubArray(vector<int>& nums) {int pre = 0, ans = nums[0];for (int i = 0; i < nums.size(); ++i) {pre = max(pre + nums[i], nums[i]);ans = max(ans, pre);}return ans;} };哪個好理解看哪個
最小路徑和
給定一個包含非負整數的 m x n 網格 grid ,請找出一條從左上角到右下角的路徑,使得路徑上的數字總和為最小。
說明:每次只能向下或者向右移動一步。
示例 1:
輸入:grid = [[1,3,1],[1,5,1],[4,2,1]]
輸出:7
解釋:因為路徑 1→3→1→1→1 的總和最小。
示例 2:
輸入:grid = [[1,2,3],[4,5,6]]
輸出:12
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 200
0 <= grid[i][j]<= 100
這道題同樣是求最值問題,思路很簡單,
1,dp[i][j]為到第i行j列的格子的最小和;
2,dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j];要不上一行最小,要不上一列最小,最小值加上現在格子的值;
3,dp[0][0] = grid[0][0],且當行為0時(dp[0][i])只能朝右走,列為0時(dp[i][0])只能朝左走;
代碼如下:
零錢兌換
給你一個整數數組 coins ,表示不同面額的硬幣;以及一個整數 amount ,表示總金額。
計算并返回可以湊成總金額所需的 最少的硬幣個數 。如果沒有任何一種硬幣組合能組成總金額,返回 -1 。
你可以認為每種硬幣的數量是無限的。
示例 1:
輸入:coins = [1, 2, 5], amount = 11
輸出:3
解釋:11 = 5 + 5 + 1
示例 2:
輸入:coins = [2], amount = 3
輸出:-1
示例 3:
輸入:coins = [1], amount = 0
輸出:0
示例 4:
輸入:coins = [1], amount = 1
輸出:1
示例 5:
輸入:coins = [1], amount = 2
輸出:2
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 231 - 1
0 <= amount <=104
這道題是騰訊的一道面試題,也是完全背包問題,非常經典,同樣是求最值,思路如下:
1,dp[i]是金額為i的情況下的最少硬幣數;即填滿容量為i的背包最少需要最少硬幣數;
2,dp[i] = min(dp[i - coins[j]] + 1, dp[i]);當前填滿容量i最少需要的硬幣 = min( 之前填滿容量i最少需要的硬幣, 填滿容量 i - coin 需要的硬幣 + 1個當前硬幣)
3,dp[0] = 0;當dp[i]為INT_MAX說明無法組合,為-1;
代碼如下:
買賣股票的最佳時機 II
給定一個數組 prices ,其中 prices[i] 是一支給定股票第 i 天的價格。
設計一個算法來計算你所能獲取的最大利潤。你可以盡可能地完成更多的交易(多次買賣一支股票)。
注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。
示例 1:
輸入: prices = [7,1,5,3,6,4]
輸出: 7
解釋: 在第 2 天(股票價格 = 1)的時候買入,在第 3 天(股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5-1 = 4 。
隨后,在第 4 天(股票價格 = 3)的時候買入,在第 5 天(股票價格 = 6)的時候賣出, 這筆交易所能獲得利潤 = 6-3 = 3 。
示例 2:
輸入: prices = [1,2,3,4,5]
輸出: 4
解釋: 在第 1 天(股票價格 = 1)的時候買入,在第 5 天 (股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接連購買股票,之后再將它們賣出。因為這樣屬于同時參與了多筆交易,你必須在再次購買前出售掉之前的股票。
示例 3:
輸入: prices = [7,6,4,3,1]
輸出: 0
解釋: 在這種情況下, 沒有交易完成, 所以最大利潤為 0。
提示:
1 <= prices.length <= 3 * 104
0 <= prices[i] <= 104
這道題是要求最大利潤,同樣的最值問題,這道題我們卻可以用二維數組來解決,這也是我開始沒有想到的;
1,dp[i][1]代表第i天買入股票的利潤,dp[i][0]代表第i天賣掉股票的利潤,所以這里的二維數組第二維只是用來判斷買入和賣出的;
2,第i天買入: max(若i-1天沒買,則是i天買的,那么就減去花的第i天的錢;若第i-1天已經買了,則不用管)找這兩種情況的最大值,即最大利益,dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
第i天賣出:max(若i-1天沒賣,則是i天賣的,那么就加上收入的第i天的錢;若第i-1天已經賣了,則不用管)找這兩種情況的最大值,即最大利益,dp[i][0] = max(dp[i - 1][1] + prices[i], dp[i - 1][0]);
3,如果開始不買,則dp[0][0] = 0;
如果開始買入,則dp[0][1] = -prices[0];
代碼如下:
代碼當然簡單易懂,但是這個第二維的0,1我是沒有想到;通過這個代碼,我們可以發現每次需要的數據只是前一天買入或者賣出的收入,所以我們可以對代碼進行一個優化,使其空間復雜度從O(n)達到O(1);
只需要設preOut為前一天賣出的(dp[i-1][0]),preGet為前一天買入的(dp[i-1][1])即可
代碼如下:(C++看膩了來個java版)
所以說,要想真正有優化版本,還是需要列出轉移方程,從而發現規律進行優化;
編輯距離
給你兩個單詞 word1 和 word2,請你計算出將 word1 轉換成 word2 所使用的最少操作數 。
你可以對一個單詞進行如下三種操作:
插入一個字符
刪除一個字符
替換一個字符
示例 1:
輸入:word1 = “horse”, word2 = “ros”
輸出:3
解釋:
horse -> rorse (將 ‘h’ 替換為 ‘r’)
rorse -> rose (刪除 ‘r’)
rose -> ros (刪除 ‘e’)
示例 2:
輸入:word1 = “intention”, word2 = “execution”
輸出:5
解釋:
intention -> inention (刪除 ‘t’)
inention -> enention (將 ‘i’ 替換為 ‘e’)
enention -> exention (將 ‘n’ 替換為 ‘x’)
exention -> exection (將 ‘n’ 替換為 ‘c’)
exection -> execution (插入 ‘u’)
提示:
0 <= word1.length,
word2.length <= 500
word1 和 word2 由小寫英文字母組成
這也是一道求最值問題,但是題目含有字符串,大部分字符串題目都可以用動態規劃,且一般是二維數組,分析如下:
1,dp[i] [j]是,當字符串 word1 的長度為 i,字符串 word2 的長度為 j 時,將 word1 轉化為 word2 所使用的最少操作次數為 dp[i] [j]。
2,dp[i] [j] = dp[i-1] [j-1],此時兩個字符串相同
dp[i] [j] = min(dp[i-1] [j-1],dp[i] [j-1],dp[i-1] [j]) + 1
這個是分了三種情況:
2.1,dp[i-1] [j-1]是替換字符時進行的操作
2.2,dp[i] [j-1]是word1插入時的操作
2.3,dp[i-1] [j]是word1刪除時的操作
3,初始值是計算出所有的 dp[0] [0….n] 和所有的 dp[0….m] [0],因為i和j為0時轉移方程便無法使用;
代碼如下:
爬樓梯
假設你正在爬樓梯。需要 n 階你才能到達樓頂。
每次你可以爬 1 或 2 個臺階。你有多少種不同的方法可以爬到樓頂呢?
注意:給定 n 是一個正整數。
示例 1:
輸入: 2
輸出: 2
解釋: 有兩種方法可以爬到樓頂。
1 階 + 1 階
2 階
示例 2:
輸入: 3
輸出: 3
解釋: 有三種方法可以爬到樓頂。
1 階 + 1 階 + 1 階
1 階 + 2 階
2 階 + 1 階
這道題就是一道計數類的動態規劃問題,我們通過審題可以發現,這個題就是一個斐波那契數列,那就沒有什么可說的的了,一樣的代碼,再來一遍:
class Solution { public:int climbStairs(int n) {int dp[n+1];dp[0] = 1;dp[1] = 1;for (int i = 2; i <= n; ++i) dp[i] = dp[i - 1] + dp[i - 2];//這里dp[i]是爬到第i階有幾種方法return dp[n];} };不同路徑
一個機器人位于一個 m x n 網格的左上角 (起始點在下圖中標記為 “Start” )。
機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記為 “Finish” )。
問總共有多少條不同的路徑?
示例 1:
輸入:m = 3, n = 7
輸出:28
示例 2:
輸入:m = 3, n = 2
輸出:3
解釋:
從左上角開始,總共有 3 條路徑可以到達右下角。
向右 -> 向下 -> 向下
向下 -> 向下 -> 向右
向下 -> 向右 -> 向下
示例 3:
輸入:m = 7, n = 3
輸出:28
示例 4:
輸入:m = 3, n = 3
輸出:6
提示: 1 <= m, n <= 100
題目數據保證答案小于等于 2 * 109
這道題也是一道計數類的題,讓求多少種不同的路徑,分析如下:
1,dp[i][j]是到第i行第j列的格子所有的路徑數;
2,dp[i][j] = dp[i - 1][j] + dp[i][j - 1];我們只能朝下或者朝右走,所以dp[i][j]格子的前一個格子只會是dp[i - 1][j]或者dp[i][j - 1],它倆路徑數之和就是當前格子路徑數;
3,當i為0時或者j為0時,轉移方程不成立,所以我們單獨分析,當i為0時即為第一行,能走到右下角最后一個格子的路徑只有沿邊緣的一條,即為dp[0][j] = 1,同理dp[i][0] = 1;
代碼如下:
不同路徑 II
一個機器人位于一個 m x n 網格的左上角 (起始點在下圖中標記為“Start” )。
機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記為“Finish”)。
現在考慮網格中有障礙物。那么從左上角到右下角將會有多少條不同的路徑?
網格中的障礙物和空位置分別用 1 和 0 來表示。
示例 1:
輸入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
輸出:2
解釋:
3x3 網格的正中間有一個障礙物。
從左上角到右下角一共有 2 條不同的路徑:
向右 -> 向右 -> 向下 -> 向下
向下 -> 向下 -> 向右 -> 向右
示例 2:
輸入:obstacleGrid = [[0,1],[0,0]]
輸出:1
提示:
m == obstacleGrid.length
n == obstacleGrid[i].length
1 <= m, n <=100
obstacleGrid[i][j] 為 0 或 1
這道題相對上一道題多了障礙物,但是都是一樣的思路,只需要多加上判斷障礙物的條件就可以了,直接上代碼:
class Solution { public:int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {int m = obstacleGrid.size(), n = obstacleGrid[0].size();vector<vector<int>> dp(m, vector<int> (n));int i, j;for (i = 0; i < m && !obstacleGrid[i][0]; ++i)dp[i][0] = 1;for (j = 0; j < n && !obstacleGrid[0][j]; ++j)dp[0][j] = 1;for (i = 1; i < m; ++i) {for (j = 1; j < n; ++j) {if(!obstacleGrid[i][j])//若當前位置沒有障礙物dp[i][j] = dp[i-1][j] + dp[i][j-1];}}return dp[m-1][n-1];} };跳躍游戲
給定一個非負整數數組 nums ,你最初位于數組的 第一個下標 。
數組中的每個元素代表你在該位置可以跳躍的最大長度。
判斷你是否能夠到達最后一個下標。
示例 1:
輸入:nums = [2,3,1,1,4]
輸出:true
解釋:可以先跳 1 步,從下標 0 到達下標 1, 然后再從下標 1 跳 3 步到達最后一個下標。
示例 2:
輸入:nums = [3,2,1,0,4]
輸出:false
解釋:無論怎樣,總會到達下標為 3 的位置。但該下標的最大跳躍長度是 0 , 所以永遠不可能到達最后一個下標。
提示:
1 <= nums.length <= 3 * 104
0 <= nums[i] <= 105
這道題其實時一道經典的貪心算法題,但是動態規劃也可以解決,這道題就屬于動態規劃中的判斷類的題目;
1,dp[i]表示是否能達到第i階
2,這個有點特殊就是可以將狀態方程作為一個條件,當可以達到dp[i]的前一階且前一階可以達到dp[i]時,讓dp[i]為true;
3,dp[0] = true,其他情況開始先賦一個初始值false;
代碼如下:
這就是幾道動態規劃的小題,當然動態規劃依然有好多不同的類型,只要多做題總結,就能發現其中的套路了
總結
以上是生活随笔為你收集整理的动态规划(浅层基础)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 逆序栈(递归⚠)
- 下一篇: 多态及其内部原理剖析