1. 程式人生 > >背包類動態規劃問題

背包類動態規劃問題

增加 source 以及 減法 根據 全部 class 正數 做出

背包動態規劃問題的特征

背包問題具有顯然的拓撲性,因此常被用作動態規劃的入門講解題目.
典型特征是“按某種規則消耗某種有限的資源獲得最大的收益”,那麽顯然可以圍繞這種資源的遞減設計狀態.
(事實上這個定義是寬泛的,只要有一個單調遞減的變量可以用來設計dp順序,就可看作這一類問題).

最簡單的形式

背包容量為M,有N個不同的物品,第i個物品消耗x[i]的容量,對應y[i]的收益,求最大收益

SOLUTION

樸素算法:

枚舉\(2^n\)種方案.之後我們設計算法時一定要想想它和樸素算法比起來怎麽樣,如果比樸素算法還差,或者實現之後比樸素算法優化不多,則這樣的算法是沒什麽意義的.
在考場上經常有人消耗大量時間實現一個得分並不比樸素算法高多少的算法.所以我想強調一下.

動態規劃

我們不妨先根據之前提到的單調遞減量,設計f[i],表示消耗的容量總共為i的時候所能獲得的最大收益是多少.
如何轉移?我們似乎陷入了困境!任何算法都是“一口一口吃飯”,我們一次只能放進去一個物品.對於f[i]這個狀態,我們放進去一個大小為k的物品可以變成f[i+k]這個狀態.如果所有物品都是無限多的,那麽轉移起來當然無所謂,我們從小到大枚舉i,然後再對每個i,枚舉最後放進去(或者接下來放進去的,這對應"填表法"和"刷表法"兩種轉移方式)的物品.這可以得到一個"完全背包問題"的解法,但對我們這個題無法奏效.因為我們不知道某個物體之前是否使用過.那麽我們可以用二進制數字表示集合,用集合表示狀態,f[i][S]表示使用了集合S的物品,消耗了i的容量,所能獲得的最大收益.然後我們發現對於一個集合的物品,其花費之和是確定的,於是我們只需要定義f[S]表示使用S集合中的物品能獲得的最大收益,之前剩余容量遞減的dp順序自然蘊含在集合中物品的不斷增加中.
這算法似乎沒有比樸素算法高明到哪裏去.
仔細考慮我們算法失敗的原因.我們存儲了大量的信息,任何時刻我們都知道當前包裏有哪些物品已經放進去了.但是這似乎...知道的太多了?
再思考一下...我們其實在每個時刻都重新考慮了全部物品.這有必要嗎?對每個物品其實只有“放進來”“不放進來”兩種情況,對吧?任何兩個物品之間,選擇的結果都是獨立的,可以直接加和.
那麽我們完全可以逐個考慮每一個物品是否放進來!
那麽就可以用f[j][i]表示"已經考慮了前j個物品“是否需要放進來,且當前已經使用的總容量為i時可以獲得的最大收益是多少.於是得到了經典的背包問題實現.

實現

我在註釋中做出了一些說明.主要是大家初學動態規劃時經常失誤的點.

f[0][0]=0;
for(int i=1;i<=m;++i)f[0][i]=-INF;//INF表示一個很大的正數,比如const int INF=0x3f3f3f3f;
//關於初始化:要符合狀態的實際意義,這樣你的dp數組求出來的才是你想要的東西.如果全部初始化為0也可以得到正確答案,但狀態的含義有所不同,是什麽呢?請你思考.
for(int j=1;j<=n;++j){//關於循環的嵌套順序:要考慮清楚狀態轉移的順序,這需要你清楚狀態設計的來龍去脈以及狀態轉移的那些彎彎繞.
    for(int i=0;i<x[j];++i)//避免數組越界:尤其在動態轉移中出現下標減法的時候容易出現越界.有時不是會RE而是會WA.
f[j][i]=f[j-1][i]; for(int i=x[j];i<=m;++i) f[j][i]=max(f[j-1][i],f[j-1][i-x[j]]+y[j]);//可以放第j個物品,也可以不放.如果你不是避免STL的強迫癥患者,max函數,min函數建議使用<algorithm>庫. } //為了便於閱讀我刪除了一些花括號.做題中建議加上所有可有可無的花括號.這將便於你在循環中添加一些調試語句或者之前忘記的語句. //請註意數據類型的使用.背包問題同樣可以爆int.但是為了講述方便我默認都是int.實際上,任何問題都可以爆int.

關於滾動數組:n個物體,n輪循環中,每輪循環其實只用到了數組的兩行,那麽我們可以使用一個兩行的數組,f[2][m+1]完成這個dp過程,將占用的空間由(n+1)(m+1)個int變為2*(m+1)個int,在漸進復雜度上幹掉了一個n.來回倒就行了.
代碼長這樣:

f[0][0]=0;for(int i=1;i<=m;++i)f[0][i]=-INF;
int flag=0;
for(int j=1;j<=n;++j,flag^=1){
    for(int i=0;i<x[j];++i)
        f[flag^1][i]=f[flag][i];
    for(int i=x[j];i<=m;++i)
        f[flag^1][i]=max(f[flag][i],f[flag][i-x[i]]+y[i]);
}

由於01背包問題比較特殊,甚至都不需要兩個數組來回倒,一個數組就搞定了.

f[0]=0;
for(int i=1;i<=m;++i)f[i]=-INF;
for(int j=1;j<=n;++j){
    for(int i=m;i>=x[j];--i){//這個循環順序很重要.只有倒著循環它才和我們之前的算法等價.
        f[i]=max(f[i],f[i-x[j]]+y[j]);
    }
}

錯誤的01背包:

f[0]=0;
for(int i=1;i<=m;++i)f[i]=-INF;
for(int j=1;j<=n;++j){
    for(int i=x[j];i<=m;++i){//這個循環順序的含義:每個物品允許使用任意多次.理解了這個東西你就會寫"完全背包"了.
        f[i]=max(f[i],f[i-x[j]]+y[j]);
    }
}

思考:如何輸出01背包最優解的一組方案?如何統計01背包最優解的方案數?

每個物品可用多次的背包問題

每個物品有一個使用次數z[i],拿到包裏的個數可以在0到z[i]之間改變
較好理解的:二進制拆分.
(挖坑待填)
較不好理解:單調隊列優化
(挖坑待填)

背包類動態規劃問題