當前位置:首頁 » 操作系統 » 01背包問題演算法

01背包問題演算法

發布時間: 2023-03-27 03:01:32

⑴ 01背包問題

演算法分析

對於背包問題,通常的處理方法是搜索。
用遞歸來完成搜索,演算法設計如下:
function Make( i {處理到第i件物品} , j{剩餘的空間為j}:integer) :integer;
初始時i=m , j=背包總容量
begin
if i:=0 then
Make:=0;
if j>=wi then (背包剩餘空間可以放下物品 i )
r1:=Make(i-1,j-wi)+v; (第i件物品放入所能得到的價值 )
r2:=Make(i-1,j)(第i件物品不放所能得到的價值 )
Make:=max{r1,r2}
end;
這個演算法的時間復雜度是O(2^n),我們可以做一些簡單的優化。
由於本題中的所有物品的體積均為整數,經過幾次的選擇後背包的剩餘空間可能會相等,在搜索中會重復計算這些結點,所以,如果我們把搜索過程中計算過的結點的值記錄下來,以保證不重復計算的話,速度就會提高很多。這是簡單?quot;以空間換時間"。
我們發現,由於這些計算過程中會出現重疊的結點,符合動態規劃中子問題重疊的性質。
同時,可以看出如果通過第N次選擇得到的是一個最優解的話,那麼第N-1次選擇的結果一定也是一個最優解。這符合動態規劃中最優子問題的性質。
考慮用動態規劃的方法來解決,這里的:
階段是:在前N件物品中,選取若干件物品放入背包中;
狀態是:在前N件物品中,選取若干件物品放入所剩空間為W的背包中的所能獲得的最大價值;
決策是:第N件物品放或者不放;
由此可以寫出動態轉移方程:
我們用f[i,j]表示在前 i 件物品中選擇若干件放在所剩空間為 j 的背包里所能獲得的最大價值
f[i,j]=max{f[i-1,j-Wi]+Pi (j>=Wi), f[i-1,j]}
這個方程非常重要,基本上所有跟背包相關的問題的方程都是由它衍生出來的。所以有必要將它詳細解釋一下:「將前i件物品放入容量為v的背包中」這個子問題,若只考慮第i件物品的策略(放或不放),那麼就可以轉化為一個只牽扯前i-1件物品的問題。如果不放第i件物品,那麼問題就轉化為「前i-1件物品放入容量為v的背包中」,價值為f[v];如果放第i件物品,那麼問題就轉化為「前i-1件物品放入剩下的容量為v-c的背包中」,此時能獲得的最大價值就是f[v-c]再加上通過放入第i件物品獲得的價值w。
這樣,我們可以自底向上地得出在前M件物品中取出若干件放進背包能獲得的最大價值,也就是f[m,w]
演算法設計如下:
procere Make;
begin
for i:=0 to w do
f[0,i]:=0;
for i:=1 to m do
for j:=0 to w do begin
f[i,j]:=f[i-1,j];
if (j>=w) and (f[i-1,j-w]+v>f[i,j]) then
f[i,j]:=f[i-1,j-w]+v;
end;
writeln(f[m,wt]);
end;
由於是用了一個二重循環,這個演算法的時間復雜度是O(n*w)。而用搜索的時候,當出現最壞的情況,也就是所有的結點都沒有重疊,那麼它的時間復雜度是O(2^n)。看上去前者要快很多。但是,可以發現在搜索中計算過的結點在動態規劃中也全都要計算,而且這里算得更多(有一些在最後沒有派上用場的結點我們也必須計算),在這一點上好像是矛盾的。
事實上,由於我們定下的前提是:所有的結點都沒有重疊。也就是說,任意N件物品的重量相加都不能相等,而所有物品的重量又都是整數,那末這個時候W的最小值是:1+2+2^2+2^3+……+2^n-1=2^n -1
此時n*w>2^n,動態規劃比搜索還要慢~~|||||||所以,其實背包的總容量W和重疊的結點的個數是有關的。
考慮能不能不計算那些多餘的結點……
優化時間復雜度
以上方法的時間和空間復雜度均為O(N*V),其中時間復雜度基本已經不能再優化了,但空間復雜度卻可以優化到O(V)。
先考慮上面講的基本思路如何實現,肯定是有一個主循環i=1..N,每次算出來二維數組f[0..V]的所有值。那麼,如果只用一個數組f[0..V],能不能保證第i次循環結束後f[v]中表示的就是我們定義的狀態f[v]呢?f[v]是由f[v]和f[v-c]兩個子問題遞推而來,能否保證在推f[v]時(也即在第i次主循環中推f[v]時)能夠得到f[v]和f[v-c]的值呢?事實上,這要求在每次主循環中我們以v=V..0的順序推f[v],這樣才能保證推f[v]時f[v-c]保存的是狀態f[v-c]的值。偽代碼如下:
for i=1..N
for v=V..0
f[v]=max{f[v],f[v-c]+w};
其中的f[v]=max{f[v],f[v-c]}一句恰就相當於我們的轉移方程f[v]=max{f[v],f[v-c]},因為現在的f[v-c]就相當於原來的f[v-c]。如果將v的循環順序從上面的逆序改成順序的話,那麼則成了f[v]由f[v-c]推知,與本題意不符,但它卻是另一個重要的背包問題P02最簡捷的解決方案,故學習只用一維數組解01背包問題是十分必要的。
事實上,使用一維數組解01背包的程序在後面會被多次用到,所以這里抽象出一個處理一件01背包中的物品過程,以後的代碼中直接調用不加說明。
過程ZeroOnePack,表示處理一件01背包中的物品,兩個參數cost、weight分別表明這件物品的費用和價值。
procere 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,w);
初始化的細節問題

