[轉載學習] 揹包問題九講

揹包問題九講 v1.0

目錄html

第一講 01揹包問題算法

第二講 徹底揹包問題編程

第三講 多重揹包問題數組

第四講 混合三種揹包問題數據結構

第五講 二維費用的揹包問題框架

第六講 分組的揹包問題編輯器

第七講 有依賴的揹包問題函數式編程

第八講 泛化物品函數

第九講 揹包問題問法的變化工具

附:USACO中的揹包問題

前言

本篇文章是我(dd_engi)正在進行中的一個雄心勃勃的寫做計劃的一部分,這個計劃的內容是寫做一份較爲完善的NOIP難度的動態規劃總結,名爲《解動態規劃題的基本思考方式》。如今你看到的是這個寫做計劃最早發佈的一部分。

揹包問題是一個經典的動態規劃模型。它既簡單形象容易理解,又在某種程度上可以揭示動態規劃的本質,故很多教材都把它做爲動態規劃部分的第一道例題,我也將它放在個人寫做計劃的第一部分。

讀本文最重要的是思考。由於個人語言和寫做方式向來不以易於理解爲長,思路也偶有跳躍的地方,後面更有須要大量思考才能理解的比較抽象的內容。更重要的是:不大量思考,絕對不可能學好動態規劃這一信息學奧賽中最精緻的部分。

你如今看到的是本文的1.0正式版。我會長期維護這份文本,把你們的意見和建議融入其中,也會不斷加入我在OI學習以及未來可能的ACM-ICPC的征程中獲得的新的心得。但目前本文尚未一個固定的發佈頁面,想了解本文是否有更新版本發佈,能夠在OIBH論壇中以「揹包問題九講」爲關鍵字搜索貼子,每次比較重大的版本更新都會在這裏發貼公佈。

目錄

第一講 01揹包問題

這是最基本的揹包問題,每一個物品最多隻能放一次。

第二講 徹底揹包問題

第二個基本的揹包問題模型,每種物品能夠放無限屢次。

第三講 多重揹包問題

每種物品有一個固定的次數上限。

第四講 混合三種揹包問題

將前面三種簡單的問題疊加成較複雜的問題。

第五講 二維費用的揹包問題

一個簡單的常見擴展。

第六講 分組的揹包問題

一種題目類型,也是一個有用的模型。後兩節的基礎。

第七講 有依賴的揹包問題

另外一種給物品的選取加上限制的方法。

第八講 泛化物品

我本身關於揹包問題的思考成果,有一點抽象。

第九講 揹包問題問法的變化

試圖舉一反三、觸類旁通。

附:USACO中的揹包問題

給出 USACO Training 上可供練習的揹包問題列表,及簡單的解答。

聯繫方式

若是有任何意見和建議,特別是文章的錯誤和不足,或者但願爲文章添加新的材料,能夠經過http://kontactr.com/user/tianyi/這個網頁聯繫我。

致謝

感謝如下名單:

  • 阿坦
  • jason911
  • donglixp

他們每人都最早指出了本文第一個beta版中的某個並不是可有可無的錯誤。謝謝大家如此仔細地閱讀拙做並彌補個人疏漏。

感謝 XiaQ,它針對本文的第一個beta版發表了用詞嚴厲的六條建議,雖然我只認同並採納了其中的兩條。在全部讀者幾乎一邊倒的讚賞將我包圍的當時,你的貼子是個人一劑清醒劑,讓我能清醒起來並用更嚴厲的眼光審視本身的做品。

固然,還有用各類方式對我表示鼓勵和支持的幾乎沒法計數的同窗。無論是當面讚賞,或是在論壇上回復個人貼子,無論是發來熱情洋溢的郵件,或是在即時聊天的窗口裏豎起大拇指,大家的鼓勵和支持是支撐個人寫做計劃的強大動力,也鞭策着我不斷提升自身水平,謝謝大家!

最後,感謝 Emacs 這一世界最強大的編輯器的全部貢獻者,感謝它的插件 EmacsMuse 的開發者們,本文的全部編輯工做都藉助這兩個卓越的自由軟件完成。謝謝大家——自由軟件社羣——爲社會提供瞭如此有生產力的工具。我深深欽佩大家身上體現出的自由軟件的精神,沒有大家的感召,我不能完成本文。在大家的影響下,採用自由文檔的方式發佈本文檔,也是我對自由社會事業的微薄努力。

 

P01: 01揹包問題

題目

有N件物品和一個容量爲V的揹包。第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可以使價值總和最大。

基本思路

這是最基礎的揹包問題,特色是:每種物品僅有一件,能夠選擇放或不放。

用子問題定義狀態:即f[i][v]表示前i件物品恰放入一個容量爲v的揹包能夠得到的最大價值。則其狀態轉移方程即是:

f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}

這個方程很是重要,基本上全部跟揹包相關的問題的方程都是由它衍生出來的。因此有必要將它詳細解釋一下:「將前i件物品放入容量爲v的揹包中」這個子問題,若只考慮第i件物品的策略(放或不放),那麼就能夠轉化爲一個只牽扯前i-1件物品的問題。若是不放第i件物品,那麼問題就轉化爲「前i-1件物品放入容量爲v的揹包中」,價值爲f[i-1][v];若是放第i件物品,那麼問題就轉化爲「前i-1件物品放入剩下的容量爲v-c[i]的揹包中」,此時能得到的最大價值就是f[i-1][v-c[i]]再加上經過放入第i件物品得到的價值w[i]。

優化空間複雜度

以上方法的時間和空間複雜度均爲O(N*V),其中時間複雜度基本已經不能再優化了,但空間複雜度卻能夠優化到O(V)。

先考慮上面講的基本思路如何實現,確定是有一個主循環i=1..N,每次算出來二維數組f[i][0..V]的全部值。那麼,若是隻用一個數組f[0..V],能不能保證第i次循環結束後f[v]中表示的就是咱們定義的狀態f[i][v]呢?f[i][v]是由f[i-1][v]和f[i-1][v-c[i]]兩個子問題遞推而來,可否保證在推f[i][v]時(也即在第i次主循環中推f[v]時)可以獲得f[i-1][v]和f[i-1][v-c[i]]的值呢?事實上,這要求在每次主循環中咱們以v=V..0的順序推f[v],這樣才能保證推f[v]時f[v-c[i]]保存的是狀態f[i-1][v-c[i]]的值。僞代碼以下:

for i=1..N

    for v=V..0

        f[v]=max{f[v],f[v-c[i]]+w[i]};

