算法-高級-動態規劃

算法-高級-動態規劃

說明:理論部分主要摘錄百度百科和極客時間訂閱的課程《數據結構與算法之美》(推薦),實戰部分則是自己學習過程中解決的一些題目和解題思路記錄。

1 概念(摘錄自百度百科)

動態規劃(dynamic programming)是運籌學的一個分支,是求解決策過程(decision process)最優化的數學方法。

把多階段過程轉化爲一系列單階段問題,利用各階段之間的關係,逐個求解,是一種過程優化問題的方法。

動態規劃程序設計是對解最優化問題的一種途徑、一種方法,而不是一種特殊算法。 不像搜索或數值計算那樣,具有一個標準的數學表達式和明確清晰的解題方法。

動態規劃程序設計往往是針對一種最優化問題,由於各種問題的性質不同,確定最優解的條件也互不相同,因而動態規劃的設計方法對不同的問題,有各具特色的解題方法,而不存在一種萬能的動態規劃算法,可以解決各類最優化問題。

在現實生活中,有一類活動的過程,由於它的特殊性,可將過程分成若干個互相聯繫的階段,在它的每一階段都需要作出決策,從而使整個過程達到最好的活動效果。

當然,各個階段決策的選取不是任意確定的,它依賴於當前面臨的狀態,又影響以後的發展,當各個階段決策確定後,就組成一個決策序列,因而也就確定了整個過程的一條活動路線(如下圖)

這種把一個問題看作是一個前後關聯具有鏈狀結構的多階段過程就稱爲多階段決策過程,這種問題就稱爲多階段決策問題。

多階段決策問題

2 特點

優點:可以非常顯著地降低時間複雜度,提高代碼的執行效率。

缺點:主要學習難點跟遞歸類似,那就是,求解問題的過程不太符合人類常規的思維方式

3 貪心、分治、回溯、動態規劃幾種算法的比較

算法 時間複雜度 空間複雜度 適用場景
回溯 執行效率低,時間複雜度是指數級 小規模數據時,能用的動態規劃、貪心解決的問題,我們都可以用回溯算法解決。相當於窮舉搜索。時間複雜度非常高,是指數級別的,只能用來解決小規模數據的問題。
動態規劃 執行效率高很多,但是動態規劃的空間複雜度也提高了。是一種空間換時間的算法思想。 需要滿足三個特徵,最優子結構、無後效性和重複子問題。
貪心 執行效率更加高效 貪心算法實際上是動態規劃算法的一種特殊情況。它解決問題起來更加高效,代碼實現也更加簡潔。不過,它可以解決的問題也更加有限。它能解決的問題需要滿足三個條件,最優子結構、後效性和貪心選擇性(這裏我們不怎麼強調重複子問題)。“貪心選擇性”的意思是,通過局部最優的選擇,能產生全局的最優選擇。每一個階段,我們都選擇當前看起來最優的決策,所有階段的決策完成之後,最終由這些局部最優解構成全局最優解。
分治 分治算法要求分割成的子問題,不能有重複子問題,而動態規劃正好相反,動態規劃之所以高效,就是因爲回溯算法實現中存在大量的重複子問題。

貪心、分治、回溯、動態規劃,這四個算法思想有關的理論知識,大部分都是“後驗性”的,也就是說,在解決問題的過程中,我們往往是先想到如何用某個算法思想解決問題,然後才用算法理論知識,去驗證這個算法思想解決問題的正確性。

貪心、回溯、動態規劃可以歸爲一類,而分治單獨可以作爲一類,前三個算法解決問題的模型,都可以抽象成我們今天講的那個多階段決策最優解模型,而分治算法解決的問題儘管大部分也是最優解問題,但是,大部分都不能抽象成多階段決策模型。

4 什麼時候使用動態規劃

4.1 適用場景:求解最優問題

動態規劃比較適合用來求解最優問題,比如求最大值、最小值等。

例如,在編程中常用解決最長公共子序列問題、矩陣連乘問題、凸多邊形最優三角剖分問題、電路佈線等問題。

4.2 適用條件:要符合“一個模型三個特徵”

1、模型:多階段決策最優解模型

2、“三個特徵”分別是最優子結構、無後效性和重複子問題

(1)最優子結構:後面階段的狀態可以通過前面階段的狀態推導出來。

(2)無後效性:有兩層含義,第一層含義是,在推導後面階段的狀態的時候,我們只關心前面階段的狀態值,不關心這個狀態是怎麼一步一步推導出來的。第二層含義是,某階段狀態一旦確定,就不受之後階段的決策影響。

(3)重複子問題:使用指數級時間複雜度的搜索算法時,不同的決策序列,到達某個相同的階段時,可能會產生重複的狀態。動態規劃算法的根本目的在於解決計算的冗餘。

動態規劃實質上是一種以空間換時間的技術,它在實現的過程中,存儲產生過程中的各種狀態,從而減少冗餘計算,提高執行效率,降低時間時間複雜度。但也因此,其空間複雜度要大於其它的算法。

5 動態規劃解題的一般思路(摘錄自極客時間)

5.1 狀態轉移表法

先畫出一個狀態表。狀態表一般都是二維的,所以你可以把它想象成二維數組。

其中,每個狀態包含三個變量,行、列、數組值。

我們根據決策的先後過程,從前往後,根據遞推關係,分階段填充狀態表中的每個狀態。

最後,我們將這個遞推填表的過程,翻譯成代碼,就是動態規劃代碼了。

狀態轉移表法解題思路大致可以概括爲,回溯算法實現 - 定義狀態 - 畫遞歸樹 - 找重複子問題 - 畫狀態轉移表 - 根據遞推關係填表 -將填表過程翻譯成代碼。

5.2 狀態轉移方程法