我們看到的求最優解的背包問題題目中,事實上有兩種不太相同的問法。有的題目要求「恰好裝滿背包」時的最優解,有的題目則並沒有要求必須把背包裝滿。一種區別這兩種問法的實現方法是在初始化的時候有所不同。
如果是第一種問法,要求恰好裝滿背包,那麼在初始化時除了f[0]為0其它f[1..V]均設為-∞,這樣就可以保證最終得到的f[N]是一種恰好裝滿背包的最優解。
如果並沒有要求必須把背包裝滿,而是只希望價格盡量大,初始化時應該將f[0..V]全部設為0。
為什麼呢?可以這樣理解:初始化的f數組事實上就是在沒有任何物品可以放入背包時的合法狀態。如果要求背包恰好裝滿,那麼此時只有容量為0的背包可能被價值為0的nothing「恰好裝滿」,其它容量的背包均沒有合法的解,屬於未定義的狀態,它們的值就都應該是-∞了。如果背包並非必須被裝滿,那麼任何容量的背包都有一個合法解「什麼都不裝」,這個解的價值為0,所以初始時狀態的值也就全部為0了。
這個小技巧完全可以推廣到其它類型的背包問題,後面也就不再對進行狀態轉移之前的初始化進行講解

⑵ Pascal0/1背包問題

我是小鳥,寫的不好,請大蝦們見諒,提提意見,謝謝。這個好像不是01背包問題,而是簡單的深搜。01背包還要有物品畢滾的價值,求此背包能裝的最大價值。我寫了個回溯,我把所有的情況都納基輸出了。Program gdw;
Const
Infile = 'gdw.in';
Var
a, b: Array[1..100] Of Boolean;
w: Array[1..100] Of Integer;
k, s, num, i: Integer;Procere print;
Var
i: Integer;
Begin
For i:=1 To k Do If b[i] Then Write(i, ' ');
WriteLn;
Fillchar(b, sizeof(b), False);
End;Procere try1(i, j: Integer);
Begin
If j < 0 Then Exit;
If i > k Then Begin
If j = 0 Then Begin b := a; Print; Inc(num) End;
Exit;
End;
a[i] := True;
try1(i + 1, j - w[i]);
a[i] := False;
try1(i + 1, j);
End;Begin
Assign(Input, Infile); Reset(Input);
ReadLn(k ,s);
For i:=1 To k Do Read(w[i]);
Close(Input);
try1(1, s);
If num = 0 Then WriteLn('手茄余No Answer');
End.

⑶ 關於C++ 01背包問題

1.摘要

以背包問題為例,介紹了貪心法與動態規劃的關系以及兩個方案在解決背包問題上的比較。貪心法什麼時候能取到最優界並無一般理論,但對於普通背包問題我們有一個完美的結果——貪心法可取到最優解。介紹了其它一些對背包問題的研究或者拓展。

2.介紹

貪心演算法是我們在《演算法設計技巧與分析》這門課中所學習到的幾種重要的演算法之一,顧名思義,貪心演算法總是作出在當前看來最好的選擇。也就是該演算法並不從整體最優考慮,它所作出的選擇只是在某種意義上的從局部的最優選擇,尋找到解決問題的次優解的方法。雖然我們希望貪心演算法得到的最終結果也是整體最優的,但是在某些情況下,該演算法得到的只是問題的最優解的近似。

3.演算法思想:

貪心法的基本思路:

——從問題的某一個初始解出發逐步逼近給定的目標,以盡可能快的地求得更好的解。當達到某演算法中的某一步不能再繼續前進時,演算法停止。

該演算法存在問題:

1.不能保證求得的最後解是最佳的;

2.不能用來求最大或最小解問題;

3.只能求滿足某些約束條件的可行解的范圍。

實現該演算法的過程:

在約束下最大。

(2)動態規劃解決方案:是解決0/1背包問題的最優解

(i)若i=0或j=0,V[i,j] = 0

(ii)若j<si, V[i,j] = V[i-1,j](僅用最優的方法,選取前i-1項物品裝入體積為j的背包,因為第i項體積大於j,裝不下這一項,所以背包裡面的i-1項就達到最大值)

(iii)若i>0和j>=si, Max{V[i-1,j],V[i-1,j-si]+vi} (第一種情況是包中的i-1項已經達到最大值,第二種情況是i-1項佔j-si的體積再加上第i項的總的價值,取這兩種情況的最大值。)

//sj和vj分別為第j項物品的體積和價值,C是總體積限制。

//V[i,j]表示從前i項{u1,u2,…,un}中取出來的裝入體積為j的背包的物品的最大//價值。[13]

(3)貪心演算法解決背包問題有幾種策略:

(i)一種貪婪准則為:從剩餘的物品中,選出可以裝入背包的價值最大的物品,利用這種規則,價值最大的物品首先被裝入(假設有足夠容量),然後是下一個價值最大的物品,如此繼續下去。這種策略不能保證得到最優解。例如,考慮n=2, w=[100,10,10], p =[20,15,15], c = 105。當利用價值貪婪准則時,獲得的解為x= [ 1 , 0 , 0 ],這種方案的總價值為2 0。而最優解為[ 0 , 1 , 1 ],其總價值為3 0。

(ii)另一種方案是重量貪婪准則是:從剩下的物品中選擇可裝入背包的重量最小的物品。雖然這種規則對於前面的例子能產生最優解,但在一般情況下則不一定能得到最優解。考慮n= 2 ,w=[10,20], p=[5,100], c= 2 5。當利用重量貪婪策略時,獲得的解為x =[1,0],比最優解[ 0 , 1 ]要差。

(iii)還有一種貪婪准則,就是我們教材上提到的,認為,每一項計算yi=vi/si,即該項值和大小的比,再按比值的降序來排序,從第一項開始裝背包,然後是第二項,依次類推,盡可能的多放,直到裝滿背包。

有的參考資料也稱為價值密度pi/wi貪婪演算法。這種策略也不能保證得到最優解。利用此策略試解n= 3 ,w=[20,15,15], p=[40,25,25], c=30時的最優解。雖然按pi /wi非遞(增)減的次序裝入物品不能保證得到最優解,但它是一個直覺上近似的解。