其中的f[v]=max{f[v],f[v-c[i]]}一句恰就至關於咱們的轉移方程f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]},由於如今的f[v-c[i]]就至關於原來的f[i-1][v-c[i]]。若是將v的循環順序從上面的逆序改爲順序的話,那麼則成了f[i][v]由f[i][v-c[i]]推知,與本題意不符,但它倒是另外一個重要的揹包問題P02最簡捷的解決方案,故學習只用一維數組解01揹包問題是十分必要的。

事實上,使用一維數組解01揹包的程序在後面會被屢次用到,因此這裏抽象出一個處理一件01揹包中的物品過程,之後的代碼中直接調用不加說明。

過程ZeroOnePack,表示處理一件01揹包中的物品,兩個參數cost、weight分別代表這件物品的費用和價值。

procedure ZeroOnePack(cost,weight)

    for v=V..cost

        f[v]=max{f[v],f[v-cost]+weight}

注意這個過程裏的處理與前面給出的僞代碼有所不一樣。前面的示例程序寫成v=V..0是爲了在程序中體現每一個狀態都按照方程求解了,避免沒必要要的思惟複雜度。而這裏既然已經抽象成看做黑箱的過程了,就能夠加入優化。費用爲cost的物品不會影響狀態f[0..cost-1],這是顯然的。

有了這個過程之後,01揹包問題的僞代碼就能夠這樣寫:

for i=1..N

    ZeroOnePack(c[i],w[i]);

初始化的細節問題

咱們看到的求最優解的揹包問題題目中,事實上有兩種不太相同的問法。有的題目要求「剛好裝滿揹包」時的最優解,有的題目則並無要求必須把揹包裝滿。一種區別這兩種問法的實現方法是在初始化的時候有所不一樣。

若是是第一種問法,要求剛好裝滿揹包,那麼在初始化時除了f[0]爲0其它f[1..V]均設爲-∞,這樣就能夠保證最終獲得的f[N]是一種剛好裝滿揹包的最優解。

若是並無要求必須把揹包裝滿,而是隻但願價格儘可能大,初始化時應該將f[0..V]所有設爲0。

爲何呢?能夠這樣理解:初始化的f數組事實上就是在沒有任何物品能夠放入揹包時的合法狀態。若是要求揹包剛好裝滿,那麼此時只有容量爲0的揹包可能被價值爲0的nothing「剛好裝滿」,其它容量的揹包均沒有合法的解,屬於未定義的狀態,它們的值就都應該是-∞了。若是揹包並不是必須被裝滿,那麼任何容量的揹包都有一個合法解「什麼都不裝」,這個解的價值爲0,因此初始時狀態的值也就所有爲0了。

這個小技巧徹底能夠推廣到其它類型的揹包問題,後面也就再也不對進行狀態轉移以前的初始化進行講解。

小結

01揹包問題是最基本的揹包問題,它包含了揹包問題中設計狀態、方程的最基本思想,另外,別的類型的揹包問題每每也能夠轉換成01揹包問題求解。故必定要仔細體會上面基本思路的得出方法,狀態轉移方程的意義,以及最後怎樣優化的空間複雜度。

首頁

 

P02: 徹底揹包問題

題目

有N種物品和一個容量爲V的揹包,每種物品都有無限件可用。第i種物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可以使這些物品的費用總和不超過揹包容量,且價值總和最大。

基本思路

這個問題很是相似於01揹包問題,所不一樣的是每種物品有無限件。也就是從每種物品的角度考慮,與它相關的策略已並不是取或不取兩種,而是有取0件、取1件、取2件……等不少種。若是仍然按照解01揹包時的思路,令f[i][v]表示前i種物品恰放入一個容量爲v的揹包的最大權值。仍然能夠按照每種物品不一樣的策略寫出狀態轉移方程,像這樣:

f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v}

這跟01揹包問題同樣有O(N*V)個狀態須要求解,但求解每一個狀態的時間已經不是常數了,求解狀態f[i][v]的時間是O(v/c[i]),總的複雜度是超過O(VN)的。

將01揹包問題的基本思路加以改進,獲得了這樣一個清晰的方法。這說明01揹包問題的方程的確是很重要,能夠推及其它類型的揹包問題。但咱們仍是試圖改進這個複雜度。

一個簡單有效的優化

徹底揹包問題有一個很簡單有效的優化,是這樣的:若兩件物品i、j知足c[i]<=c[j]且w[i]>=w[j],則將物品j去掉,不用考慮。這個優化的正確性顯然:任何狀況下均可將價值小費用高得j換成物美價廉的i,獲得至少不會更差的方案。對於隨機生成的數據,這個方法每每會大大減小物品的件數,從而加快速度。然而這個並不能改善最壞狀況的複雜度,由於有可能特別設計的數據能夠一件物品也去不掉。

這個優化能夠簡單的O(N^2)地實現,通常均可以承受。另外,針對揹包問題而言,比較不錯的一種方法是:首先將費用大於V的物品去掉,而後使用相似計數排序的作法,計算出費用相同的物品中價值最高的是哪一個,能夠O(V+N)地完成這個優化。這個不過重要的過程就不給出僞代碼了,但願你能獨立思考寫出僞代碼或程序。

轉化爲01揹包問題求解

既然01揹包問題是最基本的揹包問題,那麼咱們能夠考慮把徹底揹包問題轉化爲01揹包問題來解。最簡單的想法是,考慮到第i種物品最多選V/c[i]件,因而能夠把第i種物品轉化爲V/c[i]件費用及價值均不變的物品,而後求解這個01揹包問題。這樣徹底沒有改進基本思路的時間複雜度,但這畢竟給了咱們將徹底揹包問題轉化爲01揹包問題的思路:將一種物品拆成多件物品。

更高效的轉化方法是:把第i種物品拆成費用爲c[i]*2^k、價值爲w[i]*2^k的若干件物品,其中k知足c[i]*2^k<=V。這是二進制的思想,由於無論最優策略選幾件第i種物品,總能夠表示成若干個2^k件物品的和。這樣把每種物品拆成O(log(V/c[i]))件物品,是一個很大的改進。

但咱們有更優的O(VN)的算法。

O(VN)的算法

這個算法使用一維數組,先看僞代碼:

for i=1..N

    for v=0..V

        f[v]=max{f[v],f[v-cost]+weight}

你會發現,這個僞代碼與P01的僞代碼只有v的循環次序不一樣而已。爲何這樣一改就可行呢?首先想一想爲何P01中要按照v=V..0的逆序來循環。這是由於要保證第i次循環中的狀態f[i][v]是由狀態f[i-1][v-c[i]]遞推而來。換句話說,這正是爲了保證每件物品只選一次,保證在考慮「選入第i件物品」這件策略時,依據的是一個絕無已經選入第i件物品的子結果f[i-1][v-c[i]]。而如今徹底揹包的特色恰是每種物品可選無限件,因此在考慮「加選一件第i種物品」這種策略時,卻正須要一個可能已選入第i種物品的子結果f[i][v-c[i]],因此就能夠而且必須採用v=0..V的順序循環。這就是這個簡單的程序爲什麼成立的道理。

這個算法也能夠以另外的思路得出。例如,基本思路中的狀態轉移方程能夠等價地變造成這種形式:

f[i][v]=max{f[i-1][v],f[i][v-c[i]]+w[i]}

將這個方程用一維數組實現,便獲得了上面的僞代碼。

最後抽象出處理一件徹底揹包類物品的過程僞代碼,之後會用到:

procedure CompletePack(cost,weight)

    for v=cost..V

        f[v]=max{f[v],f[v-cost]+weight}

總結

徹底揹包問題也是一個至關基礎的揹包問題,它有兩個狀態轉移方程,分別在「基本思路」以及「O(VN)的算法「的小節中給出。但願你可以對這兩個狀態轉移方程都仔細地體會,不只記住,也要弄明白它們是怎麼得出來的,最好可以本身想一種獲得這些方程的方法。事實上,對每一道動態規劃題目都思考其方程的意義以及如何得來,是加深對動態規劃的理解、提升動態規劃功力的好方法。

首頁

 

P03: 多重揹包問題

題目

有N種物品和一個容量爲V的揹包。第i種物品最多有n[i]件可用,每件費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可以使這些物品的費用總和不超過揹包容量,且價值總和最大。

基本算法

這題目和徹底揹包問題很相似。基本的方程只需將徹底揹包問題的方程略微一改便可,由於對於第i種物品有n[i]+1種策略:取0件,取1件……取n[i]件。令f[i][v]表示前i種物品恰放入一個容量爲v的揹包的最大權值,則有狀態轉移方程:

f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k<=n[i]}

複雜度是O(V*Σn[i])。

轉化爲01揹包問題

另外一種好想好寫的基本方法是轉化爲01揹包求解:把第i種物品換成n[i]件01揹包中的物品,則獲得了物品數爲Σn[i]的01揹包問題,直接求解,複雜度仍然是O(V*Σn[i])。

可是咱們指望將它轉化爲01揹包問題以後可以像徹底揹包同樣下降複雜度。仍然考慮二進制的思想,咱們考慮把第i種物品換成若干件物品,使得原問題中第i種物品可取的每種策略——取0..n[i]件——均能等價於取若干件代換之後的物品。另外,取超過n[i]件的策略必不能出現。

方法是:將第i種物品分紅若干件物品,其中每件物品有一個係數,這件物品的費用和價值均是原來的費用和價值乘以這個係數。使這些係數分別爲1,2,4,...,2^(k-1),n[i]-2^k+1,且k是知足n[i]-2^k+1>0的最大整數。例如,若是n[i]爲13,就將這種物品分紅係數分別爲1,2,4,6的四件物品。

分紅的這幾件物品的係數和爲n[i],代表不可能取多於n[i]件的第i種物品。另外這種方法也能保證對於0..n[i]間的每個整數,都可以用若干個係數的和表示,這個證實能夠分0..2^k-1和2^k..n[i]兩段來分別討論得出,並不難,但願你本身思考嘗試一下。

這樣就將第i種物品分紅了O(log n[i])種物品,將原問題轉化爲了複雜度爲O(V*Σlog n[i])的01揹包問題,是很大的改進。

下面給出O(log amount)時間處理一件多重揹包中物品的過程,其中amount表示物品的數量:

procedure MultiplePack(cost,weight,amount)

if cost*amount>=V

{

        CompletePack(cost,weight)

        Return

}

    integer k=1

while k<amount

{

        ZeroOnePack(k*cost,k*weight)

        amount=amount-k

        k=k*2

    }

    ZeroOnePack(amount*cost,amount*weight)

但願你仔細體會這個僞代碼,若是不太理解的話,不妨翻譯成程序代碼之後,單步執行幾回,或者頭腦加紙筆模擬一下,也許就會慢慢理解了。

O(VN)的算法

多重揹包問題一樣有O(VN)的算法。這個算法基於基本算法的狀態轉移方程,但應用單調隊列的方法使每一個狀態的值能夠以均攤O(1)的時間求解。因爲用單調隊列優化的DP已超出了NOIP的範圍,故本文再也不展開講解。我最初瞭解到這個方法是在樓天成的「男人八題」幻燈片上。

小結

這裏咱們看到了將一個算法的複雜度由O(V*Σn[i])改進到O(V*Σlog n[i])的過程,還知道了存在應用超出NOIP範圍的知識的O(VN)算法。但願你特別注意「拆分物品」的思想和方法,本身證實一下它的正確性,並將完整的程序代碼寫出來。

首頁

 

P04: 混合三種揹包問題

問題

若是將P01P02P03混合起來。也就是說,有的物品只能夠取一次(01揹包),有的物品能夠取無限次(徹底揹包),有的物品能夠取的次數有一個上限(多重揹包)。應該怎麼求解呢?

01揹包與徹底揹包的混合

考慮到在P01P02中給出的僞代碼只有一處不一樣,故若是隻有兩類物品:一類物品只能取一次,另外一類物品能夠取無限次,那麼只需在對每一個物品應用轉移方程時,根據物品的類別選用順序或逆序的循環便可,複雜度是O(VN)。僞代碼以下:

for i=1..N

    if 第i件物品是01揹包

        for v=V..0

            f[v]=max{f[v],f[v-c[i]]+w[i]};

    else if 第i件物品是徹底揹包

        for v=0..V

            f[v]=max{f[v],f[v-c[i]]+w[i]};

再加上多重揹包

若是再加上有的物品最多能夠取有限次,那麼原則上也能夠給出O(VN)的解法:遇到多重揹包類型的物品用單調隊列解便可。但若是不考慮超過NOIP範圍的算法的話,用P03中將每一個這類物品分紅O(log n[i])個01揹包的物品的方法也已經很優了。

固然,更清晰的寫法是調用咱們前面給出的三個相關過程。