狀態轉移方程法有點類似遞歸的解題思路。

我們需要分析,某個問題如何通過子問題來遞歸求解,也就是所謂的最優子結構。

根據最優子結構,寫出遞歸公式,也就是所謂的狀態轉移方程。有了狀態轉移方程,代碼實現就非常簡單了。

一般情況下,我們有兩種代碼實現方法,一種是遞歸加“備忘錄”,另一種是迭代遞推。狀態轉移方程是解決動態規劃的關鍵。

狀態轉移方程法的大致思路可以概括:找最優子結構 - 寫狀態轉移方程 - 將狀態轉移方程翻譯成代碼。

6 案例實踐+思路理論

以找零錢的問題問案例,闡述解決問題的思考歷程。

6.1 題目(找零錢的問題爲例)

假設你是一名超市收銀員,現有n種不同面值的貨幣,每種面值的貨幣可以使用任意張。
顧客結賬時,你需要找給顧客aim元零錢,你可以給出多少種方法。

例如,有1、2、3元三種面值的貨幣,你需要找零3元,那麼共有3種方法:1張1元+1張2元、3張1元、1張3元。

6.2 解題思路

第一步,遇到問題,首先要理解題意,提取關鍵的信息,抽象問題。

本案例中,關鍵信息爲:

貨幣面值:int[] penny = {1,2,3};

找零數:假設aim=8;

問題可以抽象爲:使用數組{1,2,3}中的3個數,有多少個組合可以求和成aim=8?

第二步,接着,先畫出一個狀態表,尋找規律。狀態表一般是一個二維數組。

其中,數組中的每一個格子表示一個狀態,每個狀態包含三個變量,行、列、數組值,每個變量可以表示題中實際含義。

如本案例中,設狀態表數組爲dp[i][j] = value,則dp中的
i指使用前i個數,
j表示當前需要找的零錢總數,
dp[i][j]的值表示使用前i個數組成之和爲j時的組合數量。

這樣,i不斷增加,j也不斷增加,從小問題開始慢慢地求解,當i和j增大都指定的值,此時得到的就是需要求的解。
本案例中即使用3種面值的貨幣,找零數爲8,則i從1增加到3,j從0增加到8,此時得到的狀態表數組,就是需要求的解。

狀態表如下:

0 1 2 3 4 5 6 7 8
1 1 1 1 1 1 1 1 1 1
2 1 1 2 2 3 3 4 4 5
3 1 1 2 2 4 4 6 6 9

第三步,分析一下這個問題是否符合“一個模型和三個特徵”。

一個模型:這個問題可以按照使用前幾個數,分成多個階段去解決,即階段一爲只使用{1},階段爲使用{1,2},階段三爲使用{1,2,3}。

最優子結構:然後每個階段的每個節點的狀態都可根據前一個階段的節點或者本階段前面的節點推導出來。即能夠寫出最優子結構。在本案例中,在第3個階段的第6個節點(3,6)可以根據第三個階段的第3個節點(3,3)和第二個階段的第6個節點(2,6)推導出,即(3,6)的值 = (3,3)的值 + (2,6)的值。綜合其它節點,用狀態轉移方程表示爲:

if (j < penny[i]) {
    resultTable[i][j] = resultTable[i - 1][j];
} else {
    resultTable[i][j] = resultTable[i - 1][j] + resultTable[i][j - penny[i]];
}

無後效性:前面階段的任一節點的值一旦確認,後面無論怎麼改動都不會變化。

重複子問題:如果使用回溯算法,會產生很多重複計算的子問題。而動態規劃能夠避免再重複計算這些子問題。

第四步,將狀態表的生成過程翻譯成代碼。這裏就是具體的編碼實現了。代碼如下:

import java.util.Arrays;

/**
 * 假設你是一名超市收銀員,現有n種不同面值的貨幣,每種面值的貨幣可以使用任意張。
 * 顧客結賬時,你需要找給顧客aim元零錢,你可以給出多少種方法。
 * 例如,有1、2、3元三種面值的貨幣,你需要找零3元,那麼共有3種方法:1張1元+1張2元、3張1元、1張3元。
 */
public class Main {

    public static void main(String[] args) {
        int[] penny = {1, 2, 4};
        int size = 3;
        int aim = 8;
        System.out.println("可組合的方法數量爲:"+f(penny, size, aim));
    }

    /**
     * @param penny 每個值代表一種面值的貨幣
     * @param size  數組penny及它的大小
     * @param aim   要找的錢數
     * @return
     */
    public static int f(int[] penny, int size, int aim) throws RuntimeException {

        // 校驗
        if (penny.length > 50 || size > 50) {
            throw new RuntimeException("penny的元素個數或者size不能大於50");
        }

        // 校驗
        if (aim > 1000) {
            throw new RuntimeException("aim不能大於1000");
        }

        int[][] resultTable = new int[size][aim + 1];

        // 第一行特殊處理
        for (int i = 0; i < aim + 1; i++) {
            resultTable[0][i] = i % penny[0] == 0 ? 1 : 0;
        }
        
        // 動態規劃
        for (int i = 1; i < penny.length; i++) {

            for (int j = 0; j <= aim; j++) {

                if (j < penny[i]) {
                    resultTable[i][j] = resultTable[i - 1][j];
                } else {
                    // 例如: dp[1][2] = dp[0][2] + dp[0][2-1*1]),
                    // 代表使用penny前2個元素(即1,2)組成2的方法數 = 不使用2組成2的方法數 + 使用1個2組成2的方法數。
                    resultTable[i][j] = resultTable[i - 1][j] + resultTable[i][j - penny[i]];
                }

            }
        }
        
        // 打印結果,非算法關鍵代碼 start
        for (int[] a : resultTable) {
            System.out.println(Arrays.toString(a));
        }
        // 打印結果,非算法關鍵代碼 end
        
        return resultTable[penny.length - 1][aim];
    }
}