而且這是解決普通背包問題的最優解,因為在選擇物品i裝入背包時,可以選擇物品i的一部分,而不一定要全部裝入背包,1≤i≤n。

如圖1,大體上說明了動態規劃解決的0/1背包問題和貪心演算法解決的問題之間的區別,

圖1

(4)貪心演算法解決背包問題的演算法實現:

代碼如下:

#include<iostream.h>
structgoodinfo
{
floatp;//物品效益
floatw;//物品重量
floatX;//物品該放的數量
intflag;//物品編號
};//物品信息結構體
voidInsertionsort(goodinfogoods[],intn)
{//插入排序,按pi/wi價值收益進行排序,一般教材上按冒泡排序
intj,i;
for(j=2;j<=n;j++)
{
goods[0]=goods[j];
i=j-1;
while(goods[0].p>goods[i].p)
{
goods[i+1]=goods[i];
i--;
}
goods[i+1]=goods[0];
}
}//按物品效益,重量比值做升序排列
voidbag(goodinfogoods[],floatM,intn)
{

floatcu;
inti,j;
for(i=1;i<=n;i++)
goods[i].X=0;
cu=M;//背包剩餘容量
for(i=1;i<n;i++)
{
if(goods[i].w>cu)//當該物品重量大與剩餘容量跳出
break;
goods[i].X=1;
cu=cu-goods[i].w;//確定背包新的剩餘容量
}
if(i<=n)
goods[i].X=cu/goods[i].w;//該物品所要放的量
/*按物品編號做降序排列*/
for(j=2;j<=n;j++)
{
goods[0]=goods[j];
i=j-1;
while(goods[0].flag<goods[i].flag)
{
goods[i+1]=goods[i];
i--;
}
goods[i+1]=goods[0];
}
///////////////////////////////////////////
cout<<"最優解為:"<<endl;
for(i=1;i<=n;i++)
{
cout<<"第"<<i<<"件物品要放:";
cout<<goods[i].X<<endl;
}
}
voidmain()
{
cout<<"|--------運用貪心法解背包問題---------|"<<endl;
intj,n;floatM;
goodinfo*goods;//定義一個指針
while(j)
{
cout<<"請輸入物品的總數量:";
cin>>n;
goods=newstructgoodinfo[n+1];//
cout<<"請輸入背包的最大容量:";
cin>>M;
cout<<endl;
inti;
for(i=1;i<=n;i++)
{goods[i].flag=i;
cout<<"請輸入第"<<i<<"件物品的重量:";
cin>>goods[i].w;
cout<<"請輸入第"<<i<<"件物品的效益:";
cin>>goods[i].p;
goods[i].p=goods[i].p/goods[i].w;//得出物品的效益,重量比
cout<<endl;

}
Insertionsort(goods,n);
bag(goods,M,n);
cout<<"press<1>torunagian"<<endl;
cout<<"press<0>toexit"<<endl;
cin>>j;
}
}

⑷ 求動態規劃01背包問題c語言的代碼,要稍微簡單且無錯的。謝謝

int c[10][100];/*對應每種情況的最大價值*/

int knapsack(int m,int n){ /桐伍/ 一個載重為m的背包 總共n個物品
int i,j,w[10],p[10];
printf("each weight & value :\n");
for(i=1;i<=n;i++)
scanf("%d %d",&w[i],&p[i]); // 第i個 物品的重量w[i] 價值p[i]
for(i=0;i<10;i++)
for(j=0;j<100;j++)
c[i][j]=0;/*初始化數組*/
for(i=1;i<=n;i++) //遍歷每一個物品i
for(j=1;j<=m;j++) //假設背包的載重j為1、圓擾2、3、4、5、... ... m的情況
{
if(j >= w[i]) /*如果當前物品的重量 < 背包載重*/
{
if(p[i]+c[i-1][j-w[i]]>c[i-1][j])/*如果本物品的價值加上 背包剩下的空間能放的物品橘輪旦的價值 大於上一次選擇的最佳方案則更新c[i][j]*/
c[i][j]=p[i]+c[i-1][j-w[i]];
else
c[i][j]=c[i-1][j];
// c[i][j] = (p[i]+c[i-1][j-w[i]]>c[i-1][j])?(p[i]+c[i-1][j-w[i]]):(c[i-1][j]);
}
else c[i][j]=c[i-1][j];
}
return c[n][m];

}

int main()
{
int m,n;
printf("背包的承重量,物品的總個數:\n");
while(scanf("%d %d",&m,&n) != EOF){
printf("能裝的最大總價值為%d",knapsack(m,n));
printf("\n");
}
return 0;
}

⑸ 背包問題和0-1背包問題有什麼區別

背包問題和0-1背包問題區別為:循環變數不同、約束條件不同、最大總價值不同。

一、循環變數不同

1、背絕氏寬包問題:背包問題須先求出列坐標j較小的元素,故讓循環變數j的值從小到大遞增。

2、0-1背包問題:0-1背包問題須先求出列並亮坐標j較大的元素,故讓循環變數j的值從大到小遞減。

二、約束條件不同

1、背包問題:背包問題的約束條件是給定幾種物品,物品可以取無限次。

2、0-1背包問題:0-1背包問題的約束條件是給定幾種物品,物品只可以取一次。

三、最大總價值不同

1、背包問題:背包問題若取了1件第i個物品,則總容量變為j-W[i],剩下的仍可以在前i件物品中去取,其最大總價值為B[i][j-W[i]] + P[i]。

2、0-1背包問題:0-1背包問題若取了1件第i個物品,則總容量變為j-W[i],剩下的只能在前i-1件物品中去取了,其最大總價值為B[i-1][j-W[i]] +核宏 P[i]。

⑹ 用動態規演算法求出的0-1背包問題,寫出完整的可以運行的程序,並且給出演算法復雜性的分析與結果,謝謝