for i=1..N

    if 第i件物品是01揹包

        ZeroOnePack(c[i],w[i])

    else if 第i件物品是徹底揹包

        CompletePack(c[i],w[i])

    else if 第i件物品是多重揹包

        MultiplePack(c[i],w[i],n[i])

在最初寫出這三個過程的時候,可能徹底沒有想到它們會在這裏混合應用。我想這體現了編程中抽象的威力。若是你一直就是以這種「抽象出過程」的方式寫每一類揹包問題的,也很是清楚它們的實現中細微的不一樣,那麼在遇到混合三種揹包問題的題目時,必定能很快想到上面簡潔的解法,對嗎?

小結

有人說,困難的題目都是由簡單的題目疊加而來的。這句話是否公理暫且存之不論,但它在本講中已經獲得了充分的體現。原本01揹包、徹底揹包、多重揹包都不是什麼難題,但將它們簡單地組合起來之後就獲得了這樣一道必定能嚇倒很多人的題目。但只要基礎紮實,領會三種基本揹包問題的思想,就能夠作到把困難的題目拆分紅簡單的題目來解決。

首頁

 

P05: 二維費用的揹包問題

問題

二維費用的揹包問題是指:對於每件物品,具備兩種不一樣的費用;選擇這件物品必須同時付出這兩種代價;對於每種代價都有一個可付出的最大值(揹包容量)。問怎樣選擇物品能夠獲得最大的價值。設這兩種代價分別爲代價1和代價2,第i件物品所需的兩種代價分別爲a[i]和b[i]。兩種代價可付出的最大值(兩種揹包容量)分別爲V和U。物品的價值爲w[i]。

算法

費用加了一維,只需狀態也加一維便可。設f[i][v][u]表示前i件物品付出兩種代價分別爲v和u時可得到的最大價值。狀態轉移方程就是:

f[i][v][u]=max{f[i-1][v][u],f[i-1][v-a[i]][u-b[i]]+w[i]}

如前述方法,能夠只使用二維的數組:當每件物品只能夠取一次時變量v和u採用逆序的循環,當物品有如徹底揹包問題時採用順序的循環。當物品有如多重揹包問題時拆分物品。這裏就再也不給出僞代碼了,相信有了前面的基礎,你可以本身實現出這個問題的程序。

物品總個數的限制(???)

有時,「二維費用」的條件是以這樣一種隱含的方式給出的:最多隻能取M件物品。這事實上至關於每件物品多了一種「件數」的費用,每一個物品的件數費用均爲1,能夠付出的最大件數費用爲M。換句話說,設f[v][m]表示付出費用v、最多選m件時可獲得的最大價值,則根據物品的類型(0一、徹底、多重)用不一樣的方法循環更新,最後在f[0..V][0..M]範圍內尋找答案。

小結

當發現由熟悉的動態規劃題目變形得來的題目時,在原來的狀態中加一緯以知足新的限制是一種比較通用的方法。但願你能從本講中初步體會到這種方法。

首頁

 

P06: 分組的揹包問題

問題

有N件物品和一個容量爲V的揹包。第i件物品的費用是c[i],價值是w[i]。這些物品被劃分爲若干組,每組中的物品互相沖突,最多選一件。求解將哪些物品裝入揹包可以使這些物品的費用總和不超過揹包容量,且價值總和最大。

算法

這個問題變成了每組物品有若干種策略:是選擇本組的某一件,仍是一件都不選。也就是說設f[k][v]表示前k組物品花費費用v能取得的最大權值,則有:

f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i屬於第k組}

使用一維數組的僞代碼以下:

for 全部的組k

    for v=V..0

        for 全部的i屬於組k

            f[v]=max{f[v],f[v-c[i]]+w[i]}

注意這裏的三層循環的順序,甚至在本文的beta版中我本身都寫錯了。「for v=V..0」這一層循環必須在「for 全部的i屬於組k」以外。這樣才能保證每一組內的物品最多隻有一個會被添加到揹包中。

另外,顯然能夠對每組內的物品應用P02中「一個簡單有效的優化」。

小結

分組的揹包問題將彼此互斥的若干物品稱爲一個組,這創建了一個很好的模型。很多揹包問題的變形均可以轉化爲分組的揹包問題(例如P07),由分組的揹包問題進一步可定義「泛化物品」的概念,十分有利於解題。

首頁

 

P07: 有依賴的揹包問題

簡化的問題

這種揹包問題的物品間存在某種「依賴」的關係。也就是說,i依賴於j,表示若選物品i,則必須選物品j。爲了簡化起見,咱們先設沒有某個物品既依賴於別的物品,又被別的物品所依賴;另外,沒有某件物品同時依賴多件物品。

算法

這個問題由NOIP2006金明的預算方案一題擴展而來。聽從該題的提法,將不依賴於別的物品的物品稱爲「主件」,依賴於某主件的物品稱爲「附件」。由這個問題的簡化條件可知全部的物品由若干主件和依賴於每一個主件的一個附件集合組成。

按照揹包問題的通常思路,僅考慮一個主件和它的附件集合。但是,可用的策略很是多,包括:一個也不選,僅選擇主件,選擇主件後再選擇一個附件,選擇主件後再選擇兩個附件……沒法用狀態轉移方程來表示如此多的策略。(事實上,設有n個附件,則策略有2^n+1個,爲指數級。)

考慮到全部這些策略都是互斥的(也就是說,你只能選擇一種策略),因此一個主件和它的附件集合實際上對應於P06中的一個物品組,每一個選擇了主件又選擇了若干個附件的策略對應於這個物品組中的一個物品,其費用和價值都是這個策略中的物品的值的和。但僅僅是這一步轉化並不能給出一個好的算法,由於物品組中的物品仍是像原問題的策略同樣多。

再考慮P06中的一句話: 能夠對每組中的物品應用P02中「一個簡單有效的優化」。 這提示咱們,對於一個物品組中的物品,全部費用相同的物品只留一個價值最大的,不影響結果。因此,咱們能夠對主件i的「附件集合」先進行一次01揹包,獲得費用依次爲0..V-c[i]全部這些值時相應的最大價值f'[0..V-c[i]]。那麼這個主件及它的附件集合至關於V-c[i]+1個物品的物品組,其中費用爲c[i]+k的物品的價值爲f'[k]+w[i]。也就是說原來指數級的策略中有不少策略都是冗餘的,經過一次01揹包後,將主件i轉化爲V-c[i]+1個物品的物品組,就能夠直接應用P06的算法解決問題了。

較通常的問題