測試案例結果:

[1, 1, 1, 1, 1, 1, 1, 1, 1]
[1, 1, 2, 2, 3, 3, 4, 4, 5]
[1, 1, 2, 2, 4, 4, 6, 6, 9]
可組合的方法數量爲:9

6.3 複雜度分析

空間複雜度:需要使用數組大小爲3*(8+1),抽象成字母爲:n*w。
時間複雜度:O(n*w)。for循環嵌套,一共需要比較n*w次。

7 實戰

7.1 揹包問題

題目

對於一組不同重量、不可分割的物品,我們需要選擇一些裝入揹包,
在滿足揹包最大重量限制的前提下,揹包中物品總重量的最大值是多少呢?

解題思路

三種方法:回溯、遞歸+“備忘錄”、動態規劃。這裏主要介紹動態規劃算法。

第一步,首先要理解題意,提取關鍵的信息,抽象問題,假設一些測試數據,將其轉爲數學問題。本案例中,關鍵信息爲:

物品重量:int[] weight = {2,2,4,6,3} ;

揹包最大重量:int w = 9 ;

問題可以抽象爲:使用數組{2,2,4,6,3}中的數,取其中某幾個數,組合求和,在和不超過9的情況下,組合求和的最大值爲多少?

第二步,接着,先畫出一個狀態表,尋找規律。狀態表一般是一個二維數組。

0 1 2 3 4 5 6 7 8 9
1 true false true false false false false false false false
2 true false true false true false false false false false
3 true false true false true false true false true false
4 true false true false true false true false true false
5 true false true true true true true true true true
如本案例中,設狀態表數組爲dp[i][j] = boolean,則的dp中的
i指使用前i個數,
j表示使用前i個數時,能否能組合求和成的數。
dp[i][j]表示使用前i個數,能否能組合求和成j。如果可以,則爲true,否則爲false。

這樣,i不斷增加,j也增加,從小問題開始慢慢地求解,當i和j增大都指定的值,
本案例中即使用5個物品,揹包重量爲9,則i從0增加到5,j從0增加到9,此時得到的狀態表數組,就是需要求的解。

第三步,分析一下這個問題是否符合“一個模型和三個特徵”。

一個模型:這個問題可以按照前幾個物品,分成多個階段去解決,即階段一爲只使用{2},階段爲使用{2,2},階段三爲使用{2,2,4},階段四爲使用{2,2,4,6},階段五爲使用{2,2,4,6,3}。

最優子結構:然後每個階段的每個節點的狀態都可根據前一個階段的節點或者本階段前面的節點推導出來。即能夠寫出最優子結構。本案例中,第2個階段的第2個節點(2,2)可以根據第1個階段的第2個節點(1,2)推導出,(2,2)的值 = (2,4)的值 = (1,2)的值。綜合其它節點,用狀態轉移方程表示爲:

// 不把第i個物品放入揹包
for (int j = 0; j <= w; j++) {
    if (states[i - 1][j] == true) {
        states[i][j] = states[i - 1][j];
    }
}
// 把第i個物品放入揹包
for (int j = 0; j <= w - weight[i]; j++) {
    if (states[i - 1][j] == true) {
        states[i][j + weight[i]] = true;
    }
}

無後效性:前面階段的任一節點的值一旦確認,後面無論怎麼改動都不會變化。

重複子問題:如果使用回溯算法,會產生很多重複計算的子問題。而動態規劃能夠避免再重複計算這些子問題。

第四步,將狀態表的生成過程翻譯成代碼。這裏就是具體的編碼實現了。代碼如下:

/**
 * 對於一組不同重量、不可分割的物品,我們需要選擇一些裝入揹包,
 * 在滿足揹包最大重量限制的前提下,揹包中物品總重量的最大值是多少呢?
 */
public class Main3 {

    public static void main(String[] args) {

        // 物品重量
        int[] weight = {2, 2, 4, 6, 3};
        // 物品個數
        int n = 5;
        // 揹包承受的最大重量
        int w = 9;
        System.out.println(knapsack(weight, n, w));

    }


    /**
     * @param weight 物品重量
     * @param n      物品個數
     * @param w      揹包可承載重量
     * @return
     */
    public static int knapsack(int[] weight, int n, int w) {

        /**
         * i  : 使用前i個物品
         * j  :使用前i個物品是否可裝的數量
         * states[i][j]  = true   : 可以裝
         * states[i][j]  = false  : 不可以裝
         */
        // 默認值false
        boolean[][] states = new boolean[n][w + 1];

        // 第一行數據要特殊處理,可以利用哨兵優化
        states[0][0] = true;
        if (weight[0] <= w) {
            states[0][weight[0]] = true;
        }

        // 動態規劃狀態轉移
        for (int i = 1; i < n; i++) {
            // 不把第i個物品放入揹包
            for (int j = 0; j <= w; j++) {
                if (states[i - 1][j] == true) {
                    states[i][j] = states[i - 1][j];
                }
            }
            // 把第i個物品放入揹包
            for (int j = 0; j <= w - weight[i]; j++) {
                if (states[i - 1][j] == true) {
                    states[i][j + weight[i]] = true;
                }
            }

        }

        print(n,w,states);


        // 從最後一行的最後一個開始找,第一個爲true的爲結果
        for (int i = w; i >= 0; i--) {
            if (states[n - 1][i] == true) {
                return i;
            }
        }


        return 0;
    }

