終于來(lái)到最后一個(gè)系列了,整個(gè)系列下來(lái)發(fā)現(xiàn)大神的總結(jié)和思考就是那么的厲害,自己能在這里學(xué)習(xí)和了解不同的思維方式并能運(yùn)用到實(shí)際題目中就感覺(jué)這套系列沒(méi)白做了。那么下面就進(jìn)入最后一個(gè)系列吧。
7 有依賴的背包問(wèn)題
7.1 簡(jiǎn)化的問(wèn)題
這種背包問(wèn)題的物品間存在某種“依賴”的關(guān)系。也就是說(shuō),物品i 依賴于物品j,表示若選物品i,則必須選物品j。為了簡(jiǎn)化起見(jiàn),我們先設(shè)沒(méi)有某個(gè)物品既依賴于別的物品,又被別的物品所依賴;另外,沒(méi)有某件物品同時(shí)依賴多件物品。
7.2 算法
這個(gè)問(wèn)題由NOIP2006 中“金明的預(yù)算方案”一題擴(kuò)展而來(lái)。遵從該題的提法,將不依賴于別的物品的物品稱為“主件”,依賴于某主件的物品稱為“附件”。由這個(gè)問(wèn)題的簡(jiǎn)化條件可知所有的物品由若干主件和依賴于每個(gè)主件的一個(gè)附件集合組成。
按照背包問(wèn)題的一般思路,僅考慮一個(gè)主件和它的附件集合。可是,可用的策略非常多,包括:一個(gè)也不選,僅選擇主件,選擇主件后再選擇一個(gè)附件,選擇主件后再選擇兩個(gè)附件……無(wú)法用狀態(tài)轉(zhuǎn)移方程來(lái)表示如此多的策略。事實(shí)上,設(shè)有n 個(gè)附件,則策略有2n + 1 個(gè),為指數(shù)級(jí)。
考慮到所有這些策略都是互斥的(也就是說(shuō),你只能選擇一種策略),所以一個(gè)主件和它的附件集合實(shí)際上對(duì)應(yīng)于6中的一個(gè)物品組,每個(gè)選擇了主件又選擇了若干個(gè)附件的策略對(duì)應(yīng)于這個(gè)物品組中的一個(gè)物品,其費(fèi)用和價(jià)值都是這個(gè)策略中的物品的值的和。但僅僅是這一步轉(zhuǎn)化并不能給出一個(gè)好的算法,因?yàn)槲锲方M中的物品還是像原問(wèn)題的策略一樣多。
再考慮對(duì)每組內(nèi)的物品應(yīng)用2.3中的優(yōu)化。我們可以想到,對(duì)于第k 個(gè)物品組中的物品,所有費(fèi)用相同的物品只留一個(gè)價(jià)值最大的,不影響結(jié)果。所以,可以對(duì)主件k 的“附件集合”先進(jìn)行一次01 背包,得到費(fèi)用依次為0...V -Ck 所有這些值時(shí)相應(yīng)的最大價(jià)值Fk[0 ... V - Ck]。那么,這個(gè)主件及它的附件集合相當(dāng)于V - Ck + 1 個(gè)物品的物品組,其中費(fèi)用為v 的物品的價(jià)值為Fk[v -Ck]+Wk,v 的取值范圍是Ck ≤ v ≤ V 。
也就是說(shuō),原來(lái)指數(shù)級(jí)的策略中,有很多策略都是冗余的,通過(guò)一次01 背包后,將主件k 及其附件轉(zhuǎn)化為V - Ck + 1 個(gè)物品的物品組,就可以直接應(yīng)用6的算法解決問(wèn)題了。
下面就附上NOIP2006金明的預(yù)算這一題
描述
金明今天很開(kāi)心,家里購(gòu)置的新房就要領(lǐng)鑰匙了,新房里有一間金明自己專用的很寬敞的房間。更讓他高興的是,媽媽昨天對(duì)他說(shuō):“你的房間需要購(gòu)買(mǎi)哪些物品,怎么布置,你說(shuō)了算,只要不超過(guò)N元錢(qián)就行”。今天一早,金明就開(kāi)始做預(yù)算了,他把想買(mǎi)的物品分為兩類:主件與附件,附件是從屬于某個(gè)主件的,下表就是一些主件與附件的例子:主件 附件電腦 打印機(jī),掃描儀書(shū)柜 圖書(shū)書(shū)桌 臺(tái)燈,文具工作椅 無(wú)如果要買(mǎi)歸類為附件的物品,必須先買(mǎi)該附件所屬的主件。每個(gè)主件可以有0個(gè)、1個(gè)或2個(gè)附件。附件不再有從屬于自己的附件。金明想買(mǎi)的東西很多,肯定會(huì)超過(guò)媽媽限定的N元。于是,他把每件物品規(guī)定了一個(gè)重要度,分為5等:用整數(shù)1~5表示,第5等最重要。他還從因特網(wǎng)上查到了每件物品的價(jià)格(都是10元的整數(shù)倍)。他希望在不超過(guò)N元(可以等于N元)的前提下,使每件物品的價(jià)格與重要度的乘積的總和最大。
設(shè)第j件物品的價(jià)格為v[j],重要度為w[j],共選中了k件物品,編號(hào)依次為j1,j2,……,jk,則所求的總和為:v[j1]*w[j1]+v[j2]*w[j2]+ …+v[jk]*w[jk]。(其中*為乘號(hào))請(qǐng)你幫助金明設(shè)計(jì)一個(gè)滿足要求的購(gòu)物單。
格式
輸入格式
輸入文件的第1行,為兩個(gè)正整數(shù),用一個(gè)空格隔開(kāi):N m 其中N(<32000)表示總錢(qián)數(shù),m(<60)為希望購(gòu)買(mǎi)物品的個(gè)數(shù)。)從第2行到第m\+1行,第j行給出了編號(hào)為j\-1的物品的基本數(shù)據(jù),每行有3個(gè)非負(fù)整數(shù)v p q(其中v表示該物品的價(jià)格(v<10000),p表示該物品的重要度(1~5),q表示該物品是主件還是附件。如果q=0,表示該物品為主件,如果q>0,表示該物品為附件,q是所屬主件的編號(hào))
輸出格式
輸出文件只有一個(gè)正整數(shù),為不超過(guò)總錢(qián)數(shù)的物品的價(jià)格與重要度乘積的總和的最大值(<200000)。
樣例1
樣例輸入1
1000 5800 2 0400 5 1300 5 1400 3 0500 2 0
Copy
樣例輸出1
2200
這一題是我3個(gè)系列以來(lái)對(duì)于我來(lái)說(shuō)最蛋疼的一題,通過(guò)文章中的講解還是比較好理解這個(gè)思路的(要我自己想,那是肯定想不出來(lái)的),難就難在編程上,實(shí)踐能力對(duì)我來(lái)說(shuō)確實(shí)比較弱,費(fèi)了很長(zhǎng)的功夫最后才解出來(lái)這道題。其中還參考了一下別人的做法,一開(kāi)始我是按照思路一步步來(lái)但是總是有些地方出錯(cuò)導(dǎo)致答案有誤。參考了一下別人的做法,別人有的做法是在一開(kāi)始就把主件和附件綁定在一起。但是我偏不,我就想按照文章中的思路去解。先求附件集合,再和主件組合成一個(gè)物品組。
下面我貼一下代碼,代碼比較長(zhǎng)。代碼里面有對(duì)主要步驟的注釋。
#include <iostream>
#include <vector>
using namespace std;
const int N=60;
struct Node
{
int w,val;
Node(int ww,int vval):w(ww),val(vval){}
};
int mx[80][32000 + 20];
vector<Node> gg[N+1];
vector<int> master; //主件集合 master[i]=k 代表第i個(gè)主件的編號(hào)為k
vector<int> host[N+1]; //附件集合 host[i][j]代表主鍵編號(hào)為i的附件的編號(hào)為j
int *dp[N+1]; //每組的附件集合經(jīng)過(guò)01背包處理后的價(jià)值數(shù)組 dp[i][j]代表主件編號(hào)為i花費(fèi)為j的最大價(jià)值
int v[N+1];// 每件物品價(jià)格數(shù)組
int w[N+1];// 每件物品的重要度數(shù)組
int dp_1[32000]; //dp_1[j] 代表花費(fèi)金額為j時(shí)的物品價(jià)格*重要度的總和最大
int max(int a,int b)
{
return a>b?a:b;
}
int main()
{
int N,m;
cin>>N>>m;
int i,vi,pi,qi;
for(i=1;i<=m;i++)
{
cin>>vi>>pi>>qi;
if(qi==0)//是主件
{
master.push_back(i);
}
else // 是附件
{
host[qi].push_back(i);
}
v[i]=vi;
w[i]=pi;
}
for(i=0;i<master.size();i++)
{
int code=master[i];
dp[code]=new int [N+1];
memset(dp[code],0,(N+1)*sizeof(int));
if(host[code].size()!=0)//說(shuō)明此主件有附件存在
{
for(int j=0;j<host[code].size();j++)//對(duì)附件集合做01背包處理
{
int weight=v[host[code][j]];
int value=w[host[code][j]];
for(int m=N;m>=weight;m--)//01背包過(guò)程
{
dp[code][m]=max(dp[code][m],dp[code][m-weight]+weight*value);
}
}
}
else//只有主件沒(méi)有附件
{
int weight=v[code];
int value=w[code];
for(int m=N;m>=weight;m--)//對(duì)單獨(dú)主件做01背包處理
{
dp[code][m]=max(dp[code][m],dp[code][m-weight]+weight*value);
}
}
for (int j = 0; j <= N; j++) {//這一塊的目的主要是挑選相同花費(fèi)的情況下價(jià)值最大的背包
if (dp[code][j] == 0) continue;
if (mx[i][j] >= dp[code][j]) {
mx[i][j + 1] = mx[i][j];
continue;
}
mx[i][j] = dp[code][j];
mx[i][j + 1] = mx[i][j];
gg[code].push_back(Node(j, mx[i][j]));
}
}
//分組背包實(shí)踐
for(i=0;i<master.size();i++)//遍歷每一個(gè)組(主件)
{
int code=master[i];
int weight=v[code];
int value=w[code];
for(int j=N;j>=0;j--)//遍歷總錢(qián)數(shù)
{
if(host[code].size()==0)
{
for(int k=0;k<gg[code].size();k++)
{
if(j-gg[code][k].w>=0)
{
//不算入主件(此時(shí)依賴的dp_1的值中已經(jīng)包含有主件)
dp_1[j]=max(dp_1[j],dp_1[j-gg[code][k].w]+gg[code][k].val);
}
}
}
else
{
for(int k=0;k<gg[code].size();k++)//遍歷每組中的成員物品
{
if(j-gg[code][k].w-weight>=0)
{
//算入主件
dp_1[j]=max(dp_1[j],dp_1[j-gg[code][k].w-weight]+gg[code][k].val+weight*value);
}
}
}
}
}
cout<<dp_1[N]<<endl;
return 0;
}
再附上參考的另一種做法的鏈接,在挑選相同花費(fèi)的情況下價(jià)值最大的背包參考了一下它的方式。其他的都是按照自己的編寫(xiě)的。差點(diǎn)就想直接用他的了,可是還是忍住自己去實(shí)現(xiàn)。廢了很長(zhǎng)時(shí)間,今天想不到,就明天再想。相信自己,總會(huì)有解決的時(shí)刻。
因?yàn)槲液孟駴](méi)找到網(wǎng)上有OJ上有原題去提交,為了大家方便測(cè)試,我附上一個(gè)測(cè)試數(shù)據(jù)的下載鏈接。里面有這套題的測(cè)試用例下載,自己試試就知道對(duì)不對(duì)了。
------下面回到文章-------
7.3 較一般的問(wèn)題
更一般的問(wèn)題是:依賴關(guān)系以圖論中“森林”3的形式給出。也就是說(shuō),主件的附件仍然可以具有自己的附件集合。限制只是每個(gè)物品最多只依賴于一個(gè)物品(只有一個(gè)主件)且不出現(xiàn)循環(huán)依賴。(即多叉樹(shù)的集合)
解決這個(gè)問(wèn)題仍然可以用將每個(gè)主件及其附件集合轉(zhuǎn)化為物品組的方式。唯一不同的是,由于附件可能還有附件,就不能將每個(gè)附件都看作一個(gè)一般的01 背包中的物品了。若這個(gè)附件也有附件集合,則它必定要被先轉(zhuǎn)化為物品組,然后用分組的背包問(wèn)題解出主件及其附件集合所對(duì)應(yīng)的附件組中各個(gè)費(fèi)用的附件所對(duì)應(yīng)的價(jià)值。事實(shí)上,這是一種樹(shù)形動(dòng)態(tài)規(guī)劃,其特點(diǎn)是,在用動(dòng)態(tài)規(guī)劃求每個(gè)父節(jié)點(diǎn)的屬性之前,需要對(duì)它的各個(gè)兒子的屬性進(jìn)行一次動(dòng)態(tài)規(guī)劃式的求值。這已經(jīng)觸及到了“泛化物品”的思想。看完8后,你會(huì)發(fā)現(xiàn)這個(gè)“依賴關(guān)系樹(shù)”每一個(gè)子樹(shù)都等價(jià)于一件泛化物品,求某節(jié)點(diǎn)為根的子樹(shù)對(duì)應(yīng)的泛化物品相當(dāng)于求其所有兒子的對(duì)應(yīng)的泛化物品之和。
7.4 小結(jié)
NOIP2006 的那道背包問(wèn)題我做得很失敗,寫(xiě)了上百行的代碼,卻一分未得。后來(lái)我通過(guò)思考發(fā)現(xiàn)通過(guò)引入“物品組”和“依賴”的概念可以加深對(duì)這題的理解,還可以解決它的推廣問(wèn)題。用物品組的思想考慮那題中極其特殊的依賴關(guān)系:物品不能既作主件又作附件,每個(gè)主件最多有兩個(gè)附件,可以發(fā)現(xiàn)一個(gè)主件和它的兩個(gè)附件等價(jià)于一個(gè)由四個(gè)物品組成的物品組,這便揭示了問(wèn)題的某種本質(zhì)。后來(lái),我在《背包問(wèn)題九講》第一版中總結(jié)此事時(shí)說(shuō):“失敗不是什么丟人的事情,從失敗中全無(wú)收獲才是。”之后的NOIP2007 的比賽中,我得了滿分。
失敗不是什么丟人的事情,從失敗中全無(wú)收獲才是
重點(diǎn)劃下,考試要考
8 泛化物品
8.1 定義
考慮這樣一種物品,它并沒(méi)有固定的費(fèi)用和價(jià)值,而是它的價(jià)值隨著你分配給它的費(fèi)用而變化。這就是泛化物品的概念。
更嚴(yán)格的定義之。在背包容量為V 的背包問(wèn)題中,泛化物品是一個(gè)定義域?yàn)?... V中的整數(shù)的函數(shù)h,當(dāng)分配給它的費(fèi)用為v 時(shí),能得到的價(jià)值就是h(v)。這個(gè)定義有一點(diǎn)點(diǎn)抽象,另一種理解是一個(gè)泛化物品就是一個(gè)數(shù)組h[0 ...V ],給它費(fèi)用v,可得到價(jià)值h[v]。一個(gè)費(fèi)用為c 價(jià)值為w 的物品,如果它是01 背包中的物品,那么把它看成泛化物品,它就是除了h(c) = w 外,其它函數(shù)值都為0 的一個(gè)函數(shù)。如果它是完全背包中的物品,那么它可以看成這樣一個(gè)函數(shù),僅當(dāng)v 被c 整除時(shí)有h(v) = w * (v/c),其它函數(shù)值均為0。如果它是多重背包中重復(fù)次數(shù)最多為m 的物品,那么它對(duì)應(yīng)的泛化物品的函數(shù)有h(v) = w * (v/c)僅當(dāng)v 被c 整除且v/c ≤ m,其它情況函數(shù)值均為0。
一個(gè)物品組可以看作一個(gè)泛化物品h。對(duì)于一個(gè)0... V 中的v,若物品組中不存在費(fèi)用為v 的物品,則h(v) = 0,否則h(v) 取值為所有費(fèi)用為v 的物品的最大價(jià)值。6中每個(gè)主件及其附件集合等價(jià)于一個(gè)物品組,自然也可看作一個(gè)泛化物品。
8.2 泛化物品的和
如果給定了兩個(gè)泛化物品h 和l,要用一定的費(fèi)用從這兩個(gè)泛化物品中得到最大的價(jià)值,這個(gè)問(wèn)題怎么求呢?事實(shí)上,對(duì)于一個(gè)給定的費(fèi)用v,只需枚舉將這個(gè)費(fèi)用如何分配給兩個(gè)泛化物品就可以了。同樣的,對(duì)于0: : :V 中的每一個(gè)整數(shù)v,可以求得費(fèi)用v 分配到h 和l 中的最大價(jià)值f(v)。也即
可以看到,這里的f 是一個(gè)由泛化物品h 和l 決定的定義域?yàn)?...V 的函數(shù),也就是說(shuō),f 是一個(gè)由泛化物品h 和l 決定的泛化物品。我們將f 定義為泛化物品h 和l 的和:h、l 都是泛化物品,若函數(shù)f 滿足以上關(guān)系式,則稱f 是h 與l 的和。泛化物品和運(yùn)算的時(shí)間復(fù)雜度取決于背包的容量,是O(V^2)。
由泛化物品的定義可知:在一個(gè)背包問(wèn)題中,若將兩個(gè)泛化物品代以它們的和,不影響問(wèn)題的答案。事實(shí)上,對(duì)于其中的物品都是泛化物品的背包問(wèn)題,求它的答案的過(guò)程也就是求所有這些泛化物品之和的過(guò)程。若問(wèn)題的和為s,則答案就是s(0... V ) 中的最大值。
8.3 背包問(wèn)題的泛化物品
一個(gè)背包問(wèn)題中,可能會(huì)給出很多條件,包括每種物品的費(fèi)用、價(jià)值等屬性,物品之間的分組、依賴等關(guān)系等。但肯定能將問(wèn)題對(duì)應(yīng)于某個(gè)泛化物品。也就是說(shuō),給定了所有條件以后,就可以對(duì)每個(gè)非負(fù)整數(shù)v 求得:若背包容量為v,將物品裝入背包可得到的最大價(jià)值是多少,這可以認(rèn)為是定義在非負(fù)整數(shù)集上的一件泛化物品。這個(gè)泛化物品——或者說(shuō)問(wèn)題所對(duì)應(yīng)的一個(gè)定義域?yàn)榉秦?fù)整數(shù)的函數(shù)——包含了關(guān)于問(wèn)題本身的高度濃縮的信息。一般而言,求得這個(gè)泛化物品的一個(gè)子定義域(例如0...V )的值之后,就可以根據(jù)這個(gè)函數(shù)的取值得到背包問(wèn)題的最終答案。
綜上所述,一般而言,求解背包問(wèn)題,即求解這個(gè)問(wèn)題所對(duì)應(yīng)的一個(gè)函數(shù),即該問(wèn)題的泛化物品。而求解某個(gè)泛化物品的一種常用方法就是將它表示為若干泛化物品的和然后求之。
8.4 小結(jié)
本講是我在學(xué)習(xí)函數(shù)式編程的Scheme 語(yǔ)言時(shí),用函數(shù)編程的眼光審視各類背包問(wèn)題得出的理論。
我想說(shuō):“思考”是一個(gè)程序員最重要的品質(zhì)。簡(jiǎn)單的問(wèn)題,深入思考以后,也能發(fā)現(xiàn)更多。
9 背包問(wèn)題問(wèn)法的變化
以上涉及的各種背包問(wèn)題都是要求在背包容量(費(fèi)用)的限制下求可以取到的最大價(jià)值,但背包問(wèn)題還有很多種靈活的問(wèn)法,在這里值得提一下。但是我認(rèn)為,只要深入理解了求背包問(wèn)題最大價(jià)值的方法,即使問(wèn)法變化了,也是不難想出算法的。
例如,求解最多可以放多少件物品或者最多可以裝滿多少背包的空間。這都可以根據(jù)具體問(wèn)題利用前面的方程求出所有狀態(tài)的值(F 數(shù)組)之后得到。
還有,如果要求的是“總價(jià)值最小”“總件數(shù)最小”,只需將狀態(tài)轉(zhuǎn)移方程中的max改成min 即可。
下面說(shuō)一些變化更大的問(wèn)法。
9.1 輸出方案
一般而言,背包問(wèn)題是要求一個(gè)最優(yōu)值,如果要求輸出這個(gè)最優(yōu)值的方案,可以參照一般動(dòng)態(tài)規(guī)劃問(wèn)題輸出方案的方法:記錄下每個(gè)狀態(tài)的最優(yōu)值是由狀態(tài)轉(zhuǎn)移方程的哪一項(xiàng)推出來(lái)的,換句話說(shuō),記錄下它是由哪一個(gè)策略推出來(lái)的。便可根據(jù)這條策略找到上一個(gè)狀態(tài),從上一個(gè)狀態(tài)接著向前推即可。
還是以01 背包為例,方程為F[i; v] = maxfF[i-1, v]; F[i-1, v -Ci]+Wi。再用一個(gè)數(shù)組G[i,v],設(shè)G[i, v] = 0 表示推出F[i; v] 的值時(shí)是采用了方程的前一項(xiàng)(也即F[i, v] = F[i - 1, v]),G[i, v] = 1 表示采用了方程的后一項(xiàng)。注意這兩項(xiàng)分別表示了兩種策略:未選第i 個(gè)物品及選了第i 個(gè)物品。那么輸出方案的偽代碼可以這樣寫(xiě)(設(shè)最終狀態(tài)為F[N, V ]):
另外,采用方程的前一項(xiàng)或后一項(xiàng)也可以在輸出方案的過(guò)程中根據(jù)F[i; v] 的值實(shí)時(shí)地求出來(lái)。也即,不須紀(jì)錄G 數(shù)組,將上述代碼中的G[i, v] = 0 改成F[i,v] = F[i - 1, v],G[i, v] = 1 改成F[i, v] = F[i - 1][v - Ci] +Wi 也可。
下面我貼一下實(shí)現(xiàn)代碼
/* 輸出選擇方案
i=n-1;
j=w;
while(i>=0)
{
if(dp_1[i][j]==dp_1[i-1][j])
{
cout<<"未選第 "<<i<<" 件物品"<<endl;
}
else if(dp_1[i][j]==dp_1[i-1][j-w1[i]]+v[i])
{
cout<<"選第 "<<i<<" 件物品"<<endl;
j=w-w1[i];
}
i--;
}
*/
9.2 輸出字典序最小的最優(yōu)方案
這里“字典序最小”的意思是1 : : :N 號(hào)物品的選擇方案排列出來(lái)以后字典序最小。
以輸出01 背包最小字典序的方案為例。
一般而言,求一個(gè)字典序最小的最優(yōu)方案,只需要在轉(zhuǎn)移時(shí)注意策略。
首先,子問(wèn)題的定義要略改一些。我們注意到,如果存在一個(gè)選了物品1 的最優(yōu)方案,那么答案一定包含物品1,原問(wèn)題轉(zhuǎn)化為一個(gè)背包容量為V - C1,物品為2 ...N的子問(wèn)題。反之,如果答案不包含物品1,則轉(zhuǎn)化成背包容量仍為V ,物品為2...N的子問(wèn)題。
不管答案怎樣,子問(wèn)題的物品都是以i ...N 而非前所述的1 ... i 的形式來(lái)定義的,所以狀態(tài)的定義和轉(zhuǎn)移方程都需要改一下。
但也許更簡(jiǎn)易的方法是,先把物品編號(hào)做x ← N + 1 - x 的變換,在輸出方案時(shí)再變換回來(lái)。在做完物品編號(hào)的變換后,可以按照前面經(jīng)典的轉(zhuǎn)移方程來(lái)求值。只是在輸出方案時(shí)要注意,如果F[i,v] = F[i - 1, v] 和F[i, v] = F[i - 1][v - Ci] + Wi 都成立,應(yīng)該按照后者來(lái)輸出方案,即選擇了物品i,輸出其原來(lái)的編號(hào)N - 1 - i。
9.3 求方案總數(shù)
對(duì)于一個(gè)給定了背包容量、物品費(fèi)用、物品間相互關(guān)系(分組、依賴等)的背包問(wèn)題,除了再給定每個(gè)物品的價(jià)值后求可得到的最大價(jià)值外,還可以得到裝滿背包或?qū)⒈嘲b至某一指定容量的方案總數(shù)。
對(duì)于這類改變問(wèn)法的問(wèn)題,一般只需將狀態(tài)轉(zhuǎn)移方程中的max 改成sum 即可。例如若每件物品均是完全背包中的物品,轉(zhuǎn)移方程即為
初始條件是F[0; 0] = 1。
事實(shí)上,這樣做可行的原因在于狀態(tài)轉(zhuǎn)移方程已經(jīng)考察了所有可能的背包組成方案。
在我對(duì)著這個(gè)偽代碼理解一陣子之后,忽然想到這不就是找零錢(qián)問(wèn)題的實(shí)踐嗎?就說(shuō)這個(gè)偽代碼那么眼熟,其實(shí)和找零錢(qián)的動(dòng)態(tài)規(guī)劃問(wèn)題就是一樣的
下面附上找零錢(qián)的題目(這道題可以在牛客網(wǎng)上找到):
有數(shù)組penny,penny中所有的值都為正數(shù)且不重復(fù)。每個(gè)值代表一種面值的貨幣,每種面值的貨幣可以使用任意張,再給定一個(gè)整數(shù)aim(小于等于1000)代表要找的錢(qián)數(shù),求換錢(qián)有多少種方法。
給定數(shù)組penny及它的大小(小于等于50),同時(shí)給定一個(gè)整數(shù)aim,請(qǐng)返回有多少種方法可以湊成aim。
測(cè)試樣例:
[1,2,4],3,3
返回:2
下面是AC代碼:
#include <iostream>
#include <vector>
using namespace std;
const int N=1000;
const int P=50;
int dp[P][N+1];
class Exchange {
public:
int countWays(vector<int> penny, int n, int aim) {
int i;
for(i=0;i<=aim;i++)
{
if(i%penny[0]==0)
{
dp[0][i]=1;
}
else
{
dp[0][i]=0;
}
}
for(i=0;i<n;i++)
{
dp[i][0]=1;
}
int j;
for(i=1;i<n;i++)
{
for(j=1;j<=aim;j++)
{
if(j-penny[i]>=0)
{
dp[i][j]=dp[i-1][j]+dp[i][j-penny[i]];
}
else
{
dp[i][j]=dp[i-1][j];
}
}
}
return dp[n-1][aim];
}
};
9.4 最優(yōu)方案的總數(shù)
這里的最優(yōu)方案是指物品總價(jià)值最大的方案。以01 背包為例。結(jié)合求最大總價(jià)值和方案總數(shù)兩個(gè)問(wèn)題的思路,最優(yōu)方案的總數(shù)可以這樣求:F[i, v] 代表該狀態(tài)的最大價(jià)值,G[i, v] 表示這個(gè)子問(wèn)題的最優(yōu)方案的總數(shù),則在求F[i; v] 的同時(shí)求G[i; v] 的偽代碼如下:
如果你是第一次看到這樣的問(wèn)題,請(qǐng)仔細(xì)體會(huì)上面的偽代碼。
9.5 求次優(yōu)解、第K 優(yōu)解
對(duì)于求次優(yōu)解、第K 優(yōu)解類的問(wèn)題,如果相應(yīng)的最優(yōu)解問(wèn)題能寫(xiě)出狀態(tài)轉(zhuǎn)移方程、用動(dòng)態(tài)規(guī)劃解決,那么求次優(yōu)解往往可以相同的復(fù)雜度解決,第K 優(yōu)解則比求最優(yōu)解的復(fù)雜度上多一個(gè)系數(shù)K。其基本思想是,將每個(gè)狀態(tài)都表示成有序隊(duì)列,將狀態(tài)轉(zhuǎn)移方程中的max/min 轉(zhuǎn)化成有序隊(duì)列的合并。
這里仍然以01 背包為例講解一下。
首先看01 背包求最優(yōu)解的狀態(tài)轉(zhuǎn)移方程:F[i; v] = max(F[i - 1,v], F[i - 1, v -Ci] + Wi)。如果要求第K 優(yōu)解,那么狀態(tài)F[i, v] 就應(yīng)該是一個(gè)大小為K 的隊(duì)列F[i, v, 1 ...K]。其中F[i, v, k] 表示前i 個(gè)物品中,背包大小為v 時(shí),第k 優(yōu)解的值。這里也可以簡(jiǎn)單地理解為在原來(lái)的方程中加了一維來(lái)表示結(jié)果的優(yōu)先次序。顯然f[i,v,1 ...K] 這K 個(gè)數(shù)是由大到小排列的,所以它可看作是一個(gè)有序隊(duì)列。然后原方程就可以解釋為:F[i, v] 這個(gè)有序隊(duì)列是由F[i - 1, v] 和F[i - 1,v -Ci] + Wi 這兩個(gè)有序隊(duì)列合并得到的。前者F[i - 1][V] 即F[i - 1,v, 1 ...K],后者F[i - 1,v - Ci] + Wi 則理解為在F[i - 1,v - Ci,1 ...K] 的每個(gè)數(shù)上加上Wi 后得到的有序隊(duì)列。合并這兩個(gè)有序隊(duì)列并將結(jié)果的前K 項(xiàng)儲(chǔ)存到f[i; v; 1 : : :K] 中的復(fù)雜度是O(K)。最后的第K 優(yōu)解的答案是F[N, V,K]。總的時(shí)間復(fù)雜度是O(V NK)。
為什么這個(gè)方法正確呢?實(shí)際上,一個(gè)正確的狀態(tài)轉(zhuǎn)移方程的求解過(guò)程遍歷了所有可用的策略,也就覆蓋了問(wèn)題的所有方案。只不過(guò)由于是求最優(yōu)解,所以其它在任何一個(gè)策略上達(dá)不到最優(yōu)的方案都被忽略了。如果把每個(gè)狀態(tài)表示成一個(gè)大小為K 的數(shù)組,并在這個(gè)數(shù)組中有序地保存該狀態(tài)可取到的前K 個(gè)最優(yōu)值。那么,對(duì)于任兩個(gè)狀態(tài)的max 運(yùn)算等價(jià)于兩個(gè)由大到小的有序隊(duì)列的合并。另外還要注意題目對(duì)于“第K 優(yōu)解”的定義,是要求將策略不同但權(quán)值相同的兩個(gè)方案是看作同一個(gè)解還是不同的解。如果是前者,則維護(hù)有序隊(duì)列時(shí)要保證隊(duì)列里的數(shù)沒(méi)有重復(fù)的。
9.6 小結(jié)
顯然,這里不可能窮盡背包類動(dòng)態(tài)規(guī)劃問(wèn)題所有的問(wèn)法。甚至還存在一類將背包類動(dòng)態(tài)規(guī)劃問(wèn)題與其它領(lǐng)域(例如數(shù)論、圖論)結(jié)合起來(lái)的問(wèn)題,在這篇論背包問(wèn)題的專文中也不會(huì)論及。但只要深刻領(lǐng)會(huì)前述所有類別的背包問(wèn)題的思路和狀態(tài)轉(zhuǎn)移方程,遇到其它的變形問(wèn)題,應(yīng)該也不難想出算法。
觸類旁通、舉一反三,應(yīng)該也是一個(gè)程序員應(yīng)有的品質(zhì)吧。
到此背包系列就全部結(jié)束了,希望這個(gè)系列能幫助在看不懂作者的原文的基礎(chǔ)上有一些理解。