動態規劃(DP)

Audrey_Hall發表於2022-03-22

動態規劃問題的一般形式就是求最值。比如求最長遞增子列最小編輯距離等等,求解動態規劃的核心問題是窮舉

  • 首先,動態規劃的窮舉有點特別,這類問題存在「重疊子問題」,窮舉的話效率會極其低下,所以需要快取來優化窮舉過程,避免不必要的計算。

  • 其次,動態規劃問題一般有「最優子結構」,用子問題的最優解獲得整個問題的最優解。

  • 另外雖然動態規劃的核心思想就是窮舉求最值,但是問題可以千變萬化,只有列出正確的「狀態轉移方程」才能正確地窮舉。

斐波那契數列

斐波那契數列的數學形式就是遞迴的,寫成程式碼就是這樣:

    public function dp($number)
    {
       if($number==1||$number==2){
           return 1;
       }
        return $this->dp($number - 1) + $this->dp($number- 2);
    }

這樣寫程式碼雖然簡潔易懂,但是十分低效,當N比較大時遞迴層數太深時間複雜度為 O(2^n),很明顯演算法低效的原因:存在大量重複計算,比如 f(n) 被計算了多次,這就是動態規劃問題的第一個性質:重疊子問題
帶快取的遞迴解法
即然耗時的原因是重複計算,那麼我們可以造一個「備忘錄」,每次算出的答案後別急著返回,先記到「備忘錄」裡再返回;每次遇到一個子問題先去「備忘錄」裡查一查,如果發現之前已經解決過這個問題了,直接把答案拿出來用,不要再耗時去計算了。

class SequenceDP
{
    public $array;

    public function __construct($number)
    {
        for ($i = 0; $i < $number + 1; $i++) {
            $this->array[$i] = -1;
        }
    }

    public function dp($number)
    {
       if($number==1||$number==2){
           return 1;
       }
        if ( $this->array[$number] != -1) 
        return $this->array[$number];
        return $this->dp($number - 1) + $this->dp($number- 2);
    }
}

$xx = new SequenceDP(10);
$int = $xx->dp(10);
var_dump($int);

細節優化

據斐波那契數列的狀態轉移方程,當前狀態只和之前的兩個狀態有關,其實並不需要那麼長的一個陣列來儲存所有的狀態,只要想辦法儲存之前的兩個狀態就行了。所以,可以進一步優化,把空間複雜度降為 O(1):

function fib($n) {
    if ($n == 2 || $n == 1) 
        return 1;
    $prev = 1, $curr = 1;
    for ($i = 3;$i <=$n; $i++) {
        $sum = $prev + $curr;
        $prev = $curr;
        $curr = $sum;
    }
    return $curr;
}

湊零錢的問題

下面我們來深入瞭解湊零錢的問題

給你 n 種面值硬幣,面值分別為 a, b ... ,e,每種硬幣的數量無限,再給一個總金額 amount,問你最少需要幾枚硬幣湊出這個金額 ?

很明顯這個問題是動態規劃問題,因為它具有「最優子結構」。子問題是互相獨立,互不干擾的。
我們首先確定正確的狀態轉移方程
狀態是原問題和子問題中變化的變數,由於硬幣數量無限,所以唯一的狀態就是金額 amount,所有函式每次變化為dp(amount),分析出虛擬碼如下:

# 要湊出金額 n,至少要 dp(n) 個硬幣
function dp(n){
    # 遍歷所有面值,選擇需要硬幣最少的那個結果
    for coin in coins:
            res = min(res, 1 + dp(n - coin))
    return res
}

PHP程式碼實現如下:

<?php

class Coin
{
    public $values = [1, 5, 10, 50];
    public function dp($amount)
    {
        $return = $amount;
        if ($amount < 0) {
            $return = -1;
        }elseif ($amount = 0)) {
            $return = 0;
        } elseif (in_array($amount, $this->values)) {
            $return = 1;
        } else {
            foreach ($this->values as $v) {
                //能夠湊的面值則湊
                if ($v < $amount) {
                    $return_temp = 1 + $this->dp($amount - $v);
                    //如果當前湊個數更少則返回
                    if ($return_temp < $return) {
                        $return = $return_temp;
                    }
                }
            }
        }
        return $return;
    }
}
$xx = new Coin();
$int = $xx->dp(65);
var_dump($int);

至此,狀態轉移方程其實已經完成了,以上演算法已經是暴力解法了,但是效率堪憂,以下是數學形式就是狀態轉移方程:

觀察發現子問題總數是指數級別的。每個子問題中含有一個 for 迴圈,複雜度為 O(k)。所以總時間複雜度是指數級別。我們也可以自底向上使用 DP快取陣列 來消除重疊子問題,
DP快取陣列定義:$array[i] = x 表示當目標金額為 i 時,至少需要 x 枚硬幣。初始化為-1表示未計算過。

<?php

class Coin
{
    public $values = [1, 5, 10, 50];
    public $array;

    public function __construct($amount)
    {
        for ($i = 0; $i < $amount + 1; $i++) {
            $this->array[$i] = -1;
        }
    }

    public function dp($amount)
    {
        $return = $amount;
        if ($amount < 1) {
            $return = 0;
        } elseif ($amount = 0)) {
            $return = 0;
        } elseif ($this->array[$amount] != -1) {
            $return = $this->array[$amount];
        } elseif (in_array($amount, $this->values)) {
            $return = 1;
        } else {
            foreach ($this->values as $v) {
                //能夠湊的面值則湊
                if ($v < $amount) {
                    $return_temp = 1 + $this->dp($amount - $v);
                    //如果當前湊個數更少則返回
                    if ($return_temp < $return) {
                        $return = $return_temp;
                    }
                }
            }
        }
        $this->array[$amount] = $return;
        return $return;
    }
}

$coin = new Coin(12111);
$int = $coin->dp(12111);
var_dump($int);

規劃

下篇文章講繼續學習最長遞增子列最小編輯距離

本作品採用《CC 協議》,轉載必須註明作者和本文連結
  • 如果可以,我要變成光

相關文章