1.0-1背包: 每個背包只能使用一次或有限次(可轉化為一次):
A.求最多可放入的重量。
NOIP2001 裝箱問題
有一個箱友哪褲子容量為v(正整數,o≤v≤20000),同時有n個物品(o≤n≤30),每個物品有一個體積 (正整數)。要求從 n 個物品中,任取若千個裝入箱內,使箱子的剩餘空間為最小。
l 搜索方法
procere search(k,v:integer);
var i,j:integer;
begin
if v<best then best:=v;
if v-(s[n]-s[k-1])>=best then exit;
if k<=n then begin
if v>w[k] then search(k+1,v-w[k]);
search(k+1,v);
end;
end;

l DP
F[I,j]為前i個物品中選擇若干個放入使其體積正好為j的標志,為布爾型。
實現:將最優化問題轉化為判定性問題
f [I, j] = f [ i-1, j-w[i] ] (w[I]<=j<=v) 邊界:f[0,0]:=true.
For I:=1 to n do
For j:=w[I] to v do F[I,j]:=f[I-1,j-w[I]];
優化:當前狀態只與前一階段狀態有關,好簡可降至一維。
F[0]:=true;
For I:=1 to n do begin
F1:=f;
For j:=w[I] to v do
If f[j-w[I]] then f1[j]:=true;
F:=f1;
End;

B.求可以放入的最大價值。
F[I,j] 為容量為I時取前j個緩扒背包所能獲得的最大價值。
F [i,j] = max

C.求恰好裝滿的情況數。
DP:
Procere update;
var j,k:integer;
begin
c:=a;
for j:=0 to n do
if a[j]>0 then
if j+now<=n then inc(c[j+now],a[j]);
a:=c;
end;

2.可重復背包
A求最多可放入的重量。
F[I,j]為前i個物品中選擇若干個放入使其體積正好為j的標志,為布爾型。
狀態轉移方程為
f[I,j] = f [ I-1, j – w[I]*k ] (k=1.. j div w[I])

B.求可以放入的最大價值。
USACO 1.2 Score Inflation
進行一次競賽,總時間T固定,有若干種可選擇的題目,每種題目可選入的數量不限,每種題目有一個ti(解答此題所需的時間)和一個si(解答此題所得的分數),現要選擇若干題目,使解這些題的總時間在T以內的前提下,所得的總分最大,求最大的得分。
*易想到:
f[i,j] = max (0<=k<= i div w[j])
其中f[i,j]表示容量為i時取前j種背包所能達到的最大值。
*實現:
Begin
FillChar(f,SizeOf(f),0);
For i:=1 To M Do
For j:=1 To N Do
If i-problem[j].time>=0 Then
Begin
t:=problem[j].point+f[i-problem[j].time];
If t>f[i] Then f[i]:=t;
End;
Writeln(f[M]);
End.

C.求恰好裝滿的情況數。
Ahoi2001 Problem2
求自然數n本質不同的質數和的表達式的數目。
思路一,生成每個質數的系數的排列,在一一測試,這是通法。
procere try(dep:integer);
var i,j:integer;
begin
cal;
if now>n then exit;
if dep=l+1 then begin
cal;
if now=n then inc(tot);
exit;
end;
for i:=0 to n div pr[dep] do begin
xs[dep]:=i;
try(dep+1);
xs[dep]:=0;
end;
end;

思路二,遞歸搜索效率較高
procere try(dep,rest:integer);
var i,j,x:integer;
begin
if (rest<=0) or (dep=l+1) then begin
if rest=0 then inc(tot);
exit;
end;
for i:=0 to rest div pr[dep] do
try(dep+1,rest-pr[dep]*i);
end;

思路三:可使用動態規劃求解
USACO1.2 money system
V個物品,背包容量為n,求放法總數。
轉移方程:

Procere update;
var j,k:integer;
begin
c:=a;
for j:=0 to n do
if a[j]>0 then
for k:=1 to n div now do
if j+now*k<=n then inc(c[j+now*k],a[j]);
a:=c;
end;

begin
read(now);
i:=0;
while i<=n do begin
a[i]:=1; inc(i,now); end;
for i:=2 to v do
begin
read(now);
update;
end;
writeln(a[n]);

⑺ 01背包問題變種:從給定的N個正數中選取若干個數之和最接近M的JAVA寫法

BIAS0:= (C-MA(C,2))/MA(C,2)*100;
BIAS1 := (C-MA(C,12))/MA(C,12)*100;
BIAS2 := (C-MA(C,26))/MA(C,26)*100;
BIAS3 := (C-MA(C,48))/MA(C,48)*100;
HXL:=V/CAPITAL*100;
D1:=INDEXC;
D2:=MA(D1,56);
DR2:=D1/D2<0.94;
E1:=(C-HHV(C,12))/HHV(C,12)*10;
E2:=(C-REF(C,26))/REF(C,26)*10;

⑻ 背包問題