    private static void print(int n,int w,boolean[][] states){
        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                if (states[i][j]){
                    System.out.print("i=" + i + " " + "j=" + j + " " + states[i][j]+" ");
                }else {
                    System.out.print("i=" + i + " " + "j=" + j + " " + states[i][j]);
                }
                System.out.print(" | ");
            }
            System.out.println();
        }
    }

}

測試案例結果:

true | false | true | false | false | false | false | false | false | false | 
true | false | true | false | true | false | false | false | false | false | 
true | false | true | false | true | false | true | false | true | false | 
true | false | true | false | true | false | true | false | true | false | 
true | false | true | true | true | true | true | true | true | true | 
9

優化:上面的方法需要用到二維數組,可以只使用一維數組,減少內存使用,降低空間複雜度。
注意到,二位數組中,只有最有一行是有用的數據。故可以用一個一維數組,保存每個階段的結果即可。

/**
 * 對於一組不同重量、不可分割的物品,我們需要選擇一些裝入揹包,
 * 在滿足揹包最大重量限制的前提下,揹包中物品總重量的最大值是多少呢?
 */
public class Main4 {

    public static void main(String[] args) {

        // 物品重量
        int[] weight = {2, 2, 4, 6, 3};
        // 物品個數
        int n = 5;
        // 揹包承受的最大重量
        int w = 9;
        System.out.println(knapsack(weight, n, w));

    }


    /**
     * @param weight 物品重量
     * @param n      物品個數
     * @param w      揹包可承載重量
     * @return
     */
    public static int knapsack(int[] weight, int n, int w) {

        /**
         * i  : 使用前i個物品
         * j  :使用前i個物品是否可裝的數量
         * states[i][j]  = true   : 可以裝
         * states[i][j]  = false  : 不可以裝
         */
        // 默認值false
        boolean[] states = new boolean[w + 1];

        // 第一行數據要特殊處理,可以利用哨兵優化
        states[0] = true;
        if (weight[0] <= w) {
            states[weight[0]] = true;
        }

        print(w, states);
        // 動態規劃狀態轉移
        for (int i = 1; i < n; i++) {
            // 把第i個物品放入揹包
            for (int j = w - weight[i]; j >= 0; j--) {
                if (states[j] == true) {
                    states[j + weight[i]] = true;
                    //System.out.println();
                }
            }
            print(w, states);
        }




        // 從最後一行的最後一個開始找,第一個爲true的爲結果
        for (int i = w; i >= 0; i--) {
            if (states[i] == true) {
                return i;
            }
        }


        return 0;
    }

    private static void print(int w, boolean[] states) {
        for (int j = 0; j <= w; j++) {
            if (states[j]) {
                System.out.print("j=" + j + " " + states[j] + " ");
            } else {
                System.out.print("j=" + j + " " + states[j]);
            }
            System.out.print(" | ");
        }
        System.out.println();
    }

}

測試案例結果:

true | false | true | false | false | false | false | false | false | false | 
true | false | true | false | true | false | false | false | false | false | 
true | false | true | false | true | false | true | false | true | false | 
true | false | true | false | true | false | true | false | true | false | 
true | false | true | true | true | true | true | true | true | true | 
9

7.2 揹包問題升級版

題目

對於一組不同重量、不同價值、不可分割的物品,我們選擇將某些物品裝入揹包,
在滿足揹包最大重量限制的前提下,揹包中可裝入物品的總價值最大是多少呢?

解題思路

兩種方法:回溯、動態規劃。這裏主要介紹動態規劃算法。

第一步,首先要理解題意,提取關鍵的信息,抽象問題,假設一些測試數據,將其轉爲數學問題。本案例中,關鍵信息爲:

物品重量:int[] weight = {2, 2, 4, 6, 3};

物品價值:int[] value = {3, 4, 8, 9, 6};

揹包最大重量:int w = 9 ;

問題可以抽象爲:使用數組{2,2,4,6,3}中的數,取其中某幾個數,組合求和即重量不超過9,其對應的價值之和的最大值爲多少?

第二步,接着,先畫出一個狀態表,尋找規律。狀態表一般是一個二維數組。

0 1 2 3 4 5 6 7 8 9
0 0 0 3 0 0 0 0 0 0 0
1 0 0 4 4 7 4 4 4 4 4
2 0 0 4 4 8 8 12 12 15 12
3 0 0 4 4 8 8 12 12 15 13
4 0 0 4 6 8 10 12 14 15 18

第三步,分析一下這個問題是否符合“一個模型和三個特徵”。

第四步,將狀態表的生成過程翻譯成代碼。這裏就是具體的編碼實現了。代碼如下:

public class Main2 {

    public static void main(String[] args) {

        int[] weight = {2, 2, 4, 6, 3};
        int[] value = {3, 4, 8, 9, 6};
        int n = 5;
        int w = 9;

        System.out.println(knapsack3(weight, value, n, w));
    }

    public static int knapsack3(int[] weight, int[] value, int n, int w) {
        int[][] states = new int[n][w + 1];
        // 初始化states
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < w + 1; j++) {
                states[i][j] = 0;
            }
        }

        states[0][0] = 0;
        if (weight[0] <= w) {
            states[0][weight[0]] = value[0];
        }

        // 動態規劃狀態轉移
        for (int i = 1; i < n; i++) {
            // 不選擇第i個物品
            for (int j = 0; j <= w; j++) {
                if (states[i - 1][j] >= 0) {
                    states[i][j] = states[i - 1][j];
                }
            }
            // 選擇第i個物品
            for (int j = 0; j <= w - weight[i]; j++) {
                if (states[i - 1][j] >= 0) {
                    int v = states[i - 1][j] + value[i];
                    if (v > states[i][j + weight[i]]) {
                        states[i][j + weight[i]] = v;
                    }
                }
            }
        }

        // 找出最大值
        int maxvalue = -1;
        for (int j = 0; j <= w; j++) {
            if (states[n - 1][j] > maxvalue) {
                maxvalue = states[n - 1][j];
            }
        }

        print(n, w, states);

        return maxvalue;
    }

    private static void print(int n, int w, int[][] states) {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                //System.out.print("i=" + i + " " + "j=" + j + " " + states[i][j]);
                System.out.print(states[i][j]);
                System.out.print(" | ");
            }
            System.out.println();
        }
    }

}

