【動態規划】01背包問題【續】


說明

這段時間每天加班,確實沒有整塊的時間來寫博客了,一不小心就到周末了,要是不寫篇博客,那就又要鴿了。為了不打臉,還是加班加點的把這篇博客給寫了出來。

20190323214213.png

再說個題外話,最近一直在看一本關於Mysql的掘金小冊,感覺很棒,作者用通俗易懂的語言將Mysql的底層原理進行了介紹,圖文並茂,講解的很深入,可以看出作者應該是花了不少心思,借閱了不少書籍的。據說作者是個95后,為了寫這本小冊子還特意辭了職,簡直優秀!

20190323215229.png

一篇文章大概需要花費40~60分鍾,建議花整塊的時間進行閱讀。

從作者的身上,也看到了一種匠心精神,反觀自己,寫這么水的文章,實在是慚愧。所以決定對文章質量把把關,本着寧缺毋濫的原則來寫作,盡量不浪費大家的時間。

好了,閑話就說到這了,言歸正傳。

上一篇中,我們了解了01背包問題,並用三種方法進行了求解,但其實在最后一種解法上,我們還能再對它的空間復雜度進行優化。

優化過程

已經過去一個星期了,可能一部分人已經忘記了之前的解題思路,所以在這里把之前填表法使用到的圖貼了過來:

這是我們上一篇填表法的最終結果,在這里,聰明的你應該能發現,其實這里大部分的內容都沒有用上,那么讓我們來想想,如何優化一下空間復雜度呢?

再回頭看下之前的遞推關系式:

可以發現,每次求解 KS(i,j)只與KS(i-1,m) {m:1...j} 有關。也就是說,如果我們知道了K(i-1,1...j)就肯定能求出KS(i,j),為了更直觀的理解,再畫一張圖:

下一層只需要根據上一層的結果即可推出答案,舉個栗子,看i=3,j=5時,在求這個子問題的最優解時,根據上述推導公式,KS(3,5) = max{KS(2,5),KS(2,0) + 3} = max{6,3} = 6;如果我們得到了i=2時所有子問題的解,那么就很容易求出i=3時所有子問題的解。

因此,我們可以將求解空間進行優化,將二維數組壓縮成一維數組,此時,裝填轉移方程變為:

KS(j) = max{KS(j),KS(j - wi) + vi}

這里KS(j - wi)就相當於原來的KS(i-1, j - wi)。需要注意的是,由於KS(j)是由它前面的KS(m){m:1..j}推導出來的,所以在第二輪循環掃描的時候應該由后往前進行計算,因為如果由前往后推導的話,前一次循環保存下來的值可能會被修改,從而造成錯誤。

這么說也許還是不太清楚,回頭看上面的圖,我們從i=2推算i=3的子問題的解時,此時一維數組中存放的是{0,0,2,4,4,6,6,6,6,6,6},這是i=2時所有子問題的解,如果我們從前往后推算i=3時的解,比如,我們計算KS(0) = 0,KS(1) = KS(1) = 0 (因為j=1時,裝不下第三個珠寶,第三個珠寶的重量為5),KS(2) = 2,KS(3) = 4,KS(4) = 4, KS(5) = max{KS(5), KS(5-5) + 3} = 6,....,KS(8) = max{KS(8),KS(8 - 5) + 3} = 7。在這里計算KS(8)的時候,我們就把原來KS(8)的內容修改掉了,這樣,我們后續計算就無法找到這個位置的原值(這個栗子沒舉好。。因為后面的計算沒有用到KS(8)= =),也就是上一輪循環中計算出來的值了,所以在遍歷的時候,需要從后往前進行倒序遍歷。

public class Solution{
    int[] vs = {0,2,4,3,7};
    int[] ws = {0,2,3,5,5};
    int[] newResults = new int[11];

    @Test
    public void test() {
        int result = ksp(4,10);
        System.out.println(result);
    }

    private int ksp(int i, int c){
        // 開始填表
        for (int m = 0; m < vs.length; m++){
            int w = ws[m];
            int v = vs[m];
            for (int n = c; n >= w; n--){
                newResults[n] = Math.max(newResults[n] , newResults[n - w] + v);
            }
            // 可以在這里輸出中間結果
            System.out.println(JSON.toJSONString(newResults));
        }
        return newResults[newResults.length - 1];
    }
}

輸出如下:

[0,0,0,0,0,0,0,0,0,0,0]
[0,0,2,2,2,2,2,2,2,2,2]
[0,0,2,4,4,6,6,6,6,6,6]
[0,0,2,4,4,6,6,6,7,7,9]
[0,0,2,4,4,7,7,9,11,11,13]
13

這樣,我們就順利將空間復雜度從O(n*c)優化到了O(c)。當然,空間優化的代價是,我們只能知道最終的結果,但無法再回溯中間的選擇,也就是無法根據最終結果來找到我們要選的物品組合。

關於初始值

01背包問題一般有兩種不同的問法,一種是“恰好裝滿背包”的最優解,要求背包必須裝滿,那么在初始化的時候,除了KS(0)0,其他的KS(j)都應該設置為負無窮大,這樣就可以保證最終得到的KS(c)是恰好裝滿背包的最優解。另一種問法不要求裝滿,而是只希望最終得到的價值盡可能大,那么初始化的時候,應該將KS(0...c)全部設置為0

為什么呢?因為初始化的數組,實際上是在沒有任何物品可以放入背包的情況下的合法狀態。如果要求背包恰好裝滿,那么此時只有容量為0的背包可以在什么都不裝且價值為0的情況下被“恰好裝滿”,其他容量的背包均沒有合法的解,因此屬於未定義的狀態,應該設置為負無窮大。如果背包不需要被裝滿,那么任何容量的背包都有合法解,那就是“什么都不裝”。這個解的價值為0,所以初始狀態的值都是0。

總結

01背包問題可以用自上而下遞歸記憶法求解,也可以用自下而上填表法求解,而后者可以將二維數組的解空間優化成一維數組的解空間,從而實現空間復雜度的優化。

對於01背包問題的兩種不同問法,實際上的區別便是初始值設置不一樣,解題思路是一樣的。

關於01背包問題,介紹到這里就已經全部結束了,希望能對大家有所幫助。如果覺得有收獲,不要吝嗇你的贊哦,也歡迎關注我的公眾號留言交流。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM