什么是動態規划
-
- 在面試過程中如果是求一個問題的最優解(通常是最大值或者最小值),並且該問題能夠分解成若干個子問題,並且子問題之間好友重疊的更小子問題,就可以考慮用動態規划來解決這個問題。
- 動態規划的分類
大多數動態規划問題都可以被歸類成兩種類型:優化問題和組合問題
-
- 優化問題
優化問題就是我們常見的求一個問題最優解(最大值或者最小值)
-
- 組合問題
組合問題是希望你弄清楚做某事的數量或者某些事件發生的概率
-
- 兩種不同動態規划解決方案
- 自上而下:即從頂端不斷地分解問題,知道你看到的問題已經分解到最小並已得到解決,之后只用返回保存的答案即可
- 自下而上:你可以直接開始解決較小的子問題,從而獲得最小的解決方案。在此過程中,你需要保證在解決問題之前先解決子問題。這種方法叫做表格填充法。
- 兩種不同動態規划解決方案
- 常見的動態規划例子
裴波那契數列就是典型的組合問題,要求出做某事的數量或者概率
-
-
- 問題分析:對於題目中的青蛙爬樓梯問題,初試情況下是只有一級台階時,只有一種跳法,只有兩級台階時,有兩種跳法,當有n級台階時,設n級台階的跳法總數是f(n),如果第一步跳一級台階,則和剩下的n-1級台階的跳法是一樣的,如果第一級跳兩級台階,則和剩下的n-2級台階的跳法是一樣的,因此最終n級台階的跳法是f(n)=f(n-1)+f(n-2),即其是可以被分解為更小的子問題的,下面我們以求解f(10)為例來分析遞歸的過程
-
我們從這張圖中不難發現,在這棵樹中有很多節點都是重復的,而且重復節點會隨着n的增大而急劇增大,因此我們采用自頂向下的方式會有很低的效率,因此我們采用自下而上的方法,首先根據f(1)和f(2)計算出f(3),再根據f(2)和f(3計算出f(4),以此類推求出f(n)
實現的代碼如下
1 int jumpFloor(int number) { 2 if(number<0) 3 return 0; 4 else if(number==0||number==1||number==2) 5 return number; 6 else 7 { 8 int result=0; 9 int f1=1; 10 int f2=2; 11 for(int i=3;i<=number;++i) 12 { 13 result=f1+f2; 14 f1=f2; 15 f2=result; 16 } 17 return f2; 18 } 19 }
-
-
- 矩形覆蓋問題
-
-
-
-
- 問題分析:由於2*1的小矩形可以橫着放,也可以豎着放,當n=1時,其只有一種方式,f(1)=1,n=2時,有兩種覆蓋方式f(2)=2如圖
-
-
當要構成2*n的大矩形時,如果第一個小矩形豎着放,則其和后面n-1個小矩形的方法相等,如果第一個小矩形橫着放,則第二個小矩形也只能橫着放,即上圖右邊的方法,因此其和后面n-2個小矩形的放法相等。f(n)=f(n-1)+f(n-2),也是一個裴波那契數列。代碼如上
-
-
- 問題分析:由於是求數字的翻譯方法,因此這是一個組合問題,如果對於這種數字問題,一般將其轉換成字符串來進行求解,對於如果第一個位數是1,則其一定有兩種解法,即可以翻譯成一個數字或者兩個數字,即B[i+1]=B[i]+B[i-1],如果第一個位數是1,如果第二位數大於0,小於5,則有兩種翻譯方法,B[i+1]=B[i]+B[i-1],其他情況即只有一種翻譯方法:B[i+1]=B[i]
- 我們首先看下面一個圖
-
我們將一個字符串翻譯分解成很多子問題來進行求解
-
-
- 代碼參考
-
class Solution { public: int B[70]={1,1}; int translateNum(int num) { if(num<0) return 0; string nums=to_string(num); int numsize=nums.size(); for(int i=1;i<numsize;++i) { if(nums[i-1]=='1') B[i+1]=B[i]+B[i-1]; else if(nums[i-1]=='2') { if(nums[i]>='0'&&nums[i]<='5') B[i+1]=B[i]+B[i-1]; else B[i+1]=B[i]; } else B[i+1]=B[i]; } return B[numsize]; } };
-
- 3. 最佳觀光組合
- 題目描述
-
-
- 問題分析:這道題當然可以用暴力法進行求解,但是這樣的時間效率過低,因此考慮其他的方法。由於兩者之間的得分為A[i]+A[j]+i-j,也可以將其寫成A[i]+i+A[j]-j,當遍歷到j時A[j]-j的值是不變的,因此最大化A[i]+A[j]+i-j的值就等價於求[0,j-1]
-
中A[i]+i的最大值mx.即景點j的答案為mx+A[j]-j,而mx的值只要從后枚舉j的時候維護既可以
class Solution { public: int maxScoreSightseeingPair(vector<int>& A) { if(A.empty()) return 0; int n=A.size(); int fn=0; int tn=A[0]+0; for(int i=1;i<n;++i) { fn=max(fn,tn+A[i]-i); tn=max(tn,A[i]+i); } return fn; } };
-
- 4. 買賣股票的最佳時機
- 題目描述
-
-
- 問題分析,由於第i天的最大收益=max[前i-1天的最大收益,第i天的最大收益],因此其是一個動態規划問題,詳細分析見下圖
-
-
-
- 代碼如下
-
class Solution { public: int maxProfit(vector<int>& prices) { if(prices.empty()) return 0; vector<int> result(prices.size(),0); int minprice=prices[0]; for(int i=1;i<prices.size();++i) { //核心思路是,前i天的最大收益=max[前i-1天的最大收益,第i天的最大收益] result[i]=max(result[i-1],prices[i]-minprice); if(prices[i]<minprice) minprice=prices[i]; } return result[prices.size()-1]; } };
-
- 5. 最大子序和
- 題目描述
-
-
- 解題分析:要找到具有最大和的連續子數組,我們有兩種思路,
- 思路一:舉例分析數組的規律。從頭到尾累加數組中的每個數字,初始化為0,第一步加上第一個數字,此時和為1,第二步加上第二個數字-1,此時和變成了-1;第三步加上數字3,我們注意到此前累加的和為-1,小於0,如果用-1+3,得到的和為2,小於3,也就是說,從第一個數字開始的數組和會小於第三個數字開始的子數組的和。因此,我們不用考慮從第一個數組開始的子數組,之前累加的和也被拋棄。此時我們從第三個數字開始累加,發現得到的和是3,第四步加10,得到13.。。
- 思路二,利用動態規划的思想
-
1 class Solution { 2 public: 3 int maxSubArray(vector<int>& nums) { 4 if(nums.empty()) 5 return 0; 6 int fn=nums[0]; 7 int result=nums[0]; 8 for(int i=1;i<nums.size();++i) 9 { 10 fn=max(fn+nums[i],nums[i]); 11 result=max(result,fn); 12 } 13 return result; 14 } 15 };
-
-
- 解題分析:對於給定整數數組nums中,要求出數組從索引i到j范圍內的總和,包含i,j兩點我們可以直接求到(0-j的總和)-(0-(i-1)的總和)
-
代碼分析
class NumArray { public: vector<int> res; NumArray(vector<int>& nums) { int n=nums.size(); if(n>0) { vector<int> dp(n+1); dp[0]=0; for(int i=1;i<=n;++i) { dp[i]=dp[i-1]+nums[i-1]; } res=dp; } } int sumRange(int i, int j) { return res[j+1]-res[i]; } }; /** * Your NumArray object will be instantiated and called as such: * NumArray* obj = new NumArray(nums); * int param_1 = obj->sumRange(i,j); */
-
-
- 題目分析
-
由於我們每一次遞歸,都只用到了dp[i],dp[i-1],dp[i-2]三個位置,則dp數組有點浪費,因此可以采用滾動數組的思想來進行優化
滾動數組實際上實在動態規划中一種節省空間的方法。由於動態規划是一個自底向上擴展的過程,我們常常需要用到的是連續的解,前面的解往往可以舍去,因此利用滾動數組優化是很有效的,利用滾動數組在N很大的情況下可以達到壓縮存儲的作用
代碼
class Solution { public: int massage(vector<int>& nums) { if(nums.empty()) return 0; int ppre=0,pre=0,now=0; for(int i=1;i<=nums.size();++i) { ppre=pre; pre=now; now=max(pre,ppre+nums[i-1]); } return now; } };
解法同按摩師
-
-
- 問題解法
-
-
-
- 問題代碼
-
class Solution { public: int minCostClimbingStairs(vector<int>& cost) { int size=cost.size(); vector<int> mincost(size); mincost[0]=0; mincost[1]=min(cost[0],cost[1]); for(int i=2;i<size;++i) { mincost[i]=min(mincost[i-1]+cost[i],mincost[i-2]+cost[i-1]); } return mincost[size-1]; } };
-
-
- 問題分析
-
由於每次能夠走一步,兩步或者三步,假設n階台階的總方法是f(n),則f(1)=1,f(2)=2,f(3)=4 ,當有n級台階的時候,如果第一級台階走1步,則和后面的n-1級台階一樣,如果第一級台階走2步,則和后面的n-2級台階一樣,如果第一級台階走3步,則和后面的n-3級台階一樣,即其表達式為f(n)=f(n-1)+f(n-2)+f(n-3),類似於裴波那契數列
因此,直接調用遞歸會有很多重復的計算,因此我們采用自頂而下的思想進行實現
class Solution { public: int waysToStep(int n) { if(n<3) return n; long int first=1,second=2,third=4; long int temp; while(n>3) { temp=third; third=(first+second+third)%1000000007; first=second; second=temp; --n; } return third; } };
11. 猜數字大小2
- 題目描述
- 解題分析
- 代碼參考
1 class Solution { 2 public: 3 int getMoneyAmount(int n) { 4 if(n==1) 5 return 0; 6 //定義矩陣 7 int dp[n+1][n+1]; 8 //初始化 9 for(int i=0;i<=n;++i) 10 { 11 for(int j=0;j<=n;++j) 12 dp[i][j]=INT_MAX; 13 } 14 //定義基礎值dp[i][i] 15 for(int i=0;i<=n;++i) 16 dp[i][i]=0; 17 //按列填充,從第二列開始 18 for(int j=2;j<=n;++j) 19 { 20 //按行來,從下往上,因為填充的順序是從下往上的 21 for(int i=j-1;i>=1;--i) 22 { 23 //算除了兩端的每一個分割點 24 for(int k=i+1;k<=j-1;++k) 25 { 26 dp[i][j]=min() 27 } 28 } 29 30 } 31 32 } 33 };