進階動規之01揹包及其擴展問題

處理動規問題,找到狀態轉移方程是必不可少,而大多數動態規劃問題都可以通過填表的形式找到其狀態轉移方程,其中01揹包及其相關問題更爲突出!

01揹包問題:

有一個揹包,容量爲4榜,現有如下物品

要求:

1.達到的目標爲裝入的揹包的總價值最大,並且重量不超出

2.裝入的物品不能重複

思路:使用動態規劃的思想來解決此問題,既然是要使用動態規劃,那麼首當其衝的一點是找到此問題的狀態轉移方程。此題由於有價值和容量兩個變量,所以我們定義一個二維數組dp來記錄當前的最優解(即使得滿足要求的最大價值)。爲了更容易的找到狀態轉移方程,我們先手填畫表的形式填充dp數組。

通過上述的填表,不難得出右邊的填表規律(狀態轉移方程),有了它從而代碼的實現也就不難了。

public static void main(String[] args) {
        int[] w = {1, 4, 3};//物品的重量
        int[] val = {1500, 3000, 2000};//物品的價值
        int m = 4;//揹包的容量
        int n = val.length;//物品的個數
        int[][] dp = new int[n + 1][m + 1];
        //爲了記錄放入商品的情況,我們定義一個二維數組
        int[][] path = new int[n + 1][m + 1];
        for (int i = 1; i < dp.length; i++) {
            for (int j = 1; j < dp[0].length; j++) {
                if (w[i - 1] > j) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    //dp[i][j] = Math.max(dp[i - 1][j], val[i - 1] + dp[i - 1][j - w[i - 1]]);
                    //爲了記錄商品的存放,不能直接使用狀態轉移方程
                    if (dp[i - 1][j] < val[i - 1] + dp[i - 1][j - w[i - 1]]) {
                        dp[i][j] = val[i - 1] + dp[i - 1][j - w[i - 1]];
                        path[i][j] = 1;
                    } else {
                        dp[i][j] = dp[i - 1][j];
                    }
                }
            }
        }
        System.out.println(dp[n][m]);
        //逆向打印路徑
        int i = path.length - 1;
        int j = path[0].length - 1;
        while (i > 0 && j > 0) {
            if (path[i][j] == 1) {
                System.out.println("第" + (i) + "個商品放入揹包");
                j -= w[i - 1];
            }
            i--;
        }
    }

下面給出兩個01揹包問題的擴展問題。

目標和:

給定一個有n個正整數的數組A和一個整數sum,求選擇數組A中部分數字和爲sum的方案數,當兩種選取方案有一個數字的下標不一樣,我們就認爲是不同的組成方案。

思路: 此題也有兩個變量,一個是sum,一個是數組的內容,所以此處也使用二維數組來記錄當前的最優解,爲了方便尋找狀態轉移方程,我們也採用填表的方式。

(示例數組{5,5,10,2,3},sum=15)

得到了上述的狀態轉移方程,代碼實現也就不難了。

public long combinationSum(int[] nums, int target) {
        long[][] dp = new long[nums.length + 1][target + 1];
        dp[0][0] = 1;
        for (int i = 1; i <= nums.length; i++) {
            for (int j = 0; j <= target; j++) {
                if (j >= nums[i - 1]) {
                    dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i - 1]];
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[nums.length][target];
    }

算法優化: 仔細觀察代碼不難發現,在狀態轉移方程式中,對於i變量而言後者dp有且只與前者dp有關聯,故而想到能否只用一維dp數組來解決此題(即忽略變量i),顯然這種想法是可以實現的。

第一感覺,直接記錄下前一次的數組數據值,方便下一輪循環的直接調用。

public static long combinationSum2(int[] nums, int target) {
        long[] dp = new long[target + 1];
        dp[0] = 1;
        int tmp = nums[0];
        for (int i = 1; i <= nums.length; i++) {
            for (int j = 0; j <= target; j++) {
                if (j >= tmp) {
                    dp[j] += dp[j - tmp];
                }
            }
            System.out.println(Arrays.toString(dp));
            if (i != nums.length) {
                tmp = nums[i];
            }
        }
        return dp[target];
    }

結果如下:
在這裏插入圖片描述
結果不對,我們發現主要錯誤在於從前往後遍歷時會使得部分結果實現累加而產生重複,由此我們便調轉一下思路,反向遍歷!

public static long combinationSum3(int[] nums, int target) {
        long[] dp = new long[target + 1];
        dp[0] = 1;
        for (int num : nums) {
            for (int j = target; j >= num; j--) {
                dp[j] += dp[j - num];
            }
            System.out.println(Arrays.toString(dp));
        }
        return dp[target];
    }

結果如下:
在這裏插入圖片描述
此時結果便於我們最初的預期一模一樣了,並且相比於最開始的方法,其空間複雜度由O(n^2)降低到O(n)。

組合總和:
給定一個由正整數組成且不存在重複數字的數組,找出和爲給定目標正整數的組合的個數,同一個元素可以取多次。

思路一:本題是上篇有關回溯博客中組合總和的一個擴展,所以此題依然可以用回溯來解決,至於具體的回溯思想可以參考上篇的回溯博客,這裏就不過多講解。

private int count = 0;

    public int combinationSum4(int[] nums, int target) {
        Arrays.sort(nums);
        backtrack(nums, target, 0);
        return count;
    }

    private void backtrack(int[] nums, int target, int sum) {
        if (sum == target) {
            count++;
            return;
        }
        for (int num : nums) {
            if (sum + num > target) {
                break;
            }
            sum += num;
            backtrack(nums, target, sum);
            sum -= num;
        }
    }

此方法雖然簡單易懂,但是時間複雜度太高,很多有關遞歸的題目都可以用動態規劃來優化算法,回溯就是藉助遞歸來實現,所以此處依然可以使用動態規劃來優化算法。
思路二:運用動態規劃的思想,由於此題可以重複取得同一元素(即無限揹包),所以當前數組dp的取值就不再受限於已用過的揹包。在這裏我們只需用一維數組dp來記錄在無限揹包中能得到當前值的方案總數,所以每外層更新一次dp就需要內層循環遍歷一次nums,爲了使得狀態轉移方程更凸顯,我們再填表加以演示。
(示例數組{1,2,3},sum=4)
在這裏插入圖片描述
找到了狀態轉移方程,代碼實現也就難度不大了。

public static int combinationSum4(int[] nums, int target) {
        int[] dp = new int[target + 1];
        dp[0] = 1;
        for (int i = 1; i < dp.length; i++) {
            for (int num : nums) {
                if (i - num >= 0) {
                    dp[i] += dp[i - num];
                }
            }
        }
        return dp[target];
    }

01揹包問題是動態規劃裏面有些難度的一類問題,主要是因爲其狀態轉移方程因爲揹包“重量”的原因而使得其前後兩者之間的遞推關係並不連續。
連續示例:dp[i] += dp[i-1])(非連續示例:dp[i] += dp[i-nums[i]]
所以此類問題的狀態轉移方程往往也不容易直接得到,此時填表就是一個不錯的手段。
最後補充一句:動態規劃的思想雖然可以得到滿足條件的組合總數但是無法得到具體的各種方案,所以此時還得需要回溯來得到結果!(不是有了動規,回溯就沒用了,兩種算法思想各有各的特點,各有各的使用場景)

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