動態規劃本質依然是遞歸算法,只不過是知足特定條件的遞歸算法;動態規劃是一個設計感比較強,藝術感比較強的一種算法設計思想。ios
將原問題拆解成若干子問題,同時保存子問題的答案,使得每一個子問題只求解一次,最終得到原問題的答案。c++
#include <iostream>
#include <ctime>
using namespace std;
int num = 0;
int fib( int n ){
num ++;
if( n == 0 )
return 0;
if( n == 1 )
return 1;
return fib(n-1) + fib(n-2);
}
int main() {
num = 0;
int n = 42;
time_t startTime = clock();
int res = fib(n);
time_t endTime = clock();
cout<<"fib("<<n<<") = "<<res<<endl;
cout<<"time : "<<double(endTime-startTime)/CLOCKS_PER_SEC<<" s"<<endl;
cout<<"run function fib() "<<num<<"times."<<endl;
return 0;
}
複製代碼
經過計時咱們會發現這個算法很慢,爲何這個解法效率這麼低呢?當咱們須要計算fib(5)時,它的遞歸樹是:算法
從這個圖能夠看出這裏面有大量的重複計算,咱們怎樣避免呢,咱們能夠在程序的外面作一個數組memo,其實memo[i]就記憶了第i個斐波那契數列。數組
#include <iostream>
#include <ctime>
#include <vector>
using namespace std;
vector<int> memo;
int num = 0;
// 記憶化搜索
int fib( int n ){
num ++;
if( n == 0 )
return 0;
if( n == 1 )
return 1;
if( memo[n] == -1 )
memo[n] = fib(n-1) + fib(n-2);
return memo[n];
}
int main() {
num = 0;
int n = 42;
memo = vector<int>(n+1,-1);
time_t startTime = clock();
int res = fib(n);
time_t endTime = clock();
cout<<"fib("<<n<<") = "<<res<<endl;
cout<<"time : "<<double(endTime-startTime)/CLOCKS_PER_SEC<<" s"<<endl;
cout<<"run function fib() "<<num<<"times."<<endl;
return 0;
}
複製代碼
咱們採用一個memo數組來記憶,因此叫作記憶化搜索。記憶化搜索其實就是在遞歸的過程當中添加計劃化,是一種自上向下的解決問題,咱們假設基本的問題已經解決了,咱們已經會求fib(n-1)和fib(n-2)了,那麼咱們就能求第n個數了。bash
若是咱們能自上而下解決問題,咱們也能自下而上解決問題,只不過不少時候咱們習慣於前者。微信
#include <iostream>
#include <ctime>
#include <vector>
using namespace std;
// 動態規劃
int fib( int n ){
vector<int> memo(n+1, -1);
memo[0] = 0;
memo[1] = 1;
for( int i = 2 ; i <= n ; i ++ )
memo[i] = memo[i-1] + memo[i-2];
return memo[n];
}
int main() {
// 結果會溢出,這裏只看性能
int n = 1000;
time_t startTime = clock();
int res = fib(n);
time_t endTime = clock();
cout<<"fib("<<n<<") = "<<res<<endl;
cout<<"time : "<<double(endTime-startTime)/CLOCKS_PER_SEC<<" s"<<endl;
return 0;
}
複製代碼
咱們來看一下遞歸的思路,把一個大的問題分解成小的問題。函數
#include <iostream>
#include <vector>
using namespace std;
// 記憶化搜索
class Solution {
private:
vector<int> memo;
int calcWays(int n){
if( n == 0 || n == 1)
return 1;
if( memo[n] == -1 )
memo[n] = calcWays(n-1) + calcWays(n-2);
return memo[n];
}
public:
int climbStairs(int n) {
memo = vector<int>(n+1,-1);
return calcWays(n);
}
};
複製代碼
咱們會發現和上面斐波那契同樣,很輕易能夠轉化爲動態規劃解法。性能
#include <iostream>
#include <vector>
using namespace std;
// 動態規劃
class Solution {
public:
int climbStairs(int n) {
vector<int> memo(n+1, -1);
memo[0] = 1;
memo[1] = 1;
for ( int i = 2; i <= n; i++ ) {
memo[i] = memo[i-1] + memo[i-2];
}
return memo[n];
}
};
複製代碼
對於一個問題若是沒有思路時,咱們能夠先考慮暴力解法。話句話說,咱們使用什麼樣的方式,才能把正整數n的全部分割枚舉出來,咱們沒法知道有幾重循環,一般咱們須要使用遞歸的手段。
暴力解法:回溯遍歷將一個數作分割的全部可能性。O(2^n)大數據
之因此遞歸樹存在,是由於它有最優子結構
經過求子問題的最優解,能夠得到原問題的最優解。ui
#include <iostream>
#include <cassert>
using namespace std;
class Solution {
private:
int max3( int a , int b , int c ){
return max( a , max(b,c) );
}
// 將n進行分割(至少分割兩部分), 能夠得到的最大乘積
int breakInteger( int n ){
if( n == 1 )
return 1;
int res = -1;
for( int i = 1 ; i <= n-1 ; i ++ )
res = max3( res , i*(n-i) , i * breakInteger(n-i) );
return res;
}
public:
int integerBreak(int n) {
assert( n >= 1 );
return breakInteger(n);
}
};
複製代碼
它包含重疊子問題,下面是記憶化搜索版本:
class Solution {
private:
vector<int> memo;
int max3( int a , int b , int c ){
return max( a , max(b,c) );
}
// 將n進行分割(至少分割兩部分), 能夠得到的最大乘積
int breakInteger( int n ){
if( n == 1 )
return 1;
if( memo[n] != -1 )
return memo[n];
int res = -1;
for( int i = 1 ; i <= n-1 ; i ++ )
res = max3( res , i*(n-i) , i * breakInteger(n-i) );
memo[n] = res;
return res;
}
public:
int integerBreak(int n) {
assert( n >= 1 );
memo = vector<int>(n+1, -1);
return breakInteger(n);
}
};
複製代碼
下面咱們使用自底向上的方法,也就是動態規劃解決這個問題
class Solution {
private:
int max3( int a , int b , int c ){
return max(max(a,b),c);
}
public:
int integerBreak(int n) {
// memo[i] 表示將數字i分割(至少分割成兩部分)後獲得的最大乘積
vector<int> memo(n+1, -1);
memo[1] = 1;
for ( int i = 2; i <= n; i++ ) {
// 求解memo[i]
for ( int j = 1; j <= i-1; j++ ) {
// j + (i-j)
memo[i] = max3( memo[i], j*(i-j), j*memo[i-j] );
}
}
return memo[n];
}
};
複製代碼
考慮偷取[x...n-1]範圍裏的房子(函數定義)
f(0) = max{ v(0) + f(2), v(1) + f(3), v(2) + f(4), … , v(n-3) + f(n-1), v(n-2), v(n-1)}(狀態轉移方程)
首先依然是若是沒有思路的話,先考慮暴力解法。檢查全部的房子,對每一個組合,檢查是否有相鄰的房子,若是沒有,記錄其價值,找最大值。O((2^n)*n)
注意其中對狀態的定義:
考慮偷取[x…n-1]範圍裏的房子(函數的定義)
根據對狀態的定義,決定狀態的轉移:
f(0) = max{ v(0) + f(2), v(1) + f(3), v(2) + f(4), … , v(n-3) + f(n-1), v(n-2), v(n-1)}(狀態轉移方程)
實際上咱們的遞歸函數就是在實現狀態轉移。
class Solution {
private:
// memo[i] 表示考慮搶劫 nums[i...n) 所能得到的最大收益
vector<int> memo;
// 考慮搶劫nums[index...nums.size())這個範圍的全部房子
int tryRob( vector<int> &nums, int index){
if( index >= nums.size() )
return 0;
if( memo[index] != -1 )
return memo[index];
int res = 0;
for( int i = index ; i < nums.size() ; i ++ )
res = max(res, nums[i] + tryRob(nums, i+2));
memo[index] = res;
return res;
}
public:
int rob(vector<int>& nums) {
memo = vector<int>(nums.size(), -1);
return tryRob(nums, 0);
}
};
複製代碼
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if( n == 0 ) {
return 0;
}
// memo[i] 表示考慮搶劫 nums[i...n) 所能得到的最大收益
vector<int> memo(n, 0);
memo[n-1] = nums[n-1];
for( int i = n-2 ; i >= 0 ; i -- ) {
for (int j = i; j < n; j++) {
memo[i] = max(memo[i], nums[j] + (j + 2 < n ? memo[j + 2] : 0) );
}
}
return memo[0];
}
};
複製代碼
咱們所強調的是對於動態規劃來講,咱們要清晰本身對狀態的定義,在咱們以前的定義咱們是去考慮偷取[x…n-1]範圍裏的房子(函數的定義)。對於一樣的問題,不少時候咱們能夠設立不一樣的狀態獲得一樣正確的答案。
改變對狀態的定義:
考慮偷取[0…x]範圍裏的房子(函數的定義)。實現以下:
class Solution {
private:
vector<int> memo;
//考慮偷取[0..x]範圍裏的房子
int tryRob(vector<int>&nums, int index){
if (index < 0){
return 0;
}
if (memo[index] != -1){
return memo[index];
}
int res = 0;
for( int i = index; i >= 0; i--){
res = max(res, nums[i] + tryRob(nums, i - 2));
}
memo[index] = res;
return res;
}
public:
int rob(vector<int>& nums) {
int n = nums.size();
memo = vector<int>(n + 1, -1);
if (n == 0){
return 0;
}
return tryRob(nums, n-1);
}
};
複製代碼
class Solution {
public:
//考慮偷取[0..x]範圍裏的房子
int rob(vector<int>& nums) {
int n = nums.size();
vector<int> memo(n, -1);
if (n == 0){
return 0;
}
memo[0] = nums[0];
for(int i = 1; i < n; i++){
for(int j = i; j >= 0; j --){
memo[i] = max(memo[i], nums[j] + (j-2 >= 0? memo[j-2]: 0));
}
}
return memo[n-1];
}
};
複製代碼
-------------------------華麗的分割線--------------------
看完的朋友能夠點個喜歡/關注,您的支持是對我最大的鼓勵。
想了解更多,歡迎關注個人微信公衆號:番茄技術小棧