1. 程式人生 > 實用技巧 >LeetCode動態規劃一:基本框架

LeetCode動態規劃一:基本框架

動態規劃框架

動態規劃:Dynamic Programming

動態規劃問題的一般形式就是求最值 -> 窮舉

動態規劃的窮舉特點在於這類問題存在重疊子問題,如果暴力窮舉,效率極其低下。所以一般通過備忘錄或者DP table用空間來優化窮舉的過程,避免不必要的計算。

動態規劃三要素:重疊子問題、最優子結構、狀態轉移方程

重疊子問題:
最優子結構:
狀態轉移方程:窮舉的方式

思維框架:
明確base case -> 明確[狀態] -> 明確[選擇] -> 定義dp陣列、函式的含義

程式碼框架:

# 初始化 base case
dp[0][0][...] = base
# 進行狀態轉移
for 狀態1 in 狀態1的所有取值:
    for 狀態2 in 狀態2的所有取值:
        for ...
            dp[狀態1][狀態2][...] = 求最值(選擇1,選擇2...)

下面以例子方式詳解動態規劃的基本原理

斐波那契數列問題

509.斐波那契數
斐波那契數,通常用 F(n) 表示,形成的序列稱為斐波那契數列。該數列由 0 和 1 開始,後面的每一項數字都是前面兩項數字的和。也就是:

F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
給定 N,計算 F(N)。

示例 1:

輸入:2
輸出:1
解釋:F(2) = F(1) + F(0) = 1 + 0 = 1.
示例 2:

輸入:3
輸出:2
解釋:F(3) = F(2) + F(1) = 1 + 1 = 2.
示例 3:

輸入:4
輸出:3
解釋:F(4) = F(3) + F(2) = 2 + 1 = 3.

提示:

0 ≤ N ≤ 30

由題意可以推到出一下遞推公式:

方法一:暴力遞迴

//方法一:遞迴
int fib(int n)
{
    // base case
    if (n == 0 || n == 1)
    {
        return 1;
    }
    return fib(n - 1) + fib(n - 2);
}

方法二:遞迴 + 備忘錄(自頂向下)

//方法二:遞迴 + 備忘錄(自頂向下)
int helper(int *rec_table, int n)
{
    if (n == 0 || n == 1)
    {
        return n;
    }
    if (rec_table[n] != 0)
    {
        return rec_table[n];
    }
    rec_table[n] = helper(rec_table, n - 1) + helper(rec_table, n - 2);
    return rec_table[n];
}
int fib(int n)
{
    if (n == 0 || n == 1)
    {
        return n;
    }
    int *rec_table = (int *)malloc((n + 1) * sizeof(int));
    memset(rec_table, 0, (n + 1) * sizeof(int));
    rec_table[0] = 0;
    rec_table[1] = 1;
    return helper(rec_table, n);
}

方法三:動態規劃 + dp陣列

// 方法三:動態規劃,陣列/滾動變數
int fib(int n)
{
    if (n == 0 || n == 1)
    {
        return n;
    }
    int *dp = (int *)malloc((n + 1) * sizeof(int));
    dp[0] = 0;
    dp[1] = 1;
    for (int i = 2; i <= n; i++)
    {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}