動態規劃經典例題-國王的金礦問題

金礦問題

問題概述:
有一位國王擁有5座金礦,每座金礦的黃金儲量不同,
需要參與挖掘的工人人數也不同。例如有的金礦儲量是500kg黃金,需 要5個工人來挖掘;有的金礦儲量是200kg黃金,需要3個工人來挖 掘…… 如果參與挖礦的工人的總數是10。每座金礦要麼全挖,要麼不挖,不能 派出一半人挖取一半的金礦。要求用程序求出,要想得到儘可能多的黃 金,應該選擇挖取哪幾座金礦?
什麼是動態規劃:
動態規劃:將複雜問題簡化爲規模較小的子問題,再從簡單的子問題自底向上一步一步遞推,最終得到複雜問題的最優解
思路:
利用動態規劃思想將大問題拆分爲一個個小問題,之後一步一步解決問題
例:
在這裏插入圖片描述
如此直至人數爲0或者剩餘金礦數爲0,也就是問題的邊界
之後自底向上就可以從小問題的最優解求出整體的最優解,使利益最大化
具體解釋與步驟詳情參考代碼

package algorithm;

/**
 * 動態規劃金礦問題
 * 如何在已有人數和金礦收益情況下獲得最大的淘金收益
 *  動態規劃:將複雜問題簡化爲規模較小的子問題,
 *      再從簡單的子問題自底向上一步一步遞推,最終得到複雜問題的最優解
 *  動態規劃的要點:
 *      確定全局最優解與最優子結構間的關係
 *      確定問題的邊界
 */
public class GoldMining {
    public static void main(String[] args) {
        int w = 10;
        int[] p = {5,5,3,4,3};
        int[] g = {400,500,200,300,350};
        System.out.println("最優收益爲:"+getBestGoldMining(g.length,w,p,g));
        System.out.println("最優收益爲:"+getBestGoldMining2(g.length,w,p,g));
        System.out.println("最優收益爲:"+getBestGoldMining3(g.length,w,p,g));
    }

    /**
     * 此方法採用遞歸方式計算每種最優子結構的收益情況,遞歸到問題的邊界即可用人數爲0,或者剩餘礦數爲0
     * 缺點:
     *  會進行許多的重複計算,每次都分2種最優子結構,時間複雜度爲O(2^n)
     * @param n 總金礦數
     * @param w 總可用人數
     * @param p 每個金礦需要人數
     * @param g 每個金礦的收益
     */
    public static int getBestGoldMining(int n,int w,int[] p,int[] g){
       if(w==0 || n==0){
           return 0;
       }
       //如果當前可用人數不足以挖當前礦,則換個礦,n-1代表當前礦
       if(w<p[n-1]){
           return getBestGoldMining(n-1,w,p,g);
       }
       /**
        * 如果當前礦可以挖,則選取兩個最優子結構(挖當前礦,不挖當前礦)中收益高的方法
        * n-1 代表除去當前礦,去後一個礦查看
        * w-p[n-1] 代表挖當前礦後剩下的人數
        */
        return Math.max(getBestGoldMining(n-1,w,p,g),(getBestGoldMining(n-1,w-p[n-1],p,g)+g[n-1]));
    }

    /**
     *  根據自底向上求解的步驟,採用一張表記錄計算結果,避免重複計算
     *  時間與空間複雜度都是O(n*w)
     * @param n
     * @param w
     * @param p
     * @param g
     */
    public static int getBestGoldMining2(int n,int w,int[] p,int[] g){
        //使用二維數組表示表,縱軸是金礦信息,橫軸是人數信息
        int[][] resultTable = new int[n+1][w+1];
        //填充表格
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= w; j++) {
                //當前人數不足以挖g[i-1]礦,就把獲得收益置爲0
                if(j<p[i-1]){
                    resultTable[i][j] = 0;
                }else{
                    /**
                     * 可以挖當前礦,取挖礦與不挖礦的最優解
                     * resultTable[i][j]就代表當前礦的最大收益 = max(不挖當前礦去查看之後的收益,挖當前礦查看之後的收入並加上當前礦的收入)
                     * 爲什麼resultTable[i][j]後面的卻是resultTable[i-1][j-p[i-1]]+g[i-1]
                     * 因爲當前循環是從1開始的,g中的i-1、p中的i-1都其實代表的是當前礦的信息
                     */
                    resultTable[i][j] = Math.max(resultTable[i-1][j],resultTable[i-1][j-p[i-1]]+g[i-1]);
                }
            }
        }
        //最後一個格子就是最優收益
        return resultTable[g.length][w];
    }
    /**
     * 以上方法的空間複雜度仍有可以優化的餘地
     * 根據Math.max(resultTable[i-1][j],resultTable[i-1][j-p[i-1]]+g[i-1]);我們可以發現我們當前的最優解都是根據上一行推導而來
     * 故我們可以只用一行表格來代替整個表格,可以看出每次都是i-1使用左邊的數據,所以我們從右邊替換數據即可
     * 以下代碼我們相當於明面只保留了人數的橫座標,實際用兩重for循環保證了還是二維的表格,只是節省了空間
     */
    public static int getBestGoldMining3(int n,int w,int[] p,int[] g){
        int[] resultTable = new int[w+1];
        for (int i = 1; i <= n; i++) {
            for (int j = w; j >=1; j--) {
                if(j>=p[i-1]){
                    //左邊的resultTable[j]是i號金礦時的收益,而右邊則相當於是i-1號礦收益因爲還未被覆蓋
                    resultTable[j] = Math.max(resultTable[j],resultTable[j-p[i-1]]+g[i-1]);
                }
            }
        }
        return resultTable[w];
    }
}

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