背包問題 它是在1978年由Merkel和Hellman提出的。它的主要思路是假定某人擁有大量物品,重量各不同。此人通過秘密地選擇一部分物品並將它們放到背包中來加密消息。背包中的物品中重量是公開的,所有可能的物品也是公開的,但背包中的物品是保密的。附加一定的限制條件,給出重量,而要列出可能的物品,在計算上是不可實現的。背包問題是熟知的不可計算問題,背包體制以其加密,解密速度快而其人注目。但是,大多數一次背包體制均被破譯了,因此現在很少有人使用它。
DD牛的背包九講
P01: 01背包問題
題目
有N件物品和一個容量為V的背包。第i件物品的費用是c,價值是w。求解將哪些物品裝入背包可使這些物品的費用總和不超過背包容量,且價值總和最大。
基本思路
這是最基礎的背包問題,特點是:每種物品僅有一件,可以選擇放或不放。
用子問題定義狀態:即f[v]表示前i件物品恰放入一個容量為v的背包可以獲得的最大價值。則其狀態轉移方程便是:f[v]=max{f[v],f[v-c]+w}。
這個方程非常重要,基本上所有跟背包相關的問題的方程都是由它衍生出來的。所以有必要將它詳細解釋一下:「將前i件物品放入容量為v的背包中」這個子問題,若只考慮第i件物品的策略(放或不放),那麼就可以轉化為一個只牽扯前i-1件物品的問題。如果不放第i件物品,那麼問題就轉化為「前i-1件物品放入容量為v的背包中」;如果放第i件物品,那麼問題就轉化為「前i-1件物品放入剩下的容量為v-c的背包中」,此時能獲得的最大價值就是f [v-c]再加上通過放入第i件物品獲得的價值w。
注意f[v]有意義當且僅當存在一個前i件物品的子集,其費用總和為v。所以按照這個方程遞推完畢後,最終的答案並不一定是f[N] [V],而是f[N][0..V]的最大值。如果將狀態的定義中的「恰」字去掉,在轉移方程中就要再加入一項f[v-1],這樣就可以保證f[N] [V]就是最後的答案。至於為什麼這樣就可以,謹尺擾由你自己來體會了。
優化空間復雜度
以上方法的時間和空間復雜度均為O(N*V),其中時間復雜度基本已經不能再優化了,但空間復雜度卻可以優化到O(V)。
先考慮上面講的基本思路困差如何實現,肯定是有一個主循環i=1..N,每次算出來二維數組f[0..V]的所有值。那麼,如果只用一個數組f [0..V],能不能保證第i次循環結束後f[v]中表示的就是我們定義的狀態f[v]呢?f[v]是由f[v]和f [v-c]兩個子問題遞推而來,能否保證在推f[v]時(也即在第i次主循環中推f[v]時)能夠得到f[v]和f[v -c]的值呢?事實上,這要求在每次主循環中我們以v=V..0的順序推f[v],這樣才能保證推f[v]時f[v-c]保存的是狀態f[v-c]的值。偽代碼如下:
for i=1..N
for v=V..0
f[v]=max{f[v],f[v-c]+w};
其中的f[v]=max{f[v],f[v-c]}一句恰就相當於我們的轉移方程f[v]=max{f[v],f[v-c]},因為現在的f[v-c]就相當於原來的f[v-c]。如果將v的循環順序從上面的逆序改成順序的話,那麼則成了f[v]由f[v-c]推知,與本題意不符,但它卻是另一個重要的背包問題P02最簡捷的解決方案,故學習只用一維數組解01背包問題是十分必要的。
總結
01背包問題是最基本的背包問題,它包含了背包問題中設計狀態、祥旦方程的最基本思想,另外,別的類型的背包問題往往也可以轉換成01背包問題求解。故一定要仔細體會上面基本思路的得出方法,狀態轉移方程的意義,以及最後怎樣優化的空間復雜度。
P02: 完全背包問題
題目
有N種物品和一個容量為V的背包,每種物品都有無限件可用。第i種物品的費用是c,價值是w。求解將哪些物品裝入背包可使這些物品的費用總和不超過背包容量,且價值總和最大。
基本思路
這個問題非常類似於01背包問題,所不同的是每種物品有無限件。也就是從每種物品的角度考慮,與它相關的策略已並非取或不取兩種,而是有取0件、取1件、取2件……等很多種。如果仍然按照解01背包時的思路,令f[v]表示前i種物品恰放入一個容量為v的背包的最大權值。仍然可以按照每種物品不同的策略寫出狀態轉移方程,像這樣:f[v]=max{f[v-k*c]+k*w|0<=k*c<= v}。這跟01背包問題一樣有O(N*V)個狀態需要求解,但求解每個狀態的時間則不是常數了,求解狀態f[v]的時間是O(v/c),總的復雜度是超過O(VN)的。
將01背包問題的基本思路加以改進,得到了這樣一個清晰的方法。這說明01背包問題的方程的確是很重要,可以推及其它類型的背包問題。但我們還是試圖改進這個復雜度。
一個簡單有效的優化
完全背包問題有一個很簡單有效的優化,是這樣的:若兩件物品i、j滿足c<=c[j]且w>=w[j],則將物品j去掉,不用考慮。這個優化的正確性顯然:任何情況下都可將價值小費用高得j換成物美價廉的i,得到至少不會更差的方案。對於隨機生成的數據,這個方法往往會大大減少物品的件數,從而加快速度。然而這個並不能改善最壞情況的復雜度,因為有可能特別設計的數據可以一件物品也去不掉。
轉化為01背包問題求解
既然01背包問題是最基本的背包問題,那麼我們可以考慮把完全背包問題轉化為01背包問題來解。最簡單的想法是,考慮到第i種物品最多選V/c 件,於是可以把第i種物品轉化為V/c件費用及價值均不變的物品,然後求解這個01背包問題。這樣完全沒有改進基本思路的時間復雜度,但這畢竟給了我們將完全背包問題轉化為01背包問題的思路:將一種物品拆成多件物品。
更高效的轉化方法是:把第i種物品拆成費用為c*2^k、價值為w*2^k的若干件物品,其中k滿足c*2^k<V。這是二進制的思想,因為不管最優策略選幾件第i種物品,總可以表示成若干個2^k件物品的和。這樣把每種物品拆成O(log(V/c))件物品,是一個很大的改進。 但我們有更優的O(VN)的演算法。 * O(VN)的演算法 這個演算法使用一維數組,先看偽代碼: <pre class"example"> for i=1..N for v=0..V f[v]=max{f[v],f[v-c]+w};
你會發現,這個偽代碼與P01的偽代碼只有v的循環次序不同而已。為什麼這樣一改就可行呢?首先想想為什麼P01中要按照v=V..0的逆序來循環。這是因為要保證第i次循環中的狀態f[v]是由狀態f[v-c]遞推而來。換句話說,這正是為了保證每件物品只選一次,保證在考慮「選入第i件物品」這件策略時,依據的是一個絕無已經選入第i件物品的子結果f[v-c]。而現在完全背包的特點恰是每種物品可選無限件,所以在考慮「加選一件第i種物品」這種策略時,卻正需要一個可能已選入第i種物品的子結果f[v-c],所以就可以並且必須採用v= 0..V的順序循環。這就是這個簡單的程序為何成立的道理。
這個演算法也可以以另外的思路得出。例如,基本思路中的狀態轉移方程可以等價地變形成這種形式:f[v]=max{f[v],f[v-c]+w},將這個方程用一維數組實現,便得到了上面的偽代碼。
總結
完全背包問題也是一個相當基礎的背包問題,它有兩個狀態轉移方程,分別在「基本思路」以及「O(VN)的演算法「的小節中給出。希望你能夠對這兩個狀態轉移方程都仔細地體會,不僅記住,也要弄明白它們是怎麼得出來的,最好能夠自己想一種得到這些方程的方法。事實上,對每一道動態規劃題目都思考其方程的意義以及如何得來,是加深對動態規劃的理解、提高動態規劃功力的好方法。
P03: 多重背包問題
題目
有N種物品和一個容量為V的背包。第i種物品最多有n件可用,每件費用是c,價值是w。求解將哪些物品裝入背包可使這些物品的費用總和不超過背包容量,且價值總和最大。
基本演算法
這題目和完全背包問題很類似。基本的方程只需將完全背包問題的方程略微一改即可,因為對於第i種物品有n+1種策略:取0件,取1件……取 n件。令f[v]表示前i種物品恰放入一個容量為v的背包的最大權值,則:f[v]=max{f[v-k*c]+ k*w|0<=k<=n}。復雜度是O(V*∑n)。
轉化為01背包問題
另一種好想好寫的基本方法是轉化為01背包求解:把第i種物品換成n件01背包中的物品,則得到了物品數為∑n的01背包問題,直接求解,復雜度仍然是O(V*∑n)。
但是我們期望將它轉化為01背包問題之後能夠像完全背包一樣降低復雜度。仍然考慮二進制的思想,我們考慮把第i種物品換成若干件物品,使得原問題中第i種物品可取的每種策略——取0..n件——均能等價於取若干件代換以後的物品。另外,取超過n件的策略必不能出現。
方法是:將第i種物品分成若干件物品,其中每件物品有一個系數,這件物品的費用和價值均是原來的費用和價值乘以這個系數。使這些系數分別為 1,2,4,...,2^(k-1),n-2^k+1,且k是滿足n-2^k+1>0的最大整數。例如,如果n為13,就將這種物品分成系數分別為1,2,4,6的四件物品。
分成的這幾件物品的系數和為n,表明不可能取多於n件的第i種物品。另外這種方法也能保證對於0..n間的每一個整數,均可以用若干個系數的和表示,這個證明可以分0..2^k-1和2^k..n兩段來分別討論得出,並不難,希望你自己思考嘗試一下。
這樣就將第i種物品分成了O(log n)種物品,將原問題轉化為了復雜度為O(V*∑log n)的01背包問題,是很大的改進。
O(VN)的演算法
多重背包問題同樣有O(VN)的演算法。這個演算法基於基本演算法的狀態轉移方程,但應用單調隊列的方法使每個狀態的值可以以均攤O(1)的時間求解。由於用單調隊列優化的DP已超出了NOIP的范圍,故本文不再展開講解。我最初了解到這個方法是在樓天成的「男人八題」幻燈片上。
小結
這里我們看到了將一個演算法的復雜度由O(V*∑n)改進到O(V*∑log n)的過程,還知道了存在應用超出NOIP范圍的知識的O(VN)演算法。希望你特別注意「拆分物品」的思想和方法,自己證明一下它的正確性,並用盡量簡潔的程序來實現。
P04: 混合三種背包問題
問題
如果將P01、P02、P03混合起來。也就是說,有的物品只可以取一次(01背包),有的物品可以取無限次(完全背包),有的物品可以取的次數有一個上限(多重背包)。應該怎麼求解呢?
01背包與完全背包的混合
考慮到在P01和P02中最後給出的偽代碼只有一處不同,故如果只有兩類物品:一類物品只能取一次,另一類物品可以取無限次,那麼只需在對每個物品應用轉移方程時,根據物品的類別選用順序或逆序的循環即可,復雜度是O(VN)。偽代碼如下:
for i=1..N
if 第i件物品是01背包
for v=V..0
f[v]=max{f[v],f[v-c]+w};
else if 第i件物品是完全背包
for v=0..V
f[v]=max{f[v],f[v-c]+w};
再加上多重背包
如果再加上有的物品最多可以取有限次,那麼原則上也可以給出O(VN)的解法:遇到多重背包類型的物品用單調隊列解即可。但如果不考慮超過NOIP范圍的演算法的話,用P03中將每個這類物品分成O(log n)個01背包的物品的方法也已經很優了。
小結
有人說,困難的題目都是由簡單的題目疊加而來的。這句話是否公理暫且存之不論,但它在本講中已經得到了充分的體現。本來01背包、完全背包、多重背包都不是什麼難題,但將它們簡單地組合起來以後就得到了這樣一道一定能嚇倒不少人的題目。但只要基礎扎實,領會三種基本背包問題的思想,就可以做到把困難的題目拆分成簡單的題目來解決。
P05: 二維費用的背包問題
問題
二維費用的背包問題是指:對於每件物品,具有兩種不同的費用;選擇這件物品必須同時付出這兩種代價;對於每種代價都有一個可付出的最大值(背包容量)。問怎樣選擇物品可以得到最大的價值。設這兩種代價分別為代價1和代價2,第i件物品所需的兩種代價分別為a和b。兩種代價可付出的最大值(兩種背包容量)分別為V和U。物品的價值為w。
演算法
費用加了一維,只需狀態也加一維即可。設f[v]表示前i件物品付出兩種代價分別為v和u時可獲得的最大價值。狀態轉移方程就是:f [v]=max{f[v],f[v-a]]+w}。如前述方法,可以只使用二維的數組:當每件物品只可以取一次時變數v和u採用順序的循環,當物品有如完全背包問題時採用逆序的循環。當物品有如多重背包問題時拆分物品。
物品總個數的限制
有時,「二維費用」的條件是以這樣一種隱含的方式給出的:最多隻能取M件物品。這事實上相當於每件物品多了一種「件數」的費用,每個物品的件數費用均為1,可以付出的最大件數費用為M。換句話說,設f[v][m]表示付出費用v、最多選m件時可得到的最大價值,則根據物品的類型(01、完全、多重)用不同的方法循環更新,最後在f[0..V][0..M]范圍內尋找答案。
另外,如果要求「恰取M件物品」,則在f[0..V][M]范圍內尋找答案。
小結
事實上,當發現由熟悉的動態規劃題目變形得來的題目時,在原來的狀態中加一緯以滿足新的限制是一種比較通用的方法。希望你能從本講中初步體會到這種方法。
P06: 分組的背包問題
問題
有N件物品和一個容量為V的背包。第i件物品的費用是c,價值是w。這些物品被劃分為若干組,每組中的物品互相沖突,最多選一件。求解將哪些物品裝入背包可使這些物品的費用總和不超過背包容量,且價值總和最大。
演算法
這個問題變成了每組物品有若干種策略:是選擇本組的某一件,還是一件都不選。也就是說設f[k][v]表示前k組物品花費費用v能取得的最大權值,則有f[k][v]=max{f[k-1][v],f[k-1][v-c]+w|物品i屬於第k組}。
使用一維數組的偽代碼如下:
for 所有的組k
for 所有的i屬於組k
for v=V..0
f[v]=max{f[v],f[v-c]+w}
另外,顯然可以對每組中的物品應用P02中「一個簡單有效的優化」。
小結
分組的背包問題將彼此互斥的若干物品稱為一個組,這建立了一個很好的模型。不少背包問題的變形都可以轉化為分組的背包問題(例如P07),由分組的背包問題進一步可定義「泛化物品」的概念,十分有利於解題。
P07: 有依賴的背包問題
簡化的問題
這種背包問題的物品間存在某種「依賴」的關系。也就是說,i依賴於j,表示若選物品i,則必須選物品j。為了簡化起見,我們先設沒有某個物品既依賴於別的物品,又被別的物品所依賴;另外,沒有某件物品同時依賴多件物品。
演算法
這個問題由NOIP2006金明的預算方案一題擴展而來。遵從該題的提法,將不依賴於別的物品的物品稱為「主件」,依賴於某主件的物品稱為「附件」。由這個問題的簡化條件可知所有的物品由若干主件和依賴於每個主件的一個附件集合組成。
按照背包問題的一般思路,僅考慮一個主件和它的附件集合。可是,可用的策略非常多,包括:一個也不選,僅選擇主件,選擇主件後再選擇一個附件,選擇主件後再選擇兩個附件……無法用狀態轉移方程來表示如此多的策略。(事實上,設有n個附件,則策略有2^n+1個,為指數級。)
考慮到所有這些策略都是互斥的(也就是說,你只能選擇一種策略),所以一個主件和它的附件集合實際上對應於P06中的一個物品組,每個選擇了主件又選擇了若干個附件的策略對應於這個物品組中的一個物品,其費用和價值都是這個策略中的物品的值的和。但僅僅是這一步轉化並不能給出一個好的演算法,因為物品組中的物品還是像原問題的策略一樣多。
再考慮P06中的一句話: 可以對每組中的物品應用P02中「一個簡單有效的優化」。這提示我們,對於一個物品組中的物品,所有費用相同的物品只留一個價值最大的,不影響結果。所以,我們可以對主件i的「附件集合」先進行一次01背包,得到費用依次為0..V-c所有這些值時相應的最大價值f'[0..V-c]。那麼這個主件及它的附件集合相當於V-c+1個物品的物品組,其中費用為c+k的物品的價值為f'[k]+w。也就是說原來指數級的策略中有很多策略都是冗餘的,通過一次01背包後,將主件i轉化為 V-c+1個物品的物品組,就可以直接應用P06的演算法解決問題了。
更一般的問題
更一般的問題是:依賴關系以圖論中「森林」的形式給出(森林即多叉樹的集合),也就是說,主件的附件仍然可以具有自己的附件集合,限制只是每個物品最多隻依賴於一個物品(只有一個主件)且不出現循環依賴。
解決這個問題仍然可以用將每個主件及其附件集合轉化為物品組的方式。唯一不同的是,由於附件可能還有附件,就不能將每個附件都看作一個一般的01 背包中的物品了。若這個附件也有附件集合,則它必定要被先轉化為物品組,然後用分組的背包問題解出主件及其附件集合所對應的附件組中各個費用的附件所對應的價值。
事實上,這是一種樹形DP,其特點是每個父節點都需要對它的各個兒子的屬性進行一次DP以求得自己的相關屬性。這已經觸及到了「泛化物品」的思想。看完P08後,你會發現這個「依賴關系樹」每一個子樹都等價於一件泛化物品,求某節點為根的子樹對應的泛化物品相當於求其所有兒子的對應的泛化物品之和。
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[v]=max{f[v],f[v-c]+w}。再用一個數組g [v],設g[v]=0表示推出f[v]的值時是採用了方程的前一項(也即f[v]=f[v]),g[v]表示採用了方程的後一項。注意這兩項分別表示了兩種策略:未選第i個物品及選了第i個物品。那麼輸出方案的偽代碼可以這樣寫(設最終狀態為f[N][V]):
i=N
v=V
while(i>0)
if(g[v]==0)
print "未選第i項物品"
else if(g[v]==1)
print "選了第i項物品"
v=v-c
另外,採用方程的前一項或後一項也可以在輸出方案的過程中根據f[v]的值實時地求出來,也即不須紀錄g數組,將上述代碼中的g [v]==0改成f[v]==f[v],g[v]==1改成f[v]==f[v-c]+w也可。
輸出字典序最小的最優方案
這里「字典序最小」的意思是1..N號物品的選擇方案排列出來以後字典序最小。以輸出01背包最小字典序的方案為例。
一般而言,求一個字典序最小的最優方案,只需要在轉移時注意策略。首先,子問題的定義要略改一些。我們注意到,如果存在一個選了物品1的最優方案,那麼答案一定包含物品1,原問題轉化為一個背包容量為v-c[1],物品為2..N的子問題。反之,如果答案不包含物品1,則轉化成背包容量仍為V,物品為2..N的子問題。不管答案怎樣,子問題的物品都是以i..N而非前所述的1..i的形式來定義的,所以狀態的定義和轉移方程都需要改一下。但也許更簡易的方法是先把物品逆序排列一下,以下按物品已被逆序排列來敘述。
在這種情況下,可以按照前面經典的狀態轉移方程來求值,只是輸出方案的時候要注意:從N到1輸入時,如果f[v]==f及f[v]==f[f-c]+w同時成立,應該按照後者(即選擇了物品i)來輸出方案。
求方案總數
對於一個給定了背包容量、物品費用、物品間相互關系(分組、依賴等)的背包問題,除了再給定每個物品的價值後求可得到的最大價值外,還可以得到裝滿背包或將背包裝至某一指定容量的方案總數。
對於這類改變問法的問題,一般只需將狀態轉移方程中的max改成sum即可。例如若每件物品均是01背包中的物品,轉移方程即為f[v]=sum{f[v],f[v-c]+w},初始條件f[0][0]=1。
事實上,這樣做可行的原因在於狀態轉移方程已經考察了所有可能的背包組成方案。
最優方案的總數
這里的最優方案是指物品總價值最大的方案。還是以01背包為例。
結合求最大總價值和方案總數兩個問題的思路,最優方案的總數可以這樣求:f[v]意義同前述,g[v]表示這個子問題的最優方案的總數,則在求f[v]的同時求g[v]的偽代碼如下:
for i=1..N
for v=0..V
f[v]=max{f[v],f[v-c]+w}
g[v]=0
if(f[v]==f[v])
inc(g[v],g[v]
if(f[v]==f[v-c]+w)
inc(g[v],g[v-c])
如果你是第一次看到這樣的問題,請仔細體會上面的偽代碼。
小結
顯然,這里不可能窮盡背包類動態規劃問題所有的問法。甚至還存在一類將背包類動態規劃問題與其它領域(例如數論、圖論)結合起來的問題,在這篇論背包問題的專文中也不會論及。但只要深刻領會前述所有類別的背包問題的思路和狀態轉移方程,遇到其它的變形問法,只要題目難度還屬於NOIP,應該也不難想出演算法。
觸類旁通、舉一反三,應該也是一個OIer應有的品質吧。

⑼ 01背包問題

請搜索」背包九講「,非常詳細,看前兩講或前三講就可以了,以下是節選前兩講。如果是學競賽的話必須要能看懂。

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的背包中」;如果放第i件物品,那麼問題就轉化為「前i-1件物品放入剩下的容量為v-c[i]的背包中」,此時能獲得的最大價值就是f [i-1][v-c[i]]再加上通過放入第i件物品獲得的價值w[i]。

注意f[i][v]有意義當且僅當存在一個前i件物品的子集,其費用總和為v。所以按照這個方程遞推完畢後,最終的答案並不一定是f[N] [V],而是f[N][0..V]的最大值。如果將狀態的定義中的「恰」字去掉,在轉移方程中就要再加入一項f[i][v-1],這樣就可以保證f[N] [V]就是最後的答案。至於為什麼這樣就可以,由你自己來體會了。

優化空間復雜度
以上方法的時間和空間復雜度均為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背包問題求解。故一定要仔細體會上面基本思路的得出方法,狀態轉移方程的意義,以及最後怎樣優化的空間復雜度。

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,得到至少不會更差的方案。對於隨機生成的數據,這個方法往往會大大減少物品的件數,從而加快速度。然而這個並不能改善最壞情況的復雜度,因為有可能特別設計的數據可以一件物品也去不掉。

轉化為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)的演算法 這個演算法使用一維數組,先看偽代碼: <pre class"example"> for i=1..N for v=0..V f[v]=max{f[v],f[v-c[i]]+w[i]};

