C++---算法學習筆記之動態規劃(一)入門

一. 關於動歸的一些概念

不要小瞧概念,有時候 一些概念性的術語可以幫助你更深入的瞭解一門算法,在這裏把一些我認爲重要的概念交代清楚。

1.1 什麼是動歸?

動歸是是分治思想的延伸,通俗一點來說就是大事化小,小事化無的藝術。
在將大問題化解爲小問題的分治過程中,保存對這些小問題已經處理好的結果,並供後面處理更大規模的問題時直接
使用這些結果。

1.2 動歸的特徵

  • 原來的問題可以分解成幾個相似的子問題
  • 所有的子問題都只需要解決一次
  • 存儲子問題的解

1.3動歸的四個本質(重要)

  • 狀態定義
  • 狀態間的轉移方程
  • 狀態的初始化
  • 返回結果

所有用動歸解題的過程中,只要將上述4個要素定義清楚,那麼就算成功了。

1.4 運用動歸解決的題目場景(動歸解題信號)

  • 最大值、最小值問題
  • 可行或不可行的方案問題
  • 求方案個數問題
  • 題目中包含“是否可以用”等規劃性問題

二. 通過實戰感受動歸思想

概念說再多都只是紙上談兵,下面將由易到難,通過三道比較經典,難度比較低的動歸算法題來感受動態規劃的思維,作爲動歸入門學習。

2.1 斐波那契數列

上機練習地址:牛客–斐波那契數列

題目描述:大家都知道斐波那契數列,現在要求輸入一個整數n,請你輸出斐波那契數列的第n項(從0開始,第0項爲0,第1項是1)。 n<=39

這個題目已經寫爛了,遞歸的方法就不再贅述,來看看這題目在動歸的思維下是什麼樣子。
上面已經提到,所有用動歸解的題,核心就是找四個量:狀態定義、狀態轉移方程、狀態初始化、返回值。那麼在這個題目中,這四個量非常明顯可以找出。

狀態定義:斐波那契數列的第i項的值
狀態方程:F(i)=F(i-1)+F(i-2)
狀態初始化:F(0)=0,F(1)=1
返回值:F(n)

源代碼

class Solution {
public:
    int Fibonacci(int n) {
    
        int* F=new int[n+1];//用來保存每一次結果的值
        
        //初始化
        F[0]=0;
        F[1]=1;
        for(int i=2;i<=n;i++)
        {
            //轉換方程
            F[i]=F[i-1]+F[i-2];
        }
        //返回值
        return F[n];
    }
};

上述方法開闢了一個數組用於保存每次結果的值,實際上,可以只通過一個變量保存最終的值,節省更多的空間。代碼如下:

class Solution {
public:
    int Fibonacci(int n) {
        if(n<=0)
        {
            return 0;
        }
        if(n==1||n==2)
        {
            return 1;
        }
        //初始化
        int first=1,second=1;
        int fn=0;
        for(int i=3;i<=n;i++)
        {
            //狀態轉移方程
            fn=first+second;
            first=second;
            second=fn;
        }
        return fn;
    }
};

2.2 青蛙跳臺階

上機練習地址:牛客—變態青蛙跳臺階

題目描述:一隻青蛙一次可以跳上1級臺階,也可以跳上2級……它也可以跳上n級。求該青蛙跳上一個n級的臺階總共有多少種跳法。

這個題目應該也都見得多,遞歸解法也不再談。但是這個題目和上一個斐波那契數列比,狀態轉移方程沒有那麼容易get到,下面我通過畫圖來描述。
在這裏插入圖片描述
通過寫出前面幾階臺階的方法數,我們可以遞推出,當前第i階臺階方法數等於前一階(也就是第i-1階)臺階方法數的兩倍,由此得到轉移方程爲F(i)=2*F(i-1),寫出四要素。

狀態定義:跳上第i級臺階的方法數
狀態轉移方程:F(i)=2*F(i-1)
狀態初始化:F(1)=1
返回值:F(n)

代碼:

class Solution {
public:
    int jumpFloorII(int number) {
        if(number<=0)
        {
            return 0;
        }
        //初始化
        int f1=1;
        //狀態定義
        int fn=f1;
        for(int i=2;i<=number;i++)
        {
            //轉換方程
            fn=fn*2;
        }
        //返回值
        return fn;
    }
};

補充拓展
該題目如果改編一下:現在讓它變成一個正常的青蛙,限制它 一次只能跳1階或者2階,現在該如何解答
變化的只有轉移方程,由於青蛙一次只可以跳1階或者2階,所以任意第i個臺階的方法數都=前一個(第i-1個)臺階的方法數+前兩個(第i-2)個臺階的方法數,因爲第i個臺階只能由第i-1個臺階和第i-2個臺階跳過來。

狀態定義:跳上第i級臺階的方法數
狀態轉移方程:F(i)=F(i-1)+F(i-2)
狀態初始化:F(1)=1
返回值:F(n)

這個問題就是斐波那契數列的思想。

2.3 最大連續子數組和

上機練習地址:牛客—連續子數組的最大和

題目描述
在古老的一維模式識別中,常常需要計算連續子向量的最大和,當向量全爲正數的時候,問題很好解決。但是,如果向量中包含負數,是否應該包含某個負數,並期望旁邊的正數會彌補它呢?例如:{6,-3,-2,7,-15,1,2,2},連續子向量的最大和爲8(從第0個開始,到第3個爲止)。給一個數組,返回它的最大連續子序列的和。

同樣通過畫圖來遞推狀態方程,紅框內的爲當前最大連續子數組。
在這裏插入圖片描述
這裏的F(i)是在:前一個以i-1元素結尾的連續最大子數組和+array[i] 與 array[i]之間找最大值。 也就是說,比如F(2),只會在(6,-3,-2)和(-3)之間選擇最大值,爲什麼拋棄(-3,-2)?因爲(6,-3)是以第i-1個元素結尾的最大連續子數組和。類似的,F(3)選擇時會拋棄(-3,-2,7),(-2,7)。

狀態定義:
F(i) 以第i+1個元素結尾的最大連續和
狀態方程:
F(i)=max(F(i)+arr[i],arr[i])
初始化:
F(0)=arr[0]
返回值:
max(F(i)) i<n

代碼
1.開闢數組保存每次當前元素結尾的最大連續和F(i)

class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
        
        vector<int> F(array.size());//用於保存每次以當前元素結尾的最大連續和F(i)
        //初始化
        F[0]=array[0];
        
        for(int i=1;i<F.size();i++)
        {
            //轉移方程
            F[i]=max(F[i-1]+array[i],array[i]);
        }
        
        int maxsum=F[0];//用於保存F(i)的最大值
        //找最大的和
        for(int i=1;i<F.size();i++)
        {
            maxsum=max(maxsum,F[i]);
        }
        //返回值
        return maxsum;
    }
};

2.不開闢數組,每次更新最大連續子數組和

class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
        //初始化
        int cursum=array[0];//代表當前的連續子數組和
        int maxsum=array[0];//最大連續子數組和
        for(int i=1;i<array.size();i++)
        {
            //轉移方程
            cursum=max(cursum+array[i],array[i]);
            //更新每次最大的子數組和
            maxsum=max(maxsum,cursum);
        }
        //返回值
        return maxsum;
    }
};

兩種方法都可以。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章