【蓝桥杯】最难算法没有之一· 动态规划真的这么好理解?(引入)
??
歡迎回到:遇見藍(lán)橋遇見你,不負(fù)代碼不負(fù)卿!??
目錄
一、何為動(dòng)態(tài)規(guī)劃DP
二、記憶化搜索
典例1.斐波那契數(shù)列
方法一:暴力遞歸
方法二:記憶化搜索
變形題
典例2:爬樓梯(青蛙跳臺(tái)階)
方法一:暴力遞歸?
方法二:記憶化搜索
變形題?
典例3.第N個(gè)泰波那契數(shù)
典例4.Function
三、遞推
1.遞歸
2.遞推
典例5.骨牌問題
典例6.楊輝三角
典例7.數(shù)字三角形
四、藍(lán)橋結(jié)語:遇見藍(lán)橋遇見你,不負(fù)代碼不負(fù)卿。
推薦老鐵們兩個(gè)學(xué)習(xí)網(wǎng)站:
面試?yán)?amp;算法學(xué)習(xí):牛客網(wǎng)
風(fēng)趣幽默的學(xué)人工智能:人工智能學(xué)習(xí)
【前言】
在學(xué)習(xí)動(dòng)態(tài)規(guī)劃之前,我們必須要先掌握記憶化搜索和遞推,這兩塊東西搞好了之后,面對動(dòng)態(tài)規(guī)劃那就容易多啦!好,接下來向鐵汁們詳細(xì)介紹這兩塊內(nèi)容,走著。
??
哈哈,昨天在群里發(fā)現(xiàn)這張表清包,覺得很好玩,特意放到這里皮一下,嘿嘿,這下真的要開始咯。
一、何為動(dòng)態(tài)規(guī)劃DP
動(dòng)態(tài)規(guī)劃(英語:Dynamic programming,簡稱 DP),通過把原問題分解為相對簡單的子問題的方式求解復(fù)雜問題的方法。 (是不是很像前面講解過的一種算法——分治,其實(shí)可以認(rèn)為動(dòng)態(tài)規(guī)劃就是特殊的分治)
動(dòng)態(tài)規(guī)劃常常適用于有重疊子問題和最優(yōu)子結(jié)構(gòu)性質(zhì)的問題,并且記錄所有子問題的結(jié)果,因此動(dòng)態(tài)規(guī)劃方法所耗時(shí)間往往遠(yuǎn)少于暴力遞歸解法。
使用動(dòng)態(tài)規(guī)劃解決的問題有個(gè)明顯的特點(diǎn),一旦一個(gè)子問題的求解得到結(jié)果,以后的計(jì)算過程就不會(huì)修改它,這樣的特點(diǎn)叫做無后效性,求解問題的過程形成了一張有向無環(huán)圖。動(dòng)態(tài)規(guī)劃只解決每個(gè)子問題一次,具有天然剪枝的功能,從而減少計(jì)算量。
動(dòng)態(tài)規(guī)劃有自底向上和自頂向下兩種解決問題的方式。自頂向下即記憶化搜索,自底向上就是遞推。
好,那么本文主要講解的記憶化搜索和遞推也就浮出水面了,下面詳細(xì)介紹(請放心,后面會(huì)詳細(xì)講解動(dòng)態(tài)規(guī)劃的,這部分既重要又很難,所以需要慢慢去理解。)
二、記憶化搜索
記憶化搜索的本質(zhì):動(dòng)態(tài)規(guī)劃。
記憶化搜索是動(dòng)態(tài)規(guī)劃的一種實(shí)現(xiàn)方式,記憶化搜索是用搜索的方式實(shí)現(xiàn)了動(dòng)態(tài)規(guī)劃,因此記憶化搜索就是動(dòng)態(tài)規(guī)劃。
提問:
何為記憶化搜索?
回答:
顧名思義,記憶化搜索肯定也就和“搜索”脫不了關(guān)系,?前面講解的遞歸、DFS和BFS想必大家都已經(jīng)掌握的差不多了,它們有個(gè)最大的弊病就是:低效!原因在于沒有很好地處理重疊子問題。那么對于記憶化搜索呢,它雖然采用搜索的形式,但是它還有動(dòng)態(tài)規(guī)劃里面遞推的思想,巧就巧在它將這兩種方法很好的綜合在了一起,簡單又實(shí)用。
記憶化搜索,也叫記憶化遞歸,其實(shí)就是拆分子問題的時(shí)候,發(fā)現(xiàn)有很多重復(fù)子問題,然后再求解它們以后記錄下來。以后再遇到要求解同樣規(guī)模的子問題的時(shí)候,直接讀取答案。
其實(shí)可以淺顯的認(rèn)為記憶化搜索就是簡單DP,因?yàn)閷?shí)在是太像了。?
下面詳細(xì)講解四道例題,讓大家深入理解上面的概念:
典例1.斐波那契數(shù)列
原題鏈接:力扣
題目描述:
示例1:
輸入:2 輸出:1 解釋:F(2) = F(1) + F(0) = 1 + 0 = 1示例2:
輸入:3 輸出:2 解釋:F(3) = F(2) + F(1) = 1 + 1 = 2方法一:暴力遞歸
代碼執(zhí)行:
class Solution { public:int fib(int n){//方法一:暴力遞歸//找邊界if(n == 0){return 0;}if(n == 1){return 1;}return fib(n - 1) + fib(n - 2);} };不用多說,學(xué)校老師講遞歸的時(shí)候似乎都是拿這個(gè)舉例。我們也知道這樣寫代碼雖然簡潔易懂,但是十分低效,低效在哪里?假設(shè) n = 20,請畫出遞歸樹:
很顯然,進(jìn)行了大量的重復(fù)計(jì)算,雖然本題是一個(gè)很好的用來講解遞歸的例子,但是嘞,本題在實(shí)際運(yùn)用當(dāng)中用遞歸來做那就太不明智啦,所以,這里就需要引入一個(gè)帶有「備忘錄」的遞歸,也就是前面所提到的記憶化搜索,下面看看具體怎么操作:
方法二:記憶化搜索
即然低效的原因是重復(fù)計(jì)算,那么我們可以造一個(gè)「備忘錄」,每次算出某個(gè)子問題的答案后別急著返回,先記到「備忘錄」里再返回;每次遇到一個(gè)子問題先去「備忘錄」里查一查,如果發(fā)現(xiàn)之前已經(jīng)解決過這個(gè)問題了,直接把答案拿出來用,不要再耗時(shí)去計(jì)算了。
一般使用一個(gè)數(shù)組充當(dāng)這個(gè)「備忘錄」,當(dāng)然你也可以使用哈希表(字典),思想都是一樣的。
這樣去處理的話就無需進(jìn)行重復(fù)計(jì)算了,相比前面的暴力遞歸就顯得高效明智得多,而且很容易理解,不信你看:
代碼執(zhí)行:
class Solution { public:int fib(int n){//方法三:記憶化搜索(簡單DP)//找邊界if(n == 0){return 0;}if(n == 1){return 1;}//需要定義一個(gè)大小為(n+1)的整形數(shù)組,并且初始化為0//之所以是n+1,是因?yàn)橐褂玫絥這個(gè)下標(biāo)vector<int> dp(n+1, 0);dp[0] = 0;dp[1] = 1;for(int i = 2; i < n+1; i++){dp[i] = dp[i - 1] + dp[i - 2];}return dp[n];} };當(dāng)然也有這樣的變形,多了一個(gè)要求,很簡單的,大家看一下:
變形題
代碼執(zhí)行:
//找邊界if(n == 0)return 0;if(n == 1)return 1;vector<int> dp(n + 1, 0);//開辟一個(gè)大小為n+1的整形數(shù)組(因?yàn)樾枰褂孟聵?biāo)n),并且初始化為0dp[0] = 0;dp[1] = 1;for(int i = 2; i < n+1; i++){dp[i] = dp[i - 1] + dp[i - 2];dp[i] = dp[i] % 1000000007;}return dp[n];}實(shí)際上,這種解法和迭代的動(dòng)態(tài)規(guī)劃非常相似,只不過這種方法叫做「自頂向下」,動(dòng)態(tài)規(guī)劃叫做「自底向上」。
啥叫「自頂向下」?
注意我們剛才畫的遞歸樹(或者說圖),是從上向下延伸,都是從一個(gè)規(guī)模較大的原問題比如說 f(20),向下逐漸分解,直至?f(1) 和 f(2) 這兩個(gè) base case,然后逐層返回答案,這就叫「自頂向下」。
啥叫「自底向上」?
反過來,我們直接從最底下,最簡單,問題規(guī)模最小的 f(1) 和 f(2) 開始往上推,直到推到我們想要的答案 f(20),這就是動(dòng)態(tài)規(guī)劃的思路,這也是為什么動(dòng)態(tài)規(guī)劃一般都脫離了遞歸,而是由循環(huán)迭代完成計(jì)算。
典例2:爬樓梯(青蛙跳臺(tái)階)
原題鏈接:力扣
題目描述:
示例1:
輸入: 2 輸出: 2 解釋: 有兩種方法可以爬到樓頂。 1. 1 階 + 1 階 2. 2 階示例2:
輸入: 3 輸出: 3 解釋: 有三種方法可以爬到樓頂。 1. 1 階 + 1 階 + 1 階 2. 1 階 + 2 階 3. 2 階 + 1 階首先分析一下本題:
注意:這個(gè)題目問的是什么?
問的不是能爬多少次,而是有多少種方法能到最后一個(gè)臺(tái)階。
問題分析:
當(dāng)n > 2時(shí),第一次爬就有兩種不同的選擇:一是第一次只爬一級(jí),此時(shí)爬法數(shù)目等于后面剩下的(n - 1)級(jí)臺(tái)階的爬法數(shù)目,即為f(n - 1); 還有一種選擇是第一次爬兩級(jí),此時(shí)爬法數(shù)目等于后面剩下的(n - 2)級(jí)臺(tái)階的爬法數(shù)目,即為f(n - 2).
所以有:f(n) = f(n - 1) + f(n - 2)
當(dāng)n == 1時(shí),有1種爬法;
當(dāng)n == 2時(shí),有2種爬法;
當(dāng)n == 3時(shí),有3種爬法;
當(dāng)n == 4時(shí),有5種爬法。
是呀,這題跟斐波那契數(shù)列基本上一樣,不過這道題目需要思考一下,沒有斐波那契數(shù)列這么明顯。但是需要注意的是,遞歸邊界還是有所不同的哦!
方法一:暴力遞歸?
代碼執(zhí)行:
class Solution { public:int climbStairs(int n) {//方法一:暴力遞歸//找邊界if(n == 1){return 1;}if(n == 2){return 2;}return climbStairs(n - 1) + climbStairs(n - 2);} };方法二:記憶化搜索
代碼執(zhí)行:
class Solution { public:int climbStairs(int n) {//方法二:記憶化搜索(簡單DP)//找邊界if(n == 1){return 1;}if(n == 2){return 2;}//定義一個(gè)大小為n+1的整型數(shù)組,并且初始化為0vector<int> dp(n+1, 0);dp[1] = 1;dp[2] = 2;for(int i = 3; i < n+1; i++){dp[i] = dp[i - 1] + dp[i - 2];}return dp[n];} };變形題?
其實(shí)就是多了一個(gè)要求...
代碼執(zhí)行:
class Solution { public:int numWays(int n) {//找邊界if(n == 0 || n == 1)return 1;if(n == 2)return 2;vector<int> dp(n+1, 0);dp[1] = 1;dp[2] = 2;for(int i = 3; i < n+1; i++){dp[i] = dp[i - 1] + dp[i - 2];dp[i] %= 1000000007;}return dp[n];} };典例3.第N個(gè)泰波那契數(shù)
原題鏈接:力扣
題目描述:?
示例1:
輸入:n = 4 輸出:4 解釋: T_3 = 0 + 1 + 1 = 2 T_4 = 1 + 1 + 2 = 4示例2:
輸入:n = 25 輸出:1389537代碼執(zhí)行:
class Solution { public:int tribonacci(int n) {//找邊界if(n == 0)return 0;if(n == 1 || n == 2)return 1;//定義一個(gè)大小為n+1的整形數(shù)組,并將其初始為0vector<int> dp(n+1, 0);dp[0] = 0;dp[1] = 1;dp[2] = 1;for(int i = 3; i < n+1;i++){dp[i] = dp[i - 3] + dp[i - 2] + dp[i - 1];//遞推公式}return dp[n];}};上面這三道題是非常簡單的,理解了上面的題目,下面這道題肯定就會(huì)非常容易了。
典例4.Function
原題鏈接:?Function - 洛谷
題目描述:
輸入格式:
輸入有若干行。并以?1,?1,?1結(jié)束。
保證輸入的數(shù)在[?9223372036854775808,9223372036854775807]之間,并且是整數(shù)。
輸出格式
輸出若干行,每一行格式:w(a, b, c) = ans
思路:
本題重在理解題意,題目不難,但是要把題目多讀幾遍。?
代碼執(zhí)行:
#include <stdio.h>#define LL long longLL dp[25][25][25];LL w(LL a, LL b, LL c) {//兩個(gè)特殊情況判斷if(a <= 0 || b <= 0 || c <= 0) return 1;if(a > 20 || b > 20 || c > 20) return w(20, 20, 20);if(a < b && b < c){if(dp[a][b][c-1] == 0){dp[a][b][c-1] = w(a, b, c-1);}if(dp[a][b-1][c-1] == 0){dp[a][b-1][c-1] = w(a, b-1 ,c-1);}if(dp[a][b-1][c] == 0){dp[a][b-1][c] = w(a, b-1, c);}dp[a][b][c] = dp[a][b][c-1] + dp[a][b-1][c-1] - dp[a][b-1][c];}else{if(dp[a-1][b][c] == 0){dp[a-1][b][c] = w(a-1, b, c);}if(dp[a-1][b-1][c] == 0){dp[a-1][b-1][c] = w(a-1, b-1 ,c);}if(dp[a-1][b][c-1] == 0){dp[a-1][b][c-1] = w(a-1, b, c-1);}if(dp[a-1][b-1][c-1] == 0){dp[a-1][b-1][c-1] = w(a-1, b-1, c-1);}dp[a][b][c] = dp[a-1][b][c] + dp[a-1][b][c-1] + dp[a-1][b-1][c] - dp[a-1][b-1][c-1];}return dp[a][b][c]; }int main() {LL a, b, c;while(scanf("%lld%lld%lld", &a, &b, &c) != EOF){if(a == -1 && b == -1 && c == -1) return 0;//當(dāng)輸入的值為-1 -1 -1時(shí)直接結(jié)束循環(huán)printf("w(%lld, %lld, %lld) = ", a, b, c);printf("%lld\n", w(a, b, c));} }三、遞推
看到“遞推”,大家肯定能聯(lián)想到“遞歸”,好嘞,接下來向大家詳細(xì)講解遞推和遞歸有哪些區(qū)別。
1.遞歸
如果大家對于遞歸部分掌握的不是很好的話可以看看我之前的博文哦,里面的基礎(chǔ)概念講解的很詳細(xì),而且還配備大量練習(xí)題。
藍(lán)橋杯算法競賽系列第二章——深入理解重難點(diǎn)之遞歸(上)_安然無虞的博客-CSDN博客一、遞歸是什么https://blog.csdn.net/weixin_57544072/article/details/120836167
遞歸是大問題轉(zhuǎn)化為小問題,不斷調(diào)用自身或不斷被間接調(diào)用的一類算法。
1.遞歸算法的關(guān)鍵是要找出大問題和小問題的聯(lián)系----即找重復(fù),進(jìn)而使大問題的規(guī)模不斷減小,直至可以被直接解決。
2.遞歸算法的另一個(gè)關(guān)鍵點(diǎn)是終止條件----即找邊界,這個(gè)是十分重要的。
有時(shí),遞歸算法的效率會(huì)很低,這時(shí)候就可以用上面所說的記憶化搜索,即建立一個(gè)數(shù)組,用來記錄每次遞歸得到的答案,這樣如果后面要繼續(xù)使用這個(gè)值的時(shí)候,就不用再次計(jì)算了,也就避免了重復(fù)計(jì)算問題。
2.遞推
部分定義和題解參考博客鏈接:
遞推算法-五種典型的遞推關(guān)系_lybc2019的博客-CSDN博客_遞推法
遞推和遞歸非常相似。
遞推是把問題劃分為若干個(gè)步驟,每個(gè)步驟之間,或者是這個(gè)步驟與之前的幾個(gè)步驟之間有一定的數(shù)量關(guān)系,可以用前幾項(xiàng)的值表示出這一項(xiàng)的值,這樣就可以把一個(gè)復(fù)雜的問題變成很多小的問題。
遞推算法注意的是設(shè)置什么樣的遞推狀態(tài),因?yàn)橐粋€(gè)好的遞推狀態(tài)可以讓問題很簡單。最難的是想出遞推公式,一般遞推公式是從后面向前想,倒推回去。
典例5.骨牌問題
題目描述:
有 2*n的一個(gè)長方形方格,用一個(gè)1*2的骨牌鋪滿方格。請編寫一個(gè)程序,試對給出的任意一個(gè)n(n>0), 輸出鋪法總數(shù)。?
思路:
其實(shí)這道題目很簡單的,找到遞推公式即可,跟上面的爬樓梯問題很相似,這里就詳細(xì)分析一下:
n = 1 時(shí),只有一種鋪法
n = 2 時(shí),如下圖,有全部豎著鋪和橫著鋪兩種
n = 3 時(shí),骨牌可以全部豎著鋪,也可以認(rèn)為在方格中已經(jīng)有一個(gè)豎鋪的骨牌,則需要在方格中排列兩個(gè)橫排骨牌(無重復(fù)方法),若已經(jīng)在方格中排列兩個(gè)橫排骨牌,則必須在方格中排列一個(gè)豎排骨牌。如下圖,再無其他排列方法,因此鋪法總數(shù)表示為三種。
通過上面的分析,不難看出規(guī)律:f(3) = f(1) + f(2)
所以可以的得到遞推關(guān)系:f(n) = f(n - 1) + f(n - 2)
??
代碼執(zhí)行:
class Solution { public:int brand(int n) {//找邊界if(n == 1){return 1;}if(n == 2){return 2;}//定義一個(gè)大小為n+1的整型數(shù)組,并且初始化為0vector<int> dp(n+1, 0);dp[1] = 1;dp[2] = 2;for(int i = 3; i < n+1; i++){dp[i] = dp[i - 1] + dp[i - 2];//找出遞推關(guān)系}return dp[n];} };典例6.楊輝三角
原題鏈接:力扣
題目描述:?
示例1:
輸入: numRows = 5 輸出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]示例2:
輸入: numRows = 1 輸出: [[1]]思路:
本題比較簡單,很容易就能看出遞推關(guān)系,從第三行第二列開始,每個(gè)數(shù)是它左上方和右上方的數(shù)的和。?
代碼執(zhí)行:
class Solution { public:vector<vector<int>> generate(int numRows) {vector<vector<int> >ret(numRows);//定義一個(gè)二維數(shù)組用于存放結(jié)果//首先將第一列和最后一列元素全部賦值為1for(int i = 0; i < numRows; i++){ret[i].resize(i+1);//resize()的作用就是為一維數(shù)組分配空間ret[i][0] = ret[i][i] = 1;//從第三行第二列開始有遞推關(guān)系:ret[i][j] = ret[i+1][j]+ret[i+1][j+1];for(int j = 1; j < i; j++){ret[i][j] = ret[i-1][j] + ret[i-1][j-1];}}return ret;} };代碼中需要注意的是:vector 中的resize() 是重新分配空間的。
典例7.數(shù)字三角形
原題鏈接:[USACO1.5][IOI1994]數(shù)字三角形 Number Triangles - 洛谷
題目描述:
輸入格式:
第一個(gè)行一個(gè)正整數(shù) n ,表示行的數(shù)目。
后面每行為這個(gè)數(shù)字金字塔特定行包含的整數(shù)。
輸出格式:
單獨(dú)的一行,包含那個(gè)可能得到的最大的和。
數(shù)據(jù)范圍:
1 ≤ n ≤ 1000,三角形數(shù)字值在 [0,100] 范圍內(nèi)。?
示例:
輸入:
5 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5輸出:
30思路:
本題采用倒推的方式:
假設(shè)func[i][j]表示的是從 i, j 到最后一層的最大路徑之和
當(dāng)從頂層沿某條路徑走到第i層向第i+1層前進(jìn)時(shí),我們的選擇是沿其下兩條可行路徑中最大數(shù)字的方向前進(jìn),所以找出遞推關(guān)系:func[i][j] += max(func[i+1][j],func[i+1][j+1]);
注意:func[i][j]表示當(dāng)前數(shù)字的值,func[i+1][j]和func[i+1][j+1]分別表示從i+1,j、i+1,j+1到最后一層的最大路徑之和;
最終func[0][0]就是所求
代碼執(zhí)行:
#include<stdio.h> #include<algorithm> using namespace std;int func[1005][1005] = {0};int main() {int n = 0;scanf("%d", &n);int i = 0;int j = 0;for(i = 0; i < n; i++){for(j = 0; j <= i; j++){scanf("%d", &func[i][j]);}}//假設(shè)func[i][j]表示的是從i, j到最后一層的最大路徑之和//找出遞推關(guān)系:func[i][j]+=max(func[i+1][j],func[i+1][j+1]);//func[i][j]表示當(dāng)前數(shù)字的值,func[i+1][j]和func[i+1][j+1]分別表示從i+1,j、i+1,j+1到最后一層的最大路徑之和//最終func[0][0]就是所求for(i = n - 2; i >= 0; i--){for(j = 0; j <= i; j++){func[i][j] += max(func[i+1][j], func[i+1][j+1]);}}printf("%d\n", func[0][0]);return 0; }??
四、藍(lán)橋結(jié)語:遇見藍(lán)橋遇見你,不負(fù)代碼不負(fù)卿。
又到了說再見的時(shí)候了,上面是動(dòng)規(guī)引入部分,看起來不難,但其實(shí)動(dòng)態(tài)規(guī)劃算法難起來是真變態(tài),后面會(huì)慢慢深入,不過藍(lán)橋幾乎不會(huì)考到特別難的動(dòng)規(guī),正常都是簡單DP,所以后面會(huì)有大量的練習(xí)側(cè)重于簡單DP。OK,今天到這就結(jié)束咯。886
求求兄弟姐妹們 · 賞俺個(gè)三連吧~
總結(jié)
以上是生活随笔為你收集整理的【蓝桥杯】最难算法没有之一· 动态规划真的这么好理解?(引入)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从理论到实战!视频流车辆计数和目标跟踪
- 下一篇: 安徽省计算机vfp,安徽省计算机二级VF