你會發現,這個偽代碼與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]},將這個方程用一維數組實現,便得到了上面的偽代碼。

總結
完全背包問題也是一個相當基礎的背包問題,它有兩個狀態轉移方程,分別在「基本思路」以及「O(VN)的演算法「的小節中給出。希望你能夠對這兩個狀態轉移方程都仔細地體會,不僅記住,也要弄明白它們是怎麼得出來的,最好能夠自己想一種得到這些方程的方法。事實上,對每一道動態規劃題目都思考其方程的意義以及如何得來,是加深對動態規劃的理解、提高動態規劃功力的好方法。

熱點內容
android開發必備 發布:2025-05-19 22:36:08 瀏覽:888
硬碟緩存什麼用 發布:2025-05-19 22:09:41 瀏覽:12
蘋果筆記本配置好的有哪些 發布:2025-05-19 22:08:57 瀏覽:16
oracle存儲過程中批量修改表結構 發布:2025-05-19 22:02:22 瀏覽:521
php支付寶sdk 發布:2025-05-19 22:01:06 瀏覽:603
雲掃墓源碼 發布:2025-05-19 22:00:32 瀏覽:594
executeupdatesql 發布:2025-05-19 21:58:36 瀏覽:218
中國電信如何轉人工密碼是多少 發布:2025-05-19 21:44:54 瀏覽:209
求階乘的c語言 發布:2025-05-19 21:15:20 瀏覽:965
話嘮安卓哪裡下載 發布:2025-05-19 20:27:04 瀏覽:166