更通常的問題是:依賴關係以圖論中「森林」的形式給出(森林即多叉樹的集合),也就是說,主件的附件仍然能夠具備本身的附件集合,限制只是每一個物品最多隻依賴於一個物品(只有一個主件)且不出現循環依賴。

解決這個問題仍然能夠用將每一個主件及其附件集合轉化爲物品組的方式。惟一不一樣的是,因爲附件可能還有附件,就不能將每一個附件都看做一個通常的01揹包中的物品了。若這個附件也有附件集合,則它一定要被先轉化爲物品組,而後用分組的揹包問題解出主件及其附件集合所對應的附件組中各個費用的附件所對應的價值。

事實上,這是一種樹形DP,其特色是每一個父節點都須要對它的各個兒子的屬性進行一次DP以求得本身的相關屬性。這已經觸及到了「泛化物品」的思想。看完P08後,你會發現這個「依賴關係樹」每個子樹都等價於一件泛化物品,求某節點爲根的子樹對應的泛化物品至關於求其全部兒子的對應的泛化物品之和。

小結

NOIP2006的那道揹包問題我作得很失敗,寫了上百行的代碼,卻一分未得。後來我經過思考發現經過引入「物品組」和「依賴」的概念能夠加深對這題的理解,還能夠解決它的推廣問題。用物品組的思想考慮那題中極其特殊的依賴關係:物品不能既做主件又做附件,每一個主件最多有兩個附件,能夠發現一個主件和它的兩個附件等價於一個由四個物品組成的物品組,這便揭示了問題的某種本質。

我想說:失敗不是什麼丟人的事情,從失敗中全無收穫纔是。

首頁

 

P08: 泛化物品

定義

考慮這樣一種物品,它並無固定的費用和價值,而是它的價值隨着你分配給它的費用而變化。這就是泛化物品的概念。

更嚴格的定義之。在揹包容量爲V的揹包問題中,泛化物品是一個定義域爲0..V中的整數的函數h,當分配給它的費用爲v時,能獲得的價值就是h(v)。

這個定義有一點點抽象,另外一種理解是一個泛化物品就是一個數組h[0..V],給它費用v,可獲得價值h[V]。

一個費用爲c價值爲w的物品,若是它是01揹包中的物品,那麼把它當作泛化物品,它就是除了h(c)=w其它函數值都爲0的一個函數。若是它是徹底揹包中的物品,那麼它能夠當作這樣一個函數,僅當v被c整除時有h(v)=v/c*w,其它函數值均爲0。若是它是多重揹包中重複次數最多爲n的物品,那麼它對應的泛化物品的函數有h(v)=v/c*w僅當v被c整除且v/c<=n,其它狀況函數值均爲0。

一個物品組能夠看做一個泛化物品h。對於一個0..V中的v,若物品組中不存在費用爲v的的物品,則h(v)=0,不然h(v)爲全部費用爲v的物品的最大價值。P07中每一個主件及其附件集合等價於一個物品組,天然也可看做一個泛化物品。

泛化物品的和

若是面對兩個泛化物品h和l,要用給定的費用從這兩個泛化物品中獲得最大的價值,怎麼求呢?事實上,對於一個給定的費用v,只需枚舉將這個費用如何分配給兩個泛化物品就能夠了。一樣的,對於0..V的每個整數v,能夠求得費用v分配到h和l中的最大價值f(v)。也即f(v)=max{h(k)+l(v-k)|0<=k<=v}。能夠看到,f也是一個由泛化物品h和l決定的定義域爲0..V的函數,也就是說,f是一個由泛化物品h和l決定的泛化物品。

由此能夠定義泛化物品的和:h、l都是泛化物品,若泛化物品f知足f(v)=max{h(k)+l(v-k)|0<=k<=v},則稱f是h與l的和,即f=h+l。這個運算的時間複雜度取決於揹包的容量,是O(V^2)。

泛化物品的定義代表:在一個揹包問題中,若將兩個泛化物品代以它們的和,不影響問題的答案。事實上,對於其中的物品都是泛化物品的揹包問題,求它的答案的過程也就是求全部這些泛化物品之和的過程。設此和爲s,則答案就是s[0..V]中的最大值。

揹包問題的泛化物品

一個揹包問題中,可能會給出不少條件,包括每種物品的費用、價值等屬性,物品之間的分組、依賴等關係等。但確定能將問題對應於某個泛化物品。也就是說,給定了全部條件之後,就能夠對每一個非負整數v求得:若揹包容量爲v,將物品裝入揹包可獲得的最大價值是多少,這能夠認爲是定義在非負整數集上的一件泛化物品。這個泛化物品——或者說問題所對應的一個定義域爲非負整數的函數——包含了關於問題自己的高度濃縮的信息。通常而言,求得這個泛化物品的一個子域(例如0..V)的值以後,就能夠根據這個函數的取值獲得揹包問題的最終答案。

綜上所述,通常而言,求解揹包問題,即求解這個問題所對應的一個函數,即該問題的泛化物品。而求解某個泛化物品的一種方法就是將它表示爲若干泛化物品的和而後求之。

小結

本講能夠說都是我本身的原創思想。具體來講,是我在學習函數式編程的 Scheme 語言時,用函數編程的眼光審視各種揹包問題得出的理論。這一講真的很抽象,也許在「模型的抽象程度」這一方面已經超出了NOIP的要求,因此暫且看不懂也不要緊。相信隨着你的OI之路逐漸延伸,有一天你會理解的。

我想說:「思考」是一個OIer最重要的品質。簡單的問題,深刻思考之後,也能發現更多。

首頁

 

P09: 揹包問題問法的變化

以上涉及的各類揹包問題都是要求在揹包容量(費用)的限制下求能夠取到的最大價值,但揹包問題還有不少種靈活的問法,在這裏值得提一下。可是我認爲,只要深刻理解了求揹包問題最大價值的方法,即便問法變化了,也是不難想出算法的。

例如,求解最多能夠放多少件物品或者最多能夠裝滿多少揹包的空間。這均可以根據具體問題利用前面的方程求出全部狀態的值(f數組)以後獲得。

還有,若是要求的是「總價值最小」「總件數最小」,只需簡單的將上面的狀態轉移方程中的max改爲min便可。

下面說一些變化更大的問法。

輸出方案

通常而言,揹包問題是要求一個最優值,若是要求輸出這個最優值的方案,能夠參照通常動態規劃問題輸出方案的方法:記錄下每一個狀態的最優值是由狀態轉移方程的哪一項推出來的,換句話說,記錄下它是由哪個策略推出來的。即可根據這條策略找到上一個狀態,從上一個狀態接着向前推便可。