測試案例結果:

0 | 0 | 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 
0 | 0 | 4 | 4 | 7 | 4 | 4 | 4 | 4 | 4 | 
0 | 0 | 4 | 4 | 8 | 8 | 12 | 12 | 15 | 12 | 
0 | 0 | 4 | 4 | 8 | 8 | 12 | 12 | 15 | 13 | 
0 | 0 | 4 | 6 | 8 | 10 | 12 | 14 | 15 | 18 | 
18

7.3 國王和金礦

題目

有一個國家發現了5座金礦,每座金礦的黃金儲量不同,需要參與挖掘的工人數也不同,具體見圖。參與挖礦工人的總數是10人。每座金礦要麼全挖,要麼不挖,不能派出一半人挖取一半金礦,每個人也只會最多挖一次礦。要求用程序求解出,要想得到儘可能多的黃金,應該選擇挖取哪幾座金礦?

金礦列表:
500金/5人
400金/5人
350金/3人
300金/4人
200金/3人

解題思路

第一步,首先要理解題意,提取關鍵的信息,抽象問題,假設一些測試數據,將其轉爲數學問題。本案例中,關鍵信息爲:

問題可以抽象爲:

第二步,接着,先畫出一個狀態表,尋找規律。狀態表一般是一個二維數組。

第三步,分析一下這個問題是否符合“一個模型和三個特徵”。

第四步,將狀態表的生成過程翻譯成代碼。這裏就是具體的編碼實現了。代碼如下:

import java.util.Arrays;

public class Main {

	/**
	 * @param worker 工人數量,如:10
	 * @param gold 每個金礦開採所需的工人數量,如:{ 500, 400, 350, 300, 200 }
	 * @param p 每個金礦儲量,如:{ 5, 5, 3, 4, 3 }
	 * @return
	 */
	public static int getBestGoldMiningV2(int worker, int[] gold, int[] p) {
		// 校驗入參
		//
		if (worker == 0 || gold.length == 0) {
			return 0;
		}
        // 前i個金礦,j個人時的最大收益
		int[][] reslutTable = new int[gold.length + 1][worker + 1];
		// 前i個金礦
		for (int i = 1; i <= gold.length; i++) {
			// 有j個工人時
			for (int j = 1; j <= worker; j++) {
				if (j < p[i - 1]) {
					// 如果當前工人數量<當前金礦開採所需的工人數量,則當前最大收益爲上一個金礦同樣工人數量的收益
					reslutTable[i][j] = reslutTable[i - 1][j];
				} else {
					// 如果當前工人數量>=當前金礦開採所需的工人數量,則當前最大收益爲以下兩者的最大值:
					// (1)上一個金礦同樣工人數量的收益 
					// (2)上一個金礦,減去本礦工人數量收益 + 當前金礦的收益, 
					reslutTable[i][j] = Math.max(reslutTable[i - 1][j],
							reslutTable[i - 1][j - p[i-1]] + gold[i-1]);
				}
			}
		}

		// 打印結果集
		for(int[] a : reslutTable){
			System.out.println(Arrays.toString(a));
		}
		
        // 返回所有金礦,所有工人的時的收益
		return reslutTable[gold.length][worker];
	}
	
	public static int getBestGoldMiningV3(int w,int[] gold,int[] p){
		
		if(w == 0 || gold.length == 0){
			return 0 ;
		}
		// 存儲最後一行的結果集
		int[] results = new int[w+1];
		
		// 前i個金礦
		for(int i = 1;i<gold.length;i++){
			// 有j個工人時
			for(int j = w;j>0;j--){
				if(j>=p[i-1]){
					results[j] = Math.max(results[j], results[j-p[i-1]]+gold[i-1]); 
				}
			}
		}
		// 打印結果集
		System.out.println(Arrays.toString(results));
		return results[w];
	}

	public static void main(String[] args) {

		int worker = 10;
		int[] gold = { 500, 400, 350, 300, 200 };
		int[] p = { 5, 5, 3, 4, 3 };

		int result = getBestGoldMiningV2(worker, gold, p);
		System.out.println("最大收益:" + result);

		// 優化方法,使用一位數組存儲結果集
		int result3 = getBestGoldMiningV3(worker, gold, p);
		System.out.println("最大收益:" + result3);
	}
}

輸出結果:

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 500, 500, 500, 500, 500, 500]
[0, 0, 0, 0, 0, 500, 500, 500, 500, 500, 900]
[0, 0, 0, 350, 350, 500, 500, 500, 850, 850, 900]
[0, 0, 0, 350, 350, 500, 500, 650, 850, 850, 900]
[0, 0, 0, 350, 350, 500, 550, 650, 850, 850, 900]
最大收益:900
[0, 0, 0, 350, 350, 500, 500, 650, 850, 850, 900]
最大收益:900

7.4 矩陣最短路徑

題目

矩陣存儲的都是正整數。棋子起始位置在左上角,終止位置在右下角。我們將棋子從左上角移動到右下角。每次只能向右或者向下移動一位。從左上角到右下角,會有很多不同的路徑可以走。我們把每條路徑經過的數字加起來看作路徑的長度。那從左上角移動到右下角的最短路徑長度是多少呢?

解題思路

第一步,首先要理解題意,提取關鍵的信息,抽象問題,假設一些測試數據,將其轉爲數學問題。本案例中,關鍵信息爲:

