動態規劃是由遞迴一步步優化出來的

遞迴–>記憶化遞迴–>動態規劃

動態規劃與其說是一個演算法,不如說是一種方法論。該方法論主要致力於將合適的問題拆分成三個子目標——擊破:

1。建立狀態轉移方程

2。快取並複用以往結果

3。按順序從小往大算

直接看題吧

比如:

一、斐波那契額數列(劍指offer10-I)

寫一個函式,輸入 n ,求斐波那契(Fibonacci)數列的第 n 項。斐波那契數列的定義如下:

F(0) = 0, F(1) = 1

F(N) = F(N - 1) + F(N - 2), 其中 N > 1。

斐波那契數列由 0 和 1 開始,之後的斐波那契數就是由之前的兩數相加而得出。

答案需要取模 1e9+7(1000000007),如計算初始結果為:1000000008,請返回 1。

示例 1:

輸入:n = 2

輸出:1

示例 2:

輸入:n = 5

輸出:5

1.最常用的解法,遞迴:

class Solution {

public:

int fib(int n) {

//1。結束條件

if(n<2) return n;

if(n>(1e9+7))return 1;

//2。找等價函式

return (fib(n-1)+fib(n-2));

}

};

結果超時,因為一旦要求的n偏大,就會出現大量的重複計算:

動態規劃--5道題入門

2.遞迴+記憶化

到達32的時候仍然會超時。按理說應該可以透過1

class Solution {

public:

int fib(int n) {

if(n==0) return 0;

if(n==1) return 1;

vectormem(n+1,-1);

//1。結束條件

//mem[0]=0;

//mem[1]=1;

//2。找等價函式

if(mem[n]==-1) mem[n]=(fib(n-1)+fib(n-2))%1000000007;

return mem[n] ;

}

};

修改後成功了!!!!!!!!!!!!!!!!!!!!!!!!!!!!

class Solution {

public:

int fib(int n) {

mem=vector(n+1,-1);

return fibs(n);

}

private:

vectormem;

int fibs(int n) {

if(n<2) return n;

if(mem[n]==-1) mem[n]=(fibs(n-1)+fibs(n-2))%1000000007;

return mem[n] ;

}

};

**總結:

遞迴到記憶化遞迴有三步:

1.將遞迴函式轉換為輔助函式,新建全域性容器,並在主函數里面初始化大小。

2.在輔助函式中新增if條件語句,考慮當陣列容器為-1時的情況

3.將遞迴等價關係取得的值記憶到陣列容器中去,最後返回容器。

分開的原因暫時未知!

3.動態規劃

自下而上的解決問題

class Solution {

public:

int fib(int n) {

if(n==0) return 0;

vectormem(n+1,-1);

//1。結束條件

mem[0]=0;

mem[1]=1;

//注意for迴圈裡面時三個mem

for (int i = 2; i <= n; i++){

mem[i]=(mem[i-1]+mem[i-2])%1000000007;

}

return mem[n] ;

}

};

記憶化遞迴到動態規劃總結:

1.又合併只有一個函式,對陣列容器原地進行建立並初始化。

2.把遞迴的結束條件轉換為陣列容器的值。

3.利用遍歷和陣列容器中已存在的值,找到陣列容器中的等價關係,最後返回所需要的容器值。

4.最佳化空間之後

class Solution {

public int fib(int n) {

if(n==0)return 0;

if(n==1)return 1;

int a=0,b=1;

for(int i=2;i<=n;i++){

int x = (a+b)%1000000007;

a=b;

b=x;

}

return b;

}

}

二、青蛙跳臺問題(劍指offer10-II)

1.遞迴:

class Solution {

public:

int numWays(int n) {

if(n==0)return 1;

if(n==1)return 1;

return numWays(n-1)+numWays(n-2);

}

};

2.遞迴+記憶最佳化

動態規劃--5道題入門

因為重複太多,所以需要記憶。

遞迴到記憶化遞迴有三步:

1.將遞迴函式轉換為輔助函式,新建全域性容器,並在主函數里面初始化大小。