仍是以01揹包爲例,方程爲f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。再用一個數組g[i][v],設g[i][v]=0表示推出f[i][v]的值時是採用了方程的前一項(也即f[i][v]=f[i-1][v]),g[i][v]表示採用了方程的後一項。注意這兩項分別表示了兩種策略:未選第i個物品及選了第i個物品。那麼輸出方案的僞代碼能夠這樣寫(設最終狀態爲f[N][V]):

i=N

v=V

while(i>0)

    if(g[i][v]==0)

        print "未選第i項物品"

    else if(g[i][v]==1)

        print "選了第i項物品"

        v=v-c[i]

另外,採用方程的前一項或後一項也能夠在輸出方案的過程當中根據f[i][v]的值實時地求出來,也即不須紀錄g數組,將上述代碼中的g[i][v]==0改爲f[i][v]==f[i-1][v],g[i][v]==1改爲f[i][v]==f[i-1][v-c[i]]+w[i]也可。

輸出字典序最小的最優方案

這裏「字典序最小」的意思是1..N號物品的選擇方案排列出來之後字典序最小。以輸出01揹包最小字典序的方案爲例。

通常而言,求一個字典序最小的最優方案,只須要在轉移時注意策略。首先,子問題的定義要略改一些。咱們注意到,若是存在一個選了物品1的最優方案,那麼答案必定包含物品1,原問題轉化爲一個揹包容量爲v-c[1],物品爲2..N的子問題。反之,若是答案不包含物品1,則轉化成揹包容量仍爲V,物品爲2..N的子問題。無論答案怎樣,子問題的物品都是以i..N而非前所述的1..i的形式來定義的,因此狀態的定義和轉移方程都須要改一下。但也許更簡易的方法是先把物品逆序排列一下,如下按物品已被逆序排列來敘述。

在這種狀況下,能夠按照前面經典的狀態轉移方程來求值,只是輸出方案的時候要注意:從N到1輸入時,若是f[i][v]==f[i-v]及f[i][v]==f[i-1][f-c[i]]+w[i]同時成立,應該按照後者(即選擇了物品i)來輸出方案。

求方案總數

對於一個給定了揹包容量、物品費用、物品間相互關係(分組、依賴等)的揹包問題,除了再給定每一個物品的價值後求可獲得的最大價值外,還能夠獲得裝滿揹包或將揹包裝至某一指定容量的方案總數。

對於這類改變問法的問題,通常只需將狀態轉移方程中的max改爲sum便可。例如若每件物品均是徹底揹包中的物品,轉移方程即爲

f[i][v]=sum{f[i-1][v],f[i][v-c[i]]}

初始條件f[0][0]=1。

事實上,這樣作可行的緣由在於狀態轉移方程已經考察了全部可能的揹包組成方案。

最優方案的總數

這裏的最優方案是指物品總價值最大的方案。以01揹包爲例。

結合求最大總價值和方案總數兩個問題的思路,最優方案的總數能夠這樣求:f[i][v]意義同前述,g[i][v]表示這個子問題的最優方案的總數,則在求f[i][v]的同時求g[i][v]的僞代碼以下:

for i=1..N

   for v=0..V

        f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}

        g[i][v]=0

        if(f[i][v]==f[i-1][v])

            inc(g[i][v],g[i-1][v]

        if(f[i][v]==f[i-1][v-c[i]]+w[i])

            inc(g[i][v],g[i-1][v-c[i]])

若是你是第一次看到這樣的問題,請仔細體會上面的僞代碼。

求次優解、第K優解

對於求次優解、第K優解類的問題,若是相應的最優解問題能寫出狀態轉移方程、用動態規劃解決,那麼求次優解每每能夠相同的複雜度解決,第K優解則比求最優解的複雜度上多一個係數K。

其基本思想是將每一個狀態都表示成有序隊列,將狀態轉移方程中的max/min轉化成有序隊列的合併。這裏仍然以01揹包爲例講解一下。

首先看01揹包求最優解的狀態轉移方程:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。若是要求第K優解,那麼狀態f[i][v]就應該是一個大小爲K的數組f[i][v][1..K]。其中f[i][v][k]表示前i個物品、揹包大小爲v時,第k優解的值。「f[i][v]是一個大小爲K的數組」這一句,熟悉C語言的同窗可能比較好理解,或者也能夠簡單地理解爲在原來的方程中加了一維。顯然f[i][v][1..K]這K個數是由大到小排列的,因此咱們把它認爲是一個有序隊列。

而後原方程就能夠解釋爲:f[i][v]這個有序隊列是由f[i-1][v]和f[i-1][v-c[i]]+w[i]這兩個有序隊列合併獲得的。有序隊列f[i-1][v]即f[i-1][v][1..K],f[i-1][v-c[i]]+w[i]則理解爲在f[i-1][v-c[i]][1..K]的每一個數上加上w[i]後獲得的有序隊列。合併這兩個有序隊列並將結果(的前K項)儲存到f[i][v][1..K]中的複雜度是O(K)。最後的答案是f[N][V][K]。總的複雜度是O(NVK)。

爲何這個方法正確呢?實際上,一個正確的狀態轉移方程的求解過程遍歷了全部可用的策略,也就覆蓋了問題的全部方案。只不過因爲是求最優解,因此其它在任何一個策略上達不到最優的方案都被忽略了。若是把每一個狀態表示成一個大小爲K的數組,並在這個數組中有序的保存該狀態可取到的前K個最優值。那麼,對於任兩個狀態的max運算等價於兩個由大到小的有序隊列的合併。

另外還要注意題目對於「第K優解」的定義,將策略不一樣但權值相同的兩個方案是看做同一個解仍是不一樣的解。若是是前者,則維護有序隊列時要保證隊列裏的數沒有重複的。

小結

顯然,這裏不可能窮盡揹包類動態規劃問題全部的問法。甚至還存在一類將揹包類動態規劃問題與其它領域(例如數論、圖論)結合起來的問題,在這篇論揹包問題的專文中也不會論及。但只要深入領會前述全部類別的揹包問題的思路和狀態轉移方程,遇到其它的變形問法,只要題目難度還屬於NOIP,應該也不難想出算法。舉一反三、觸類旁通,應該也是一個OIer應有的品質吧。

首頁

 

P11: 揹包問題的搜索解法

《揹包問題九講》的本意是將揹包問題做爲動態規劃問題中的一類進行講解。但鑑於的確有一些揹包問題只能用搜索來解,因此這裏也對用搜索解揹包問題作簡單介紹。大部分以01揹包爲例,其它的應該能夠舉一反三。

簡單的深搜

對於01揹包問題,簡單的深搜的複雜度是O(2^N)。就是枚舉出全部2^N種將物品放入揹包的方案,而後找最優解。基本框架以下:

procedure SearchPack(i,cur_v,cur_w)

    if(i>N)

        if(cur_w>best)

            best=cur_w

        return

    if(cur_v+v[i]<=V)

        SearchPack(i+1,cur_v+v[i],cur_w+w[i])

    SearchPack(i+1,cur_v,cur_w)

其中cur_v和cur_w表示當前解的費用和權值。主程序中調用SearchPack(1,0,0)便可。

搜索的剪枝

基本的剪枝方法不外乎可行性剪枝或最優性剪枝。

可行性剪枝即判斷按照當前的搜索路徑搜下去可否找到一個可行解,例如:若將剩下全部物品都放入揹包仍然沒法將揹包充滿(設題目要求必須將揹包充滿),則剪枝。

最優性剪枝即判斷按照當前的搜索路徑搜下去可否找到一個最優解,例如:若加上剩下全部物品的權值也沒法獲得比當前獲得的最優解更優的解,則剪枝。

搜索的順序

在搜索中,能夠認爲順序靠前的物品會被優先考慮。因此利用貪心的思想,將更有可能出如今結果中的物品的順序提早,能夠較快地得出貪心地較優解,更有利於最優性剪枝。因此,能夠考慮將按照「性價比」(權值/費用)來排列搜索順序。

另外一方面,若將費用較大的物品排列在前面,能夠較快地填滿揹包,有利於可行性剪枝。

最後一種能夠考慮的方案是:在開始搜索前將輸入文件中給定的物品的順序隨機打亂。這樣能夠避免命題人故意設置的陷阱。

以上三種決定搜索順序的方法很難說哪一種更好,事實上每種方法都有適用的題目和數據,也有可能將它們在某種程度上混合使用。

子集和問題

子集和問題是一個NP-Complete問題,與前述的(加權的)01揹包問題並不相同。給定一個整數的集合S和一個整數X,問是否存在S的一個子集知足其中全部元素的和爲X。

這個問題有一個時間複雜度爲O(2^(N/2))的較高效的搜索算法,其中N是集合S的大小。

第一步思想是二分。將集合S劃分紅兩個子集S1和S2,它們的大小都是N/2。對於S1和S2,分別枚舉出它們全部的2^(N/2)個子集和,保存到某種支持查找的數據結構中,例如hash set。

而後就要將兩部分結果合併,尋找是否有和爲X的S的子集。事實上,對於S1的某個和爲X1的子集,只需尋找S2是否有和爲X-X1的子集。

假設採用的hash set是理想的,每次查找和插入都僅花費O(1)的時間。兩步的時間複雜度顯然都是O(2^(N/2))。

實踐中,每每能夠先將第一步獲得的兩組子集和分別排序,而後再用兩個指針掃描的方法查找是否有知足要求的子集和。這樣的實現,在可接受的時間內能夠解決的最大規模約爲N=42。

搜索仍是DP?

在看到一道揹包問題時,應該用搜索仍是動態規劃呢?

首先,能夠從數據範圍中獲得命題人意圖的線索。若是一個揹包問題能夠用DP解,V必定不能很大,不然O(VN)的算法沒法承受,而通常的搜索解法都 是僅與N有關,與V無關的。因此,V很大時(例如上百萬),命題人的意圖就應該是考察搜索。另外一方面,N較大時(例如上百),命題人的意圖就頗有多是考 察動態規劃了。

另外,當想不出合適的動態規劃算法時,就只能用搜索了。例如看到一個從未見過的揹包中物品的限制條件,沒法想出DP的方程,只好寫搜索以謀求必定的分數了。

 

附:USACO中的揹包問題

USACO是USA Computing Olympiad的簡稱,它組織了不少面向全球的計算機競賽活動。

USACO Trainng是一個很適合初學者的題庫,我認爲它的特點是題目質量高,按部就班,還配有不錯的課文和題目分析。其中關於揹包問題的那篇課文 (TEXT Knapsack Problems) 也值得一看。

另外,USACO Contest是USACO常年組織的面向全球的競賽系列,在此也推薦NOIP選手參加。

我整理了USACO Training中涉及揹包問題的題目,應該能夠做爲不錯的習題。其中標加號的是我比較推薦的,標歎號的是我認爲對NOIP選手比較有挑戰性的。

題目列表

  • Inflate (+) (基本01揹包)
  • Stamps (+)(!) (對初學者有必定挑戰性)
  • Money
  • Nuggets
  • Subsets
  • Rockers (+) (另外一類有趣的「二維」揹包問題)
  • Milk4 (!) (很怪的揹包問題問法,較難用純DP求解)

題目簡解

如下文字來自我所撰的《USACO心得》一文,該文的完整版本,包括個人程序,可在DD的USACO征程中找到。

Inflate 是加權01 揹包問題,也就是說:每種物品只有一件,只能夠選擇放或者不放;並且每種物品有對應的權值,目標是使總權值最大或最小。它最樸素的狀態轉移方程是:f[k][i] = max{f[k-1][i] , f[k-1][i-v[k]]+w[k]}。f[k][i]表示前k 件物品花費代價i 能夠獲得的最大權值。v[k]和w[k]分別是第k 件物品的花費和權值。能夠看到, f[k]的求解過程就是使用第k 件物品對f[k-1]進行更新的過程。那麼事實上就不用使用二維數組,只須要定義f[i],而後對於每件物品k,順序地檢查f[i]與f[i-v[k]]+w[k]的大小,若是後者更大,就對前者進行更新。這是揹包問題中典型的優化方法。

題目stamps 中,每種物品的使用量沒有直接限制,但使用物品的總量有限制。求第一個不能用這有限個物品組成的揹包的大小。(能夠這樣等價地認爲)設f[k][i] 表示前k 件物品組成大小爲i 的揹包, 最少須要物品的數量。則f[k][i]= min{f[k-1][i],f[k-1][i-j*s[k]]+j},其中j 是選擇使用第k 件物品的數目,這個方程運用時能夠用和上面同樣的方法處理成一維的。求解時先設置一個粗糙的循環上限,即最大的物品乘最多物品數。

Money 是多重揹包問題。也就是每一個物品可使用無限屢次。要求解的是構成一種揹包的不一樣方案總數。基本上就是把通常的多重揹包的方程中的min 改爲sum 就好了。

Nuggets 的模型也是多重揹包。要求求解所給的物品不能剛好放入的揹包大小的最大值(可能不存在)。只須要根據「若i、j 互質,則關於x、y 的不定方程i*x+y*j=n 必有正整數解,其中n>i*j」這必定理得出一個循環的上限。 Subsets 子集和問題至關於物品大小是前N 個天然數時求大小爲N*(N+1)/4 的 01 揹包的方案數。

Rockers 能夠利用求解揹包問題的思想設計解法。個人狀態轉移方程以下: f[i][j][t]=max{f[i][j][t-1] , f[i-1][j][t] , f[i-1][j][t-time[i]]+1 , f[i-1][j-1][T]+(t>=time[i])}。其中 f[i][j][t]表示前i 首歌用j 張完整的盤和一張錄了t 分鐘的盤能夠放入的最多歌數,T 是一張光盤的最大容量,t>=time[i]是一個bool 值轉換成int 取值爲0 或1。但我後來發現我當時設計的狀態和方程效率有點低,若是換成這樣:f[i][j]=(a,b)表示前i 首歌中選了j 首須要用到a 張完整的光盤以及一張錄了b 分鐘的光盤,會將時空複雜度都大大下降。這種將狀態的值設爲二維的方法值得注意。

Milk4 是這些類揹包問題中難度最大的一道了。不少人沒法作到將它用純DP 方法求解,而是用迭代加深搜索枚舉使用的桶,將其轉換成多重揹包問題再DP。因爲 USACO 的數據弱,迭代加深的深度很小,這樣也能夠AC,但咱們仍是能夠用純DP 方法將它完美解決的。設f[k]爲稱量出k 單位牛奶須要的最少的桶數。那麼能夠用相似多重揹包的方法對f 數組反覆更新以求得最小值。然而困難在於如何輸出字典序最小的方案。咱們能夠對每一個i 記錄pre_f[i]和pre_v[i]。表示獲得i 單位牛奶的過程是用pre_f[i]單位牛奶加上若干個編號爲pre_v[i]的桶的牛奶。這樣就能夠一步步求得獲得i 單位牛奶的完整方案。爲了使方案的字典序最小,咱們在每次找到一個耗費桶數相同的方案時對已儲存的方案和新方案進行比較再決定是否更新方案。爲了使這種比較快捷,在使用各類大小的桶對f 數組進行更新時先大後小地進行。USACO 的官方題解正是這一思路。若是認爲以上文字比較難理解能夠閱讀官方程序或個人程序。

首頁


Copyright (c) 2007 Tianyi Cui

Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation.

整理 by stntwm

屢次揹包 
屢次揹包問題:給定 n 種物品和一個揹包。第 i 種物品 的價值是 Wi ,其體積
爲 Vi,數量是 Ki件,揹包的容量爲 C。能夠任意選擇裝入揹包中的物品,求裝入背
包中物品的最大總價值。 

方法一:能夠把此物品拆分紅Ki個只能用一次的物品,直接套用 0-1 揹包問題的經典動規實現,可是效率過低了,須要尋找更高效的算法。此算法時間複雜度爲O(C*∑(Ki)) 

方法二:拆分紅體積和價值分別爲原來1, 2 , 4..   2^m,    Ki-2^m 倍的幾個物品,用0-1揹包求解。 時間複雜度爲O(C*∑([log2Ki]))

方法三(本文重點):(對單調隊列沒有了解的請參見原論文[本文結尾連接])對於第 i 種物品來講,已知體積 v,價值 w,數量 k,那麼能夠按照當前枚舉的體積 j 對v的餘數把整個動規數組分紅 v份,如下是 v=3 的狀況: 
j             0 1 2 3 4 5 6 7 8 ……
j mod v   0 1 2 0 1 2 0 1 2 …… 

咱們能夠把每一份分開處理,假設餘數爲 d。 
編號j         0     1        2            3           4             5         ……
對應體積 d    d+v    d+2*v     d+3*v    d+4*v      d+5*v    …… 

如今看到分組之後,編號 j 能夠從 j-k 到 j-1 中的任意一個編號轉移而來(由於相鄰的體積正好相差 v) ,這看上去已經和區間最大值有點類似了。可是注意到因爲體積不同,顯然體積大的價值也會大於等於體積小的,直接比較是沒有意義的,因此還須要把價值修正到同一體積的基礎上。好比都退化到 d,也就是說用 F[j*v+d]- j*w來代替原來的價值進入隊列。

對於物品i,僞代碼以下

1. FOR d: = 0 TO v-1                     //枚舉餘數,分開處理 
2.   清空隊列 
3.   FOR j: = 0 TO (C-d) div v           //j 枚舉標號,對應體積爲 j*v+d 
4.    INSERT j , F[ j*v+d ] – j * w      //插入隊列 
5.    IF A[ L ] < j - k THEN L + 1 → L //若是隊列的首元素已經失效 
6.    B[ L ] + j * w → F[ j*v+d ]       //取隊列頭更新 
7.   END FOR 
8. END FOR 

已知單調隊列的效率是 O(n),那麼加上單調隊列優化之後的屢次揹包,
效率就是 O(n*C)了。 
(詳細請參見原論文)

==========================================================

完整程序以下(Pascal):

var
   a,b,f:array[0..100000] of longint;
   m,s,c,n,t,i,j,l,r,d:longint;
procedure insert(x,y:longint);
begin
   while (l<=r)and(b[r]<=y) do dec(r);
   inc(r);a[r]:=x;b[r]:=y;
end;
begin
   readln(n,t);              //讀入數據 n爲物品個數 t爲揹包容量
   for i:=1 to n do
   begin
      read(m,s,c);         //讀入當前物品 m爲物品體積、s爲物品價值、c爲物品可用次數(0表示無限制)
      if (c=0)or(t div m<c) then c:=t div m;
      for d:=0 to m-1 do
      begin
         l:=1;r:=0;     //清空隊列
         for j:=0 to (t-d) div m do
         begin
            insert(j,f[j*m+d]-j*s);   //將新的點插入隊列
            if a[l]<j-c then inc(l);   //刪除失效點
            f[j*m+d]:=b[l]+j*s;        //用隊列頭的值更新f[j*m+d]
         end;
      end;
   end;
   writeln(f[t]);
end.

==========================================================

相關文章
相關標籤/搜索