問題可以抽象爲:

第二步,接着,先畫出一個狀態表,尋找規律。狀態表一般是一個二維數組。

第三步,分析一下這個問題是否符合“一個模型和三個特徵”。

if (i==1){
    resultTable[i][j] = resultTable[i][j-1]  +  data[i-1][j-1];
}

if (j==1){
    resultTable[i][j] = resultTable[i-1][j] + data[i-1][j-1];
}

if (i > 1 && j>1){
    resultTable[i][j] = Math.min(resultTable[i-1][j]+data[i-1][j-1],
                                 resultTable[i][j-1]+data[i-1][j-1]);
}

第四步,將狀態表的生成過程翻譯成代碼。這裏就是具體的編碼實現了。代碼如下:

public class Main {

    public static void main(String[] args){
        int[][] data = {{1,2,3},{1,1,1},{2,4,2}};
        int n = 3 ;
        int m = 3 ;
        System.out.println(getMinPath(data,n,m));
    }


    public static int getMinPath(int[][] data,int n,int m){

        if (data == null || data.length == 0){
            return 0 ;
        }
        if (n == 0 || m == 0){
            return 0 ;
        }

        int[][] resultTable = new int[n+1][m+1];

        for (int i = 1;i<=n;i++){
            for (int j = 1;j<=m;j++){

                if (i==1){
                    resultTable[i][j] = resultTable[i][j-1]  +  data[i-1][j-1];
                }
                if (j==1){
                    resultTable[i][j] = resultTable[i-1][j] + data[i-1][j-1];
                }

                if (i > 1 && j>1){
                    resultTable[i][j] = Math.min(resultTable[i-1][j]+data[i-1][j-1],
                            resultTable[i][j-1]+data[i-1][j-1]);
                }
            }
        }
        for (int[] a : resultTable){
            System.out.println(Arrays.toString(a));
        }
        return resultTable[n][m];
    }

}

測試案例結果:

[0, 0, 0, 0]
[0, 1, 3, 6]
[0, 2, 3, 4]
[0, 4, 7, 6]
6

類似題目

走方格問題
 * 有一個矩陣map,它每個格子有一個權值。從左上角的格子開始每次只能向右或者向下走,
 * 最後到達右下角的位置,路徑上所有的數字累加起來就是路徑和,返回所有的路徑中最小的路徑和。
 * 給定一個矩陣map及它的行數n和列數m,請返回最小路徑和。保證行列數均小於等於100.
 * 測試樣例:
 * [[1,2,3],[1,1,1]],2,3
 * 返回:4

7.5 斐波那契數列

題目

斐波那契數列:1、1、2、3、5、8、13、21、34、……
遞推的方法定義:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)

解題思路

第一步,首先要理解題意,提取關鍵的信息,抽象問題,假設一些測試數據,將其轉爲數學問題。本案例中,關鍵信息爲:

問題可以抽象爲:

第二步,接着,先畫出一個狀態表,尋找規律。狀態表一般是一個二維數組。

第三步,分析一下這個問題是否符合“一個模型和三個特徵”。

第四步,將狀態表的生成過程翻譯成代碼。這裏就是具體的編碼實現了。代碼如下:

/**
 * 斐波那契數列:1、1、2、3、5、8、13、21、34、……
 * 遞推的方法定義:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)
 * 黃金分割數列、兔子數列
 */
public class Main {

    public static void main(String[] args) throws Exception{

        //System.out.println(f2(-1));
        System.out.println(f2(0));
        System.out.println(f2(1));
        System.out.println(f2(2));
        System.out.println(f2(3));
        System.out.println(f2(4));

        // 結果爲:6765
        // 共花費:0
        long start = System.currentTimeMillis();
        System.out.println(f2(20));
        long end = System.currentTimeMillis();
        System.out.println("計算f2(20)共花費:" + (end - start));

        // 結果爲:512559680
        // 共花費:0
        start = System.currentTimeMillis();
        System.out.println(f2(48));
        end = System.currentTimeMillis();
        System.out.println("計算f2(48)共花費:" + (end - start));

        // 結果爲:-980107325
        // 共花費:0
        start = System.currentTimeMillis();
        System.out.println(f2(100));
        end = System.currentTimeMillis();
        System.out.println("計算f2(100)共花費:" + (end - start));

        // 結果爲:1819143227
        // 共花費:264
        start = System.currentTimeMillis();
        System.out.println(f2(100000000));
        end = System.currentTimeMillis();
        System.out.println("計算f2(100000000)共花費:" + (end - start));

    }

    /**
     * 動態規劃
     *
     * @param n
     * @return
     */
    public static long f2(int n) throws Exception{

        if(n<0){
            throw new Exception("傳入參數不能小於0");
        }

        if (n == 0) {
            return 0;
        }

        if (n == 1) {
            return 1;
        }

        int[] result = new int[n + 1];
        result[0] = 0;
        result[1] = 1;
        for (int i = 2; i <= n; i++) {
            result[i] = result[i - 1] + result[i - 2];
        }
        return result[n];

    }
}

類似題目

黃金分割數列
兔子數列
走臺階問題
有n級臺階,一個人每次上一級或者兩級,問有多少種走完n級臺階的方法。
爲了防止溢出,請將結果Mod 1000000007
給定一個正整數int n,請返回一個數,代表上樓的方式數。保證n小於等於100000。
測試樣例:
1
返回:1
 
解析:這是一個非常經典的爲題,設f(n)爲上n級臺階的方法,要上到n級臺階的最後一步有兩種方式:
從n-1級臺階走一步;從n-1級臺階走兩步,
 
於是就有了這個公式f(n) = f(n-1)+f(n-2);
即變爲斐波那契數列