2.在輔助函式中新增if條件語句,考慮當陣列容器為-1時的情況

3.將遞迴等價關係取得的值記憶到陣列容器中去(注意等價關係的容器陣列下標不再含有n),最後返回容器。

class Solution {

public:

int numWays(int n) {

mem=vector(n+1,-1);

return tiaotai(n);

}

private:

vectormem;

int tiaotai(int n){

if(n==0)return 1;

if(n==1)return 1;

if(mem[n]==-1)

mem[n]=(tiaotai(n-1)+tiaotai(n-2))%1000000007;

return mem[n];

}

};

記憶化遞迴到動態規劃總結:

1.又合併只有一個函式,對陣列容器原地進行建立並初始化。

2.把遞迴的結束條件轉換為陣列容器的值。

3.利用遍歷和陣列容器中已存在的值,找到陣列容器中的等價關係(注意等價關係的容器陣列下標不再含有n),最後返回所需要的容器值。

3.動態規劃

直接這麼寫輸入0總是報錯

class Solution {

public:

int numWays(int n) {

vectormem(n+1,-1);

mem[0]=1;

mem[1]=1;

for(int i=2;i<=n;i++){

mem[i]=(mem[i-1]+mem[i-2])%1000000007;

}

return mem[n];

}

};

直接把1儲存到所有的陣列元素中反而正確

class Solution {

public:

int numWays(int n) {

//動態規劃4行解決

//宣告n+1大小的vector,+1是要儲存0至n共n+1個數據。

vector v(n + 1, 1);

for(int i = 2; i <= n; i++)

v[i] = (v[i - 1] + v[i - 2]) % 1000000007;//注意別忘記取餘

return v[0];//直接返回最後一個數,即最終結果

}

};

4.空間最佳化:

class Solution {

public:

int numWays(int n) {

vectormem(n+1,1);

if(n<=1)return 1;

int a=1,b=1,c=0;

for(int i=2;i<=n;i++){

c=(a+b)%1000000007;

a=b;

b=c;

}

return b;

}

};

或者這種

class Solution {

public:

int numWays(int n) {

//建立一個數組,

//陣列大小盡量大,因為它是可以取的n的最大範圍

int ans[500]={1,1};

//把每一種臺階數對應的跳法總數存到對應陣列中

for(int i=2;i<=n;i++) ans[i]=(ans[i-1]+ans[i-2])%(int(1e9+7));

//返回所取的臺階對應跳法數

return ans[n];

}

};

三、 整數拆分(Leetcode343)

給定一個正整數 n,將其拆分為至少兩個正整數的和,並使這些整數的乘積最大化。 返回你可以獲得的最大乘積。

示例 1:

輸入: 2

輸出: 1

解釋: 2 = 1 + 1, 1 × 1 = 1。

示例 2:

輸入: 10

輸出: 36

解釋: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

1.普通遞迴(超時)

class Solution {

private:

int Max(int a, int b, int c){

return max(a,max(b,c));

}

int _integerBreak(int n){

//結束條件

if(n==1)return 1;

//尋找等價關係

int res=-1;

for(int i=1; i

res=Max(res,i*(n-i), i*_integerBreak(n-i));

}

return res;

}

public:

int integerBreak(int n) {

//vector(n+1,-1);

return _integerBreak(n);

}

};

2.記憶化遞迴

class Solution {

private:

vectormem;

int Max(int a, int b, int c){

return max(a,max(b,c));

}

int _integerBreak(int n){

//結束條件

if(n==1)return 1;

//尋找等價關係

if(mem[n]!=-1)return mem[n];

int res=-1;

for(int i=1; i

res=Max(res,i*(n-i), i*_integerBreak(n-i));

}

mem[n]=res;

return res;

}

public:

int integerBreak(int n) {

assert(n>=2);

mem = vector(n+1,-1);

return _integerBreak(n);

}

};

3.動態規劃

記憶化遞迴到動態規劃總結:

1.又合併只有一個函式,對陣列容器原地進行建立並初始化。

2.把遞迴的結束條件轉換為陣列容器的值。

