從遞歸開始,完全搞懂動態規劃

從遞歸開始,完全搞懂動態規劃

前言

 之前ACM時期,一開始學習動態規劃時,並不懂這個東西究竟是個什麼概念。搜索題解,大部分每每也是甩個題目,而後直接列出狀態轉移方程,緊接着直接來個AC代碼,讓人云裏霧裏。儘管作過一些題目,可是每每遇到新的題目便抓瞎了。只會一些作過的題,例如導彈攔截,各類揹包。今天藉助算法課的做業,仔細的將動態規劃剖析一遍。一步一步說明,如何將普通的指數級複雜度的遞歸算法優化爲多項式級複雜度的動態規劃算法。算法

題目背景:投資問題

$m$ 元錢,投資 $n$ 個項目,效益函數 $f_i(x) $,表示 $i$ 個項目投資 $x$ 元的收益,求如何分配每一個項目的錢數使得總效益最大?

遞歸算法

遞歸式推導

 假設投資 $ i $ 個項目,共投資 $ x $ 元的收益狀況的全部可能性爲 $g_i(x)$ 。顯然可得:函數

$$ \begin{cases} g_1(x)=f_1(x_1),\ \ x_1 \leq x .\\ g_2(x)=f_1(x_1)+f_2(x_2),\ \ x_1+x_2 \leq x\\ g_3(x)=f_1(x_1)+f_2(x_2)+f_3(x_3),\ \ x_1+x_2+x_3 \leq x\\ \ \ ...\\ \ \ ...\\ \ \ ...\\ g_i(x)=\sum_{k=1}^if_k(x_k),\ \ \sum_{k=1}^ix_k \leq x\\ \end{cases} $$性能

 將 $g_{i-1}(x-x_i)$ 代入 $g_i(x)$ 可得:學習

$$ \begin{cases} g_1(x)=f_1(x_1),\\ g_2(x)=g_1(x-x_2)+f_2(x_2),\\ g_3(x)=g_2(x-x_3)+f_3(x_3),\\ \ \ ...\\ \ \ ...\\ \ \ ...\\ g_i(x)=g_{i-1}(x-x_i)+f_i(x_i)\\ \end{cases} $$測試

 整理可得遞歸式:優化

$$ g_i(x)= \begin{cases} f_1(x),\ \ i=1 .\\ g_{i-1}(x-x_i)+f_i(x_i),\ \ i>1, \sum_{k=1}^ix_k \leq x\\ \end{cases} $$ spa

 咱們但願總收益最大,即求
$$w_i(x)=max\{g_i(x)\}$$
此即遞歸定義的目標函數 ,注意 $g_i(x)$ 是當前項目投資收益與前面全部投資項目的收益的排列組合。翻譯

遞歸實現

Java代碼

/**
 * @Author xwh
 * @Date 2020/4/13 13:05:53
 **/
public class Investment {

    /* 投資收益函數 */
    public static int f[][] = new int[5][10];

    /**
     * 投資i個工廠, 共x元的最大收益
     */
    public static int g(int i, int x) {

        // 輸出一下當前的計算層級, 方便下個步驟分析複雜度
        System.out.println("g " + i + " " + x);

        int max = 0;
        if (i == 1) {
            // 投資第一個工廠的最大收益就是對應函數值
            return f[i][x];
        } else {
            // DFS, 根據公式窮舉全部收益狀況, 並求其中最大值返回
            // 當前收益 = 第i個工廠投資j元收益 + 前i-1個工廠投資x-j元的最大收益
            for (int j = 0; j <= x; j++) {

                int temp = f[i][j] + g(i - 1, x - j);
                if (temp > max) {
                    max = temp;
                }
            }
        }
        return max;
    }

    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);
        int n = 4, m = 6;

        // 投資函數初始化, f[i][j]表示第i個工廠投資j元的收益
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= m; j++) {
                f[i][j] = scanner.nextInt();
            }
        }

        System.out.println("搜索樹的DFS序列:");
        int w = g(4, 6);
        System.out.printf("向%d個工廠投資%d元的最大收益爲:%d元", n, m, w);
    }

}

測試數據

0 20 50 65 80 85 85
0 20 40 50 55 60 65
0 25 60 85 100 110 115
0 25 40 50 60 65 70

其中第 $i$ 行第 $j$ 列的數字表示第 $i$ 個工廠投資 $j$ 元的收益。code

複雜度分析

 對於每個 $g_i(x)$ ,遞歸求解時下一級都爲一個組合數。因此複雜度應爲:
$$C_{m+n-1}^m= \frac {(m+n-1)!} {(n-1)!m!} = O((1+\epsilon)^{m+n-1})$$
結果是指數級的。咱們知道指數就意味着爆炸。顯然這樣複雜度的算法在解決實際問題上並無太大的意義。因此咱們必須經過必定的方法將這個複雜度降階。這種方法就是動態規劃。blog

遞歸算法的問題分析

 要知道如何進行優化,咱們必須知道問題出在哪裏。下面來分析一下遞歸的性能就行損耗在了哪裏。傳入測試數據,執行上一節的代碼,獲得以下結果:

image.png

 能夠看到,計算4個工廠投資6元的收益,共窮舉了120種狀況。繼續分析:

image.png

 咱們發現 $g_2(j)$ 這種狀況被計算了28次。實際上這種狀況應該只有 $ g_2(0), g_2(1) \cdots g_2(6)$ 共6種纔對。

 爲何會出現這種狀況呢?
 對於每一次計算,遞歸程序都會去嘗試一遍更深一層遞歸的全部排列組合,致使性能很是低。例如,在計算 $g_4(6)$ 時,程序會去計算 $g_3(0) \cdots g_3(6) $, 而對於其中的每一項,都會去計算 $g_2(0) \cdots g_2(6)$,在這些排列組合中找到當前的最優解。
可見,當前遞歸算法的問題在於:1. 相同的子問題重複計算。 2. 已經得出最優解的問題重複地去窮舉次優解。

優化:動態規劃

 能用動態規劃進行優化的問題,必須知足以下三個原則:

  • 重疊子問題
  • 最優子結構
  • 無後效性

看上去很是抽象,實際上咱們已經證實了當前問題知足這三個性質,下面開始說明。

重疊子問題

重疊子問題是一個遞歸解決方案裏包含的子問題雖然不少,但不一樣子問題不多。少許的子問題被重複解決不少次。

 這是重疊子問題的定義說明,看上去是否是很熟悉。就在上一節,咱們已經發現的遞歸算法的問題1即是這個意思。咱們重複計算了不少同樣的問題,而這種計算其實是能夠經過記憶化搜索,經過時間換空間避免的。

最優子結構

一個最優化策略的子策略老是最優的。換句話說,一個最優策略,必然包含子問題的最優策略。

 一樣,咱們在上一節發現,咱們計算當前最大收益 $g_i(x)$ 時,$ g_{i-1}(j) $ 必然是所有被計算過的,遞歸發現 $g_{i-2}(k)$ 一樣如此。

無後效性

所謂無後效性原則,指的是這樣一種性質:某階段的狀態一旦肯定,則此後過程的演變再也不受此前各狀態及決策的影響。

 意思就是,對於每個 $g_i(x)$,僅僅與 $g_{i-1}(j)$ 有關,而 $g_{i+1}(k)$ 也僅僅和 $g_i(x)$ 有關。一樣的,咱們在計算中,每次的排列組合僅僅與前 $i$ 個工廠的總收益有關。

優化方案

 如今咱們已經證實,當前的投資問題,知足使用動態規劃三個原則。那麼咱們能夠經過動態規劃來進行優化。那麼說了這麼久究竟什麼是動態規劃呢?

 咱們回到以前提到過的兩個問題,即:1. 相同的子問題重複計算。 2. 已經得出最優解的問題重複地去窮舉次優解。

 問題1對應着重疊子問題,問題2對應着最優子結構。那麼咱們是否是能夠經過必定的方法,避免程序重複地去窮舉次優解,以及重複地去計算最優解呢?

 既然咱們已經得出,當前最優解只與前一個最優解有關,那咱們只要每次保存下計算的最優解,每次用到直接調用拿到,不須要再深刻遞歸去計算不就消去以上兩個問題了嗎?

換句話說,咱們只要找到一個公式,根據公式,利用當前狀態的最優解集合不斷地窮舉下一個狀態的最優解,不是能夠直接消去遞歸了嗎?

狀態轉移方程

 利用最優子結構及重疊子問題性質,改寫以前的遞歸公式,得:

$$ w_i(x)= \begin{cases} f_1(x),\ \ i=1 .\\ max\{g_i(x)\}=f_i(k)+w_{i-1}(x-k),\ \ i>1 \\ \end{cases} $$

 咱們成功地將當前最優解與以前計算過的最優解聯繫了起來,避免了每次去窮舉次優解和計算過的最優解。
此即狀態轉移方程。

Java代碼

public static void dp(int n, int m) {

        int i, j, k, temp;
        int w[][] = new int[n + 1][m + 1];

        // 計算投資第一個項目的最大收益
        for (i = 0; i <= m; i++) {
            w[1][i] = f[1][i];
        }

        // 投資前i個項目
        for (i = 2; i <= n; i++) {
            // 計算每個g[i][x], 0<=x<=m
            for (j = 0; j <= m; j++) {
                // 狀態轉移, 利用w[i-1][]計算w[i]
                // g[i][x] == temp == f[i][k] + w[i-1][j-k]
                // k投資當前項目的錢數,  0<=k<=j
                for (k = 0; k <= j; k++) {
                    temp = f[i][k] + w[i - 1][j - k];
                    if (temp > w[i][j]) {
                        // 更新當前的最優解, 給下一個最優解調用
                        w[i][j] = temp;
                    }
                }
            }
        }
        System.out.printf("向%d個工廠投資%d元的最大收益爲:%d元\n", n, m, w[n][m]);
    }

總結

 我把動態規劃理解爲進行了次優解剪枝與重複子問題記憶化的窮舉算法,經過最優解來窮舉最優解。也不知道當初的學者爲何會把 $ Dynamic \ Programming $ 翻譯成動態規劃,太字面了真的很差理解。

相關文章
相關標籤/搜索