7.6 萊文斯坦距離

如何量化兩個字符串之間的相似程度呢?有一個非常著名的量化方法,那就是編輯距離(Edit Distance)。包括以下兩種距離:

(1)萊文斯坦距離

(2)最長公共子串長度

這裏先來學習萊文斯坦距離。

題目


解題思路

第一步,首先要理解題意,提取關鍵的信息,抽象問題,假設一些測試數據,將其轉爲數學問題。

本案例中,關鍵信息爲:

問題可以抽象爲:

第二步,接着,先畫出一個狀態表,尋找規律。狀態表一般是一個二維數組。

第三步,分析一下這個問題是否符合“一個模型和三個特徵”。

第四步,將狀態表的生成過程翻譯成代碼。這裏就是具體的編碼實現了。代碼如下:

import java.util.Arrays;

/**
 * @ClassName Main
 * @Description 萊文斯坦距離
 * @Author Dave Ding
 **/
public class Main {


    public static void main(String[] args) {
        char[] a = "mitcmu".toCharArray();
        char[] b = "mtacnu".toCharArray();
        int n = a.length;
        int m = b.length;

        int result = lwstDp(a,n,b,m);

        System.out.println("萊文斯坦距離爲:"+result);
    }

    public static int lwstDp(char[] a, int n, char[] b, int m) {

        int[][] minDist = new int[n][m];

        // 初始化第0行:即a[0] 與 b[0-j]的編輯距離
        for (int j = 0; j < m; j++) {
            if (a[0] == b[j]) {
                // 如果字符數組b後面的某一個字符和數組的第一字符一致,
                // 則意味着數組b的前j個字符都要刪除
                minDist[0][j] = j;
            } else if (j != 0) {
                // 數組b第0個後面的字符都要刪除
                minDist[0][j] = minDist[0][j - 1] + 1;
            } else {
                //
                minDist[0][j] = 1;
            }

        }

        // 初始化第0列,即a[0-i] 與 b[0]的編輯距離
        for (int i = 0; i < n; i++) {
            if (b[0] == a[i]) {
                minDist[i][0] = i;
            } else if (i != 0) {
                minDist[i][0] = minDist[i-1][0] + 1;
            } else {
                minDist[i][0] = 1;
            }
        }

        // 動態規劃
        for (int i = 1; i < n; i++) {
            for (int j = 1; j < m; j++) {

                if (a[i] == b[j]) {
                    minDist[i][j] = min(minDist[i - 1][j] + 1,
                            minDist[i][j - 1] + 1,
                            minDist[i - 1][j - 1]
                    );
                } else {
                    minDist[i][j] = min(minDist[i - 1][j] + 1,
                            minDist[i][j - 1] + 1,
                            minDist[i - 1][j - 1] + 1
                    );
                }

            }
        }

        // 打印,無意義
        for (int[] temp : minDist){
            System.out.println(Arrays.toString(temp));
        }
        
        return minDist[n-1][m-1];
    }

    private static int min(int x, int y, int z) {
        int minv = Integer.MAX_VALUE;
        if (x < minv) {
            minv = x;
        }
        if (y < minv) {
            minv = y;
        }
        if (z < minv) {
            minv = z;
        }
        return minv;
    }
}

輸出結果:

[0, 1, 2, 3, 4, 5]
[1, 1, 2, 3, 4, 5]
[2, 1, 2, 3, 4, 5]
[3, 2, 2, 2, 3, 4]
[4, 3, 3, 3, 3, 4]
[5, 4, 4, 4, 4, 3]
萊文斯坦距離爲:3

7.7 最大公共子串長度

題目

 給定兩個字符串A和B,返回兩個字符串的最長公共子序列的長度。
 例如,A="1A2C3D4B56”,B="B1D23CA45B6A”,”123456"或者"12C4B6"都是最長公共子序列。
 給定兩個字符串A和B,同時給定兩個串的長度n和m,請返回最長公共子序列的長度。
 保證兩串長度均小於等於300。

 測試樣例:
 "1A2C3D4B56",10,"B1D23CA45B6A",12
 返回:6

解題思路

第一步,首先要理解題意,提取關鍵的信息,抽象問題,假設一些測試數據,將其轉爲數學問題。本案例中,關鍵信息爲:

問題可以抽象爲:

第二步,接着,先畫出一個狀態表,尋找規律。狀態表一般是一個二維數組。

第三步,分析一下這個問題是否符合“一個模型和三個特徵”。


第四步,將狀態表的生成過程翻譯成代碼。這裏就是具體的編碼實現了。代碼如下:

import java.util.Arrays;

/**
 * 最長公共子串長度
 */
public class Main2 {

    public static void main(String[] args) {
        char[] a = "mitcmu".toCharArray();
        char[] b = "mtacnu".toCharArray();
        //char[] a = "1A2C3D4B56".toCharArray();
        //char[] b = "B1D23CA45B6A".toCharArray();
        int n = a.length;
        int m = b.length;

        int result = findLCS1(a, n, b, m);

        System.out.println("最長公共子串長度:" + result);

    }