3.利用遍歷和陣列容器中已存在的值,找到陣列容器中的等價關係(注意等價關係的容器陣列下標不再含有n),最後返回所需要的容器值。

class Solution {

private:

int Max(int a, int b, int c){

return max(a,max(b,c));

}

public:

int integerBreak(int n) {

assert(n>=2);

vectormem(n+1,-1);

mem[1]=1;

int res=-1;

for(int i=2; i<=n; i++){

for(int j=1;j

res=Max(res,j*(i-j), j*mem[i-j]);

mem[i]=res;

}

}

return mem[n];

}

};

四、三角形求和(Leetcode120)

題意:

給定三角形,每次只能移動到下一行中的相鄰結點,求從頂點到底邊的最小路徑和。

[2],

[3,4],

[6,5,7],

[4,1,8,3]

相鄰結點:與(i, j) 點相鄰的結點為 (i + 1, j) 和 (i + 1, j + 1)。

分析:

若定義 f(i, j)f(i,j) 為 (i, j)(i,j) 點到底邊的最小路徑和

,則易知遞迴求解式為:

f(i, j) = min(f(i + 1, j), f(i + 1, j + 1)) + triangle[i][j]

由此,我們將任一點到底邊的最小路徑和,轉化成了與該點相鄰兩點到底邊的最小路徑和中的較小值,再加上該點本身的值。這樣本題的 遞迴解法(解法一) 就完成了。

1.純遞迴的錯誤方法,因為會超時:

class Solution {

private:

int dfp(vector>& triangle,int i, int j){

//結束條件

//i,j代表從哪個點一直向下的路徑之和

//下面的條件是i已經超過最底端,所以路徑和返回0

if(i==triangle。size())return 0;

//等價關係

return min(dfp(triangle,i+1,j),dfp(triangle,i+1,j+1))+triangle[i][j];

}

public:

int minimumTotal(vector>& triangle) {

int n = triangle。size();

//對輔助容器的初始化,初始化後二維陣列的空間值自動賦值為0

return dfp(triangle,0,0);

}

};

暴力搜尋會有大量的重複計算,導致 超時,因此在 解法二 中結合記憶化陣列進行最佳化。

2.遞迴加記憶化的方法:

class Solution {

//遞迴的記憶化輔助

vector>mem;

private:

int dfp(vector>& triangle,int i, int j){

//結束條件

//i,j代表從哪個點一直向下的路徑之和

//下面的條件是i已經超過最底端,所以路徑和返回0

if(i==triangle。size())return 0;

if (mem[i][j] != 0) {

return mem[i][j];

}

//等價關係

return mem[i][j]=min(dfp(triangle,i+1,j),dfp(triangle,i+1,j+1))+triangle[i][j];

}

public:

int minimumTotal(vector>& triangle) {

int n = triangle。size();

//對輔助容器的初始化,初始化後二維陣列的空間值自動賦值為0

mem=vector >(n, vector(n));

return dfp(triangle,0,0);

}

};

3.動態最佳化

定義二維 dp 陣列,將解法二中「自頂向下的遞迴」改為「自底向上的遞推」

class Solution {

public:

int minimumTotal(vector>& triangle) {

int n = triangle。size();

//對輔助容器的初始化,初始化後二維陣列的空間值自動賦值為0

vector>dp(n+1, vector(n+1));

//把每一個位置從底到該處的最小路徑之和都算出來

for (int i = n - 1; i >= 0; i——) {

for (int j = 0; j <= i; j++) {

dp[i][j] = min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle[i][j];

}

}

return dp[0][0];

}

};

4.空間最佳化

3的解法把三角形每一個位置從下到上的路徑之和都搞了出來,其實沒必要,因為我們只要求從下到最頂點的路徑之和,所以只需要用一行陣列儲存前一層的路徑之和讓後一層用就可以,這樣就更加節省空間。

class Solution {

public:

int minimumTotal(vector>& triangle) {

int n = triangle。size();

vectordp(n+1);

for (int i = n - 1; i >= 0; i——) {

for (int j = 0; j <= i; j++) {

dp[j] = min(dp[j], dp[j + 1]) + triangle[i][j];

}

}

return dp[0];

}

};

