【coding】動態規劃

bilibili 看到一個博主講的比較好的動態規劃內容,自己對應整理了一下學習筆記,以便鞏固學習

在這裏插入圖片描述

題目一:Fibonacci

代碼簡單實現

/**
 * Creation         :       2018.11.18 17:03
 * Last Reversion   :       2018.11.18 17:08
 * Author           :       Lingyong Smile {[email protected]}
 * File Type        :       cpp
 * -----------------------------------------------------------------
 * Fibonacci
 *      Everyone knows the Fibonacci sequence. Now you need to enter an integer n.
 *      Please output the nth item of the Fibonacci sequence (starting at 0 and 0th item is 0).
 *      0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...
 * -----------------------------------------------------------------
 * Crop right @ 2018 Lingyong Smile {[email protected]}
 */

#include <iostream>
using namespace std;

/**
 * 遞歸方式
 */ 
int FibonacciRecursive(int n) {
    if (n == 0)
        return 0;
    else if (n == 1)
        return 1;
    return FibonacciRecursive(n - 2) + FibonacciRecursive(n - 1);
}

/**
 * 非遞歸:動態規劃
 */ 
int Fibonacci(int n) {
    if (n == 0) return 0;
    if (n == 1) return 1;
    int a = 0;
    int b = 1;
    int c;
    for (int i = 2; i <= n; i++) {
        c = a + b;
        a = b;
        b = c;
    }
    return c;
}

int main() {
    int n;
    cout << "Please input a int number:";
    while (cin >> n) {
        // Test Fibonacci function
        cout << "Fibonacci(" << n << ") is : " << Fibonacci(n) << endl;

        // Test Fibonacci Recursive function
        cout << "FibonacciRecursive(" << n << ") is : " << FibonacciRecursive(n) << endl;
        cout << "Please input a int number:";
    }
    // system("pause");
    return 0;
}

題目二:最多能賺幾塊錢?

橫軸代表時間,灰色的長方條代表從某一時刻開始的任務,長方條上的數字代表完成這個任務能夠賺幾塊錢,比如最上面的第1個任務,表示從1點開始,4點結束,完成這個任務可以獲得5元錢。其中有一個限制,開始了某一任務後,只有做完這個任務才能做下一個,比如開始做了任務7,則不能做任務4、5、6、8,因爲有時間衝突。現在有一個員工,如何才能賺最多的錢,最多能賺幾塊錢?在這裏插入圖片描述

從上往下分析,得出遞推公式(選不選)

其中 OPT(i)OPT(i) 表示,當我要考慮前 ii 個任務時,它的最優解是什麼,即最多能賺幾塊錢?
例如:當考慮前8個任務時 OPT(8)OPT(8),對於任務8有兩種狀態,選和不選。

  • 選第8個任務,首先可以賺4塊錢,再加上 OPT(5)OPT(5) ,即加上前面5個任務的最優解,最多能賺幾塊錢。(因爲選了任務8則和任務6和7有時間衝突)
  • 不選第8個任務,則就是前面7個任務的最優解 OPT(7)OPT(7)

然後選這兩個狀態結果最大的,作爲前8個任務的最優解 OPT(8)OPT(8) ,即得到如下遞歸公式:
OPT(8)=max{4+OPT(5)    ,8OPT(7)    ,8 OPT(8) = max \left\{ \begin{aligned} &amp;&amp; 4+ OPT(5) \ \ \ \ &amp;, 選任務8 \\ &amp;&amp; OPT(7) \ \ \ \ &amp;,不選任務8 \end{aligned}\right.

整理一下遞推公式
OPT(i)=max{vi+OPT(prev(i))    ,iOPT(i1)    ,i OPT(i) = max \left\{ \begin{aligned} &amp;&amp; v_i+ OPT(prev(i)) \ \ \ \ &amp;, 選任務i \\ &amp;&amp; OPT(i-1) \ \ \ \ &amp;,不選任務i \end{aligned}\right.

其中 viv_i 表示第 ii 個任務的獎勵工資,prev(i)prev(i) 表示如果要做第 ii 個任務,則它前面頂多還能做前幾個。例如:prev(8)=5prev(8) = 5 表示如果要做第8個任務,則前面頂多還能做前5個,即加上考慮前5個任務的最優解 OPT(prev(8))OPT(prev(8))
在這裏插入圖片描述
首先計算 prevprev 數組,比如 i=1i = 1 時,prev(1)=0prev(1) = 0 表示如果做任務1,則前面能做的爲0個,以此類推。我們最終要算的是前面8個任務的最優解,即 OPT(8)OPT(8) ,根據遞歸式可以得到如上狀結構,可以發現:(1)首先這是一個找最優解問題。(2)大問題的最優解依賴於子問題的最優解。(3)大問題分解成子問題,子問題間有重複子問題。 這樣的問題就可以使用動態規劃思想來做。從上往下分析問題,從下往上求解問題。從 OPT(1)OPT(1) 開始分析計算並保存對應結果。
OPT(1)=v1=5OPT(1) = v_1 = 5
OPT(2)OPT(2) 分析:如果不選任務2,則是 OPT(1)=5OPT(1) = 5 ;如果選任務2,則是 OPT(prev(2))+2=0+1OPT(prev(2)) + 任務2價值 = 0 + 1 。最後選這兩個狀態的最大值作爲前個任務2的最優解,所以 OPT(2)=5OPT(2) = 5。表示如果只有前面兩個任務的時候,最多能掙5塊錢。
在這裏插入圖片描述
OPT(4)OPT(4) 分析:如果不選任務4,則是 OPT(3)=8OPT(3) = 8 ;如果選任務4,則是 OPT(prev(4))+4=OPT(1)+v4=5+4=9OPT(prev(4)) + 任務4價值= OPT(1) + v_4 = 5 + 4 = 9 。最後選這兩個狀態的最大值作爲前4個任務的最優解,所以 OPT(4)=9OPT(4) = 9。表示如果只有前面4個任務的時候,最多能掙9塊錢。
在這裏插入圖片描述
後面以此類推,直到分析完前8個任務。
在這裏插入圖片描述

代碼簡單實現

/**
 * Creation         :       2019.04.05 14:50
 * Last Reversion   :       2019.03.05 15:22
 * Author           :       Lingyong Smile {[email protected]}
 * File Type        :       cpp
 * -----------------------------------------------------------------
 * 最多能賺幾塊錢(動態規劃) 結合筆記題目查看
 * -----------------------------------------------------------------
 * Crop right @ 2019 Lingyong Smile {[email protected]}
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define max(a, b) (((a) > (b)) ? (a) : (b))
#define min(a, b) (((a) < (b)) ? (a) : (b))

int v[9] = {0, 5, 1, 8, 4, 6, 3, 2, 4};     // 注意這裏前面多補了一個0,是爲了方便和筆記對應
int prev[9] = {0, 0, 0, 0, 1, 0, 2, 3, 5};  // 這裏的prev數組是自己預先算好了的

/**
 * 遞歸形式
 */ 
int OPTRecursive(int n) {
    if (n == 0)
        return 0;
    if (n == 1)
        return 5;
    return max(OPTRecursive(n - 1) , v[n] + OPTRecursive(prev[n]));
}

/**
 * 非遞歸形式:(動態規劃)
 */ 
int OPT(int n) {
    if (n == 0)
        return 0;
    if (n == 1)
        return 5;
    int *OPT = (int*)malloc(sizeof(int) * (n + 1));     // 這裏多開闢了一個int空間大小,是爲了和公式下標對應
    OPT[0] = 0;
    OPT[1] = 5;
    for (int i = 2; i <= n; i++) {
        OPT[i] = max(OPT[i-1], v[i] + OPT[prev[i]]);
    }
    int res = OPT[n];
    free(OPT);
    return res;
}

int main() {
    int n;
    printf("Please input a int number (1~8):\n");
    while (~scanf("%d", &n) && n >= 1 && n <= 8) {
        printf("%d\n", OPT(n));
    }
    return 0;
}

題目三:最大和

在如下一堆數字中選出一些數字,如何讓數字之和最大?
限定條件:選出的數字不能是相鄰的。

從上往下分析,得出遞歸公式(選不選)

在這裏插入圖片描述
OPT(6)OPT(6) :表示到下標爲6 這個位置的最佳方案(最優解)是什麼?
從上往下開始分析:對於 OPT(6)OPT(6) 我們有兩種狀態,選3,和不選3。即有如上遞歸式。

  • arr[6]arr[6],此時的最佳方案是 OPT(4)+arr[6]OPT(4) + arr[6] 。這裏不能選 arr[5]arr[5] ,因爲 arr[5]arr[5]arr[6]arr[6] 是相鄰的。
  • 不選 arr[6]arr[6],此時最佳方案是 OPT(5)OPT(5)

然後選這兩個狀態結果最大的,作爲到下標爲6這個位置的最優解 OPT(6)OPT(6)
在這裏插入圖片描述
進而,我們可以得到這樣的遞歸方程:

OPT(i)=max{OPT(i2)+arr[i]    ,arr[i]OPT(i1)    ,arr[i] OPT(i) = max \left\{ \begin{aligned} &amp;&amp; OPT(i-2) + arr[i] \ \ \ \ &amp;, 選arr[i] \\ &amp;&amp; OPT(i-1) \ \ \ \ &amp;,不選arr[i] \end{aligned}\right.

分析遞歸出口

OPT(0)=arr[0]OPT(1)=max(arr[0],arr[1]) OPT(0) = arr[0] \\ OPT(1) = max(arr[0], arr[1])

代碼簡單實現

/**
 * Creation         :       2019.04.06 11:10
 * Last Reversion   :       2019.04.06 11:23
 * Author           :       Lingyong Smile {[email protected]}
 * File Type        :       cpp
 * -----------------------------------------------------------------
 * 題目描述:
 *    在如下一堆數字中選出一些數字,如何讓數字之和最大?
 * 限定條件:選出的數字不能是相鄰的。
 * arr[7] = {1, 2, 4, 1, 7, 8, 3}
 * -----------------------------------------------------------------
 * Crop right @ 2019 Lingyong Smile {[email protected]}
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define max(a, b) (((a) > (b)) ? (a) : (b))
#define min(a, b) (((a) < (b)) ? (a) : (b))

int arr[7] = {1, 2, 4, 1, 7, 8, 3};

/**
 * 遞歸形式,不好存在很多重疊子問題
 */ 
int OPTRec(int n) {
    if (n == 0)
        return arr[0];
    else if (n == 1)
        return max(arr[0], arr[1]);
    else
        return max(OPTRec(n - 2) + arr[n], OPTRec(n - 1));
}

/**
 * 非遞歸形式:動態規劃
 */ 
int OPT(int n) {
    if (n == 0)
        return arr[0];
    else if (n == 1)
        return max(arr[0], arr[1]);
    
    int *OPT = (int*)malloc(sizeof(int) * (n + 1));     // 注意這裏開闢的數組大小,n爲下標,從0開始
    OPT[0] = arr[0];
    OPT[1] = max(arr[0], arr[1]);
    for (int i = 2; i <= n; i++) {
        OPT[i] = max(OPT[i-2] + arr[i], OPT[i-1]);
    }
    int res = OPT[n];
    free(OPT);
    return res;
}

int main() {
    int n;
    printf("Please input a int number (0~7):\n");
    while (~scanf("%d", &n) && n >= 0 && n <= 7) {
        printf("%d\n", OPT(n));
    }
    return 0;
}

題目四:是否存在和爲指定值

在如下一堆數字中選出一些數字求和,使其等於數字 SS。如果存在這樣的方案則返回 truetrue,否則返回 falsefalse

從上往下分析,得出遞歸公式(選不選)

在這裏插入圖片描述
分析:我們定義 SubsetSubset 表示子集,Subset(arr[5],9)Subset(arr[5], 9) 表示對前面5個數字取數求和爲9,應該怎麼分配。從上往下分析,當 i=5i = 5 時,我們有兩種狀態:

  • arr[5]arr[5] ,此時方案爲 Subset(arr[4],7)Subset(arr[4], 7) ,選了 arr[5]arr[5] 之後,只需要前面4個數字取數之和爲7就可以。
  • 不選 arr[5]arr[5] ,此時方案爲 Subset(arr[4],9)Subset(arr[4], 9) ,前面4個數字取數之和爲9。

最後只要這兩種方案有一個爲 truetrue,則滿足要求,所以中間用 oror

分析遞歸出口

在這裏插入圖片描述

  • S==0S == 0 時, 比如 Subset(arr[2],0)Subset(arr[2], 0) ,表示,2後面的數字已經存方案可以使得取數求和等於 SS ,即這個時候已滿足要求,返回 truetrue
  • i==0,S !=0i == 0, S \ != 0 時,即表示處理到第一個數字,只有當 arr[i]==Sarr[i] == S 纔會返回 truetrue ,否則返回 falsefalse 。比如,當我們給的數組只有一個元素是,只有當 arr[0]==Sarr[0] == S 時才爲 truetrue
    在這裏插入圖片描述
  • arr[i]&gt;Sarr[i] &gt; S 時,這時候我們肯定不能選 arr[i]arr[i] ,即此時返回 Subset(arr[i1],S)Subset(arr[i-1], S)
    在這裏插入圖片描述
    整理一下遞歸公式如下:
    在這裏插入圖片描述

動態規劃解法

即非遞歸方式,我們需要藉助二維數組,保存我們的中間過程(中間的這些重疊子問題)。如下設計我們的二維數組:
在這裏插入圖片描述
其中,最左邊列是 arr[]arr[ ] 數組,右邊是設計一個二維數組,行座標表示下標 ii ,縱座標表示 SS ,比如圖中的綠色方框表示 Subset(arr[2],3)Subset(arr[2], 3) 此時,由於 a[2]=4&gt;S=3a[2] = 4 &gt; S = 3 ,所以只能不選 arr[2]arr[2] ,即得到當前方案 Subset(arr[1],3)Subset(arr[1], 3) 。明白含義之後,我們來定義我們的出口條件:

  • i=0i = 0 時,只有 arr[0]=Sarr[0] = S 時返回 TrueTrue ,其餘都爲 FalseFalse 。即我們寫好了 i=0i = 0 這一行的出口條件
  • S=0S = 0 時,說明已經找到了取數方案,直接返回 TrueTrue 。即我們寫好了 S=0S = 0 這一列的出口條件。

然後按照從左往右的順序,把每一行填好,最後返回最後一個結果即可。
在這裏插入圖片描述
對於 subset[0,0]subset[0, 0] 我們規定爲 FalseFalse

代碼簡單實現

/**
 * Creation         :       2019.04.06 11:30
 * Last Reversion   :       2019.04.06 15:39:
 * Author           :       Lingyong Smile {[email protected]}
 * File Type        :       cpp
 * -----------------------------------------------------------------
 * 題目描述:
 *    在如下一堆數字中選出一些數字求和,能夠等於數字n。如果存在這樣的方案則
 * 返回true,否則返回false。
 * int arr[] = {3, 34, 4, 12, 5, 2};
 * 測試用例:
 輸入:
9
10
11
12
13
 輸出:
1
1
1
1
0
 * -----------------------------------------------------------------
 * Crop right @ 2019 Lingyong Smile {[email protected]}
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define N_SIZE 100
#define M_SIZE 100

int arr[] = {3, 34, 4, 12, 5, 2};
int subset[N_SIZE][M_SIZE];

/**
 * 遞歸形式
 */ 
int RecSubset(int arr[], int i, int S) {
    if (i == 0)                 // 這裏的兩個出口和調換了一下順序(與筆記想筆記),因爲比如當數組只有一個元素arr[] = {4}, S = 5,此時因該返回False纔對。
        return (arr[0] == S);
    else if(S == 0)
        return 1;
    else 
        return RecSubset(arr, i-1, S-arr[i]) || RecSubset(arr, i-1, S);
}

/**
 * 非遞歸形式:動態規劃
 */ 
int DPSubset(int arr[], int len, int S) {
    for (int i = 0; i < len; i++) {     // 將S == 0 列都初始化爲True
        subset[i][0] = 1;
    }
    for (int s = 0; s <= S; s++) {      // 將 i == 0 行,除了arr[0]列初始化爲True,其餘都初始化爲False
        subset[0][s] = 0;
    }
    subset[0][arr[0]] = 1;

    for (int i = 1; i < len; i++) {
        for (int s = 1; s <= S; s++) {
            if (arr[i] > s)
                subset[i][s] = subset[i-1][s];
            else {
                subset[i][s] = (subset[i-1][s-arr[i]] || subset[i-1][s]);
            }
        }
    }
    return subset[len-1][S];
}

int main() {
    if (RecSubset(arr, 5, 13))
        printf("True\n");
    else
        printf("False\n");
    if (DPSubset(arr, 6, 13))
        printf("True\n");
    else
        printf("False\n");
    return 0;
}

Todo

  • 01揹包
  • 完全揹包
  • 多重揹包

Reference

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