    public static int findLCS1(char[] a, int n, char[] b, int m) {
        int[][] maxlcs = new int[n][m];
        // 初始化第 0 行:a[0-0] 與 b[0-j] 的 maxlcs
        for (int j = 0; j < m; ++j) {
            if (a[0] == b[j]) {
                maxlcs[0][j] = 1;
            } else if (j != 0) {
                maxlcs[0][j] = maxlcs[0][j - 1];
            } else {
                maxlcs[0][j] = 0;
            }
        }
        // 初始化第 0 列:a[0-i] 與 b[0-0] 的 maxlcs
        for (int i = 0; i < n; ++i) {
            if (a[i] == b[0]) {
                maxlcs[i][0] = 1;
            } else if (i != 0) {
                maxlcs[i][0] = maxlcs[i - 1][0];
            } else {
                maxlcs[i][0] = 0;
            }
        }

        // 動態規劃
        for (int i = 1; i < n; ++i) {
            for (int j = 1; j < m; ++j) {
                if (a[i] == b[j]) {
                    maxlcs[i][j] = max(
                            maxlcs[i - 1][j], maxlcs[i][j - 1], maxlcs[i - 1][j - 1] + 1);
                } else {
                    maxlcs[i][j] = max(
                            maxlcs[i - 1][j], maxlcs[i][j - 1], maxlcs[i - 1][j - 1]);
                }
            }
        }

        // 打印,無意義
        for (int[] temp : maxlcs) {
            System.out.println(Arrays.toString(temp));
        }

        return maxlcs[n - 1][m - 1];
    }

    public static int max(int x, int y, int z) {
        int max = x;
        if (y > max) {
            max = y;
        }
        if (z > max) {
            max = z;
        }
        return max;
    }
}

測試案例結果:

[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
[0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2]
[0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2]
[0, 1, 1, 2, 2, 3, 3, 3, 3, 3, 3, 3]
[0, 1, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3]
[0, 1, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3]
[0, 1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 4]
[1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5]
[1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 5, 5]
[1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 6, 6]
最長公共子串長度:6

7.8 “楊輝三角”

題目

每個位置的數字可以隨意填寫,經過某個數字只能到達下面一層相鄰的兩個數字。
假設你站在第一層,往下移動,我們把移動到最底層所經過的所有數字之和,定義爲路徑的長度。
請你編程求出從最高層移動到最底層的最短路徑長度。

解題思路

第一步,首先要理解題意,提取關鍵的信息,抽象問題,假設一些測試數據,將其轉爲數學問題。

這個問題事實上和求矩陣最短路徑是一樣的,只是矩陣最短路徑中矩陣每一行的有效數字是一樣的,而該該題目中每行的有效數組不同,第一行只有1個,第二行2個,第三行3個,依次遞增。所以該問題關鍵的一點是要找到邊界的計算公式。

本案例中,關鍵信息爲:

三角形數組:
int[][] data = {
                {5,0,0,0,0},
                {7,8,0,0,0},
                {2,3,4,0,0},
                {4,9,6,1,0},
                {2,7,9,4,5}
};

問題可以抽象爲:

在矩陣data中找到最短路徑,且遵循以下規則:

第二步,接着,先畫出一個狀態表,尋找規律。狀態表一般是一個二維數組。

第三步,分析一下這個問題是否符合“一個模型和三個特徵”。

狀態轉移方程:

// 如果是第一列
if (j == 0){
    dp[i][j] = dp[i-1][j] + data[i][j];
}

// 如果是每一列的最後一個
if (i>0 && j>0 && i ==j){
    dp[i][j] = dp[i-1][j-1] + data[i][j] ;
}

if (i>0 && j>0 && i!=j){
    dp[i][j] = Math.min(dp[i-1][j-1],dp[i-1][j])+data[i][j] ;
}

第四步,將狀態表的生成過程翻譯成代碼。這裏就是具體的編碼實現了。代碼如下:

public class Main {

    public static void main(String[] args){

        int[][] data = {
                {5,0,0,0,0},
                {7,8,0,0,0},
                {2,3,4,0,0},
                {4,9,6,1,0},
                {2,7,9,4,5}
        };

        int n = 5;
        int m = 5;
        int min = f(data,n,m);
        System.out.println("min="+min);
    }

    public static int f(int[][] data,int n,int m){

        int[][] dp = new int[n][m] ;

        dp[0][0] = data[0][0];

        for (int i = 1;i<n;i++){
            for (int j = 0;j<m;j++){

                // 如果是第一列
                if (j == 0){
                    dp[i][j] = dp[i-1][j] + data[i][j];
                }

                // 如果是每一列的最後一個
                if (i>0 && j>0 && i ==j){
                    dp[i][j] = dp[i-1][j-1] + data[i][j] ;
                }

                if (i>0 && j>0 && i!=j){
                    dp[i][j] = Math.min(dp[i-1][j-1],dp[i-1][j])+data[i][j] ;
                }
            }
        }

        for (int i = 0;i<n;i++){
            for (int j = 0;j<m;j++){
                System.out.print(dp[i][j]+" ");
            }
            System.out.println();
        }

        int min = Integer.MAX_VALUE ;
        for (int a : dp[n-1]){
            if (a <min){
                min  = a ;
            }
        }

        return min ;
    }

}

測試案例結果:

5 0 0 0 0 
12 13 0 0 0 
14 15 17 0 0 
18 23 21 18 0 
20 25 30 22 23 
min=20

8 其它題目(摘錄自百度百科)

動態規劃一般可分爲線性動規,區域動規,樹形動規,揹包動規四類。

舉例:

線性動規:攔截導彈,合唱隊形,挖地雷,建學校,劍客決鬥等;

區域動規:石子合併, 加分二叉樹,統計單詞個數,炮兵佈陣等;

樹形動規:貪喫的九頭龍,二分查找樹,聚會的歡樂,數字三角形等;

揹包問題:01揹包問題,完全揹包問題,分組揹包問題,二維揹包,裝箱問題,擠牛奶(同濟ACM第1132題)等;

應用實例:

最短路徑問題 ,項目管理,網絡流優化等;

POJ動態規劃題目列表

9 參考資料

1、百度百科-動態規劃

2、極客時間

3、《Dynamic Programming》

4、POJ 動態規劃題目列表

5、0009算法筆記——【動態規劃】動態規劃與斐波那契數列問題,最短路徑問題

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