五、求左上到右下的最小路徑和(Leetcode64)

給定一個包含非負整數的 m x n 網格 grid ,請找出一條從左上角到右下角的路徑,使得路徑上的數字總和為最小。

說明:每次只能向下或者向右移動一步。

動態規劃--5道題入門

示例 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

1.路徑規劃的方法

最主要是的搞清楚往臨時容器中放的是什麼,一次性迴圈放和分開放都可以!

class Solution {

public:

int minPathSum(vector>& grid) {

int m=grid。size(),n=grid[0]。size();

if(m==0 && n==0)return 0;

//對輔助容器的初始化,初始化後二維陣列的空間值自動賦值為0

vector>dp(m, vector(n));

//int dp[m][n]; //容器或陣列兩個都可以

dp[0][0]=grid[0][0];

// 第1行 計算

//第一行初始化

//注意後面是加grid[][]!

for(int i=1; i

dp[0][i]=dp[0][i-1]+grid[0][i];

}

//第一列的初始化

for(int j=1; j

dp[j][0]=dp[j-1][0]+grid[j][0];

}

//把不是第一行第一列位置從起始處到該位置的和算出來初始化

for (int i = 1; i

for (int j = 1; j

dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) +grid[i][j];

}

}

return dp[m-1][n-1];

}

};

2.進行空間最佳化

想不到想不到

不知道這種二維變一維的空間最佳化大佬們是怎樣想到的,反正我想不到!

class Solution {

public:

int minPathSum(vector>& grid) {

int rows = grid。size();

int cols = grid[0]。size();

vector dp(cols, grid[0][0]);

for (int i = 1; i < cols; ++i)

dp[i] = grid[0][i] + dp[i - 1];

for (int i = 1; i < rows; ++i) {

dp[0] += grid[i][0];

for (int j = 1; j < cols; ++j) {

dp[j] = grid[i][j] + min(dp[j], dp[j - 1]);

}

}

return dp[cols-1];

}

};

個人在大學時習慣於根據決策推進方向可化分為向前和向後兩類。

官方影片講解針對樣例給出了由後向前推進的解析,並給出了簡化dp陣列為一維的方法。本文以一維dp陣列為解題基礎透過向前推進決策的方法給與此題解答。

本文將解決此題的方法概括為“陣列降維,逐行更新”

一。陣列降維

開闢一個長度等於原二維grid陣列列長(設為cols)的一維陣列(vector dp(cols, grid[0][0]))

動態規劃問題又可以理解為多段決策最優問題,那麼dp[i]表示在前面i-1步已做出最優決策的前提下第i步做出的決策,並且使得前i步得到的收益最大(本題中指路徑最短)。

值得提出的是,雖然題目中規定了每個點只能向下或者向右移動,但對於第一行的任意點來說,沒有上一行的點會向下移動到該位置。由此對於第一行建立以下等式

dp[i] = grid[0][i] + dp[i - 1]; (1)

以題幹給出的示例為例:遍歷第一行以後

dp陣列的值為{1, 4, 5}

二。逐行更新

設想我們從第二行開始,每一行的每一個點都可以由上邊的點向下移動經過。

對於第i行第j列的點(i > 1)逐行遍歷至該點時,有兩種情況

(1)對於每行的第一個點只有他上邊的點向下移動才可以經過該點

(2)剩下的點都可以由他上邊的點向下移動或左邊的點向右移動到達

對於第一類點,我們遍歷到新一行時,由於該點只能由其他點向下移動到達我們就可以根據前一行的dp[0]來更新該點的決策值

dp[0] += grid[i][0] (2)

對於第二類點,我們可以得到如下等式

dp[j] = grid[i][j] + min(dp[j-1],dp[j])

等式右邊dp[j]代表當前點上一行對應位置點的決策收益值

dp[j-1]代表當前位置左側點的決策收益值。

大佬詳解連結:動態規劃的最佳化

其他習題:

另外還有力扣的279題,91題,64題,63題。