1. 程式人生 > >揹包問題小總結 習題(動態規劃01揹包(第k優解)完全揹包,多重揹包)acm杭電HDU2639,HDU2602,HDU1114,HDU2191

揹包問題小總結 習題(動態規劃01揹包(第k優解)完全揹包,多重揹包)acm杭電HDU2639,HDU2602,HDU1114,HDU2191

1、01揹包(每種物品只有一個)

題目 有N件物品和一個容量為V的揹包。第i件物品的費用是c[i],價值是w[i]。

求解將哪些物 品裝入揹包可使價值總和最大。

基本思路 這是最基礎的揹包問題,特點是:每種物品僅有一件,可以選擇放或不放。 用子問題定義狀態:

           即表示前i件物品恰放入一個容量為v的揹包可以獲得的最大價值。 則其狀態轉移方程便是:

                                      f[i][v] =Max { f[i−1][v] ,f[i−1][v−c[i]] + w[i] }

              這個方程非常重要

               把第i個物品在考慮加進去的時候,計算一下,加入這個物品後,揹包的價值能不能提高,這就是狀態方程              的意義。

核心程式碼:

for (int i=1;i<=n;i++)
	for (int v=V;v>=c[i];v--)
		f[v]=max(f[v],f[v-c[i]]+w[i]); 

程式碼實現時一般用到三個一維陣列:f [V] , v[N] , w[N]。

V表示揹包的最大容量,N表示物品個數

陣列f存的是狀態,比如說,f [ j ] 就表示把揹包裝到 j 這麼重的時候的最大價值

陣列v 和 w 分別存價值和重量。

題意:Teddy撿石頭往他的揹包裡放,給出揹包容量,石頭個數,還有每個石頭的重量和價值。求出怎麼樣選擇                 石頭,可以獲得最大的價值。(經典)

程式碼:

#include<stdio.h>
#include<string.h>
int max(int a,int b)
{
    return a>b?a:b;
}
int main()
{
    int T,N,V;
    int i,j;
    int bag[1010],v[1010],w[1010];
    scanf("%d",&T);
    while(T--)
    {
        memset(bag,0,sizeof(bag));//把揹包各個狀態是的價值設為0
        scanf("%d%d",&N,&V);
        for(i=0;i<N;i++)
            scanf("%d",v+i);
        for(i=0;i<N;i++)
            scanf("%d",w+i);
        for(i=0;i<N;i++) //第i個物品
            for(j=V;j>=w[i];j--){ //j狀態的揹包(計算重量為j是的最大價值)
                bag[j]=max(bag[j],bag[j-w[i]]+v[i]);
            }
        printf("%d\n",bag[V]);
    }
    return 0;
}

題意:就是像一種購買方案,在最後一次買的時候,買最貴的把卡的餘額欠的最大。但是在買最後一個之前,要把

           卡的餘額刷到最接近5元(大於等於)。這個過程類似01揹包。

思路:該題需要把卡的餘額類比成揹包重量。需要注意,每種菜的價值和重量相等,都是菜價。也就是揹包問題中

           v [ i ] == w [ i ]

程式碼:

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int main()
{
    int N,V,m;
    int i,j;
    int bag[1010],v[1010];
    while(~scanf("%d",&N),N)
    {
        memset(bag,0,sizeof(bag));
        for(i=0;i<N;i++)
            scanf("%d",v+i);
        sort(v,v+N);//通過排序把最貴的菜放到最後一個位置
        
        scanf("%d",&m);//把卡餘額m看做揹包容量
        //飯菜的價格即代表他的價值,也代表他的重量
        V=m-5;//我們的目的是把卡餘額儘量刷到5元
        if(m<5){
            printf("%d\n",m);
            continue;
        }
        for(i=0;i<N-1;i++) //第i個菜
            for(j=V;j>=v[i];j--){
                bag[j]=max(bag[j],bag[j-v[i]]+v[i]);
            }
        printf("%d\n",m-v[N-1]-bag[V]);//減掉最貴的菜,和規劃好的最大消費
    }
    return 0;
}


總結:狀態方程不能死記硬背,要根據題目意思靈活應變。上面的程式碼中,揹包狀態都是從V往物品重量遞推,且要取等號,恰好裝也可以。

2、完全揹包(每種物品有無限個)

題目 有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×w[i] ] + k×v[i] }          0 <= k×c[i] <= V

             但是實際程式碼不是這麼直接套用的,而是在當前狀態下,繼續添加當前物品,直到揹包裝滿。

for(int i=0;i<N;i++)
    for(int j=w[i];j<=V;j++)//V是揹包最大容量
        f[j]=max(f[j],f[j-w[i]]+v[i]) //陣列w重量,v價值

注意:

和01揹包僅有一點區別,就是迴圈順序。但是動態規劃出來的結果是截然不同的!

             先回憶一下01揹包為什麼要到這迴圈,因為每一個物品在裝進去的時候,都要用到前一個狀態的資料來計算當前物品要不要加入。簡單說,就是由前面物品裝好的狀態推出當前物品裝入的狀態。

             而內迴圈為正序時,先更新狀態,再用更新了的狀態繼續更新當前狀態,就產生了一個現象:在不超揹包容量的狀態下,我可以一直用當前物品不斷更新揹包狀態,即不斷往揹包裡填同一個物品。打個比方,我想在正在裝一個重m的物品,進入內迴圈,我先把f [m] 這個狀態更新了,想在f [m]就是裝入了m後的最大價值,繼續更新f [m+1]的狀態,我會用到 f [j]=max( f [j], f [j-v[i]]+v[i]); 這個語句,也就是會呼叫到更小重量時的狀態,而我恰好在這之前更新過了,那不就是我可以第二次把物品m裝進揹包嗎。以此類推,我往後更新狀態過程中,我可以無限的把同一個物品 i 往裡裝。

題意:某人有個存錢罐(存硬幣),空著的時候重E,存滿錢是重F。現在他把罐子裝滿錢了,給出硬幣種類和重量,計算存錢罐最少能存多少錢。

思路:完全揹包。每種硬幣可以無限放。但是這裡不是求最大價值,而是求最小价值,所以我們先假設所有狀態為正無窮,然後不斷更新dp,以求得最小价值。這樣在動態規劃時需要注意一點,狀態0始終未0,即f [ 0 ] =0恆成立

程式碼:

#include<stdio.h>
#include<algorithm>
using namespace std;
const int INF=0x6fffffff;
int f[10010];
int main()
{
    int T,E,F,M,N;
    int p,w; //價值和重量
    scanf("%d",&T);
    while(T--)
    {
        for(int i=1;i<=10002;i++)f[i]=INF;
        f[0]=0;//這一步很重要,給動態規劃一個開頭
        scanf("%d%d%d",&E,&F,&N);
        M=F-E; //最大錢數M
        for(int i=0;i<N;i++){ //第 i 種硬幣
            scanf("%d%d",&p,&w);
            for(int j=w;j<=M;j++) //不斷往裡放這種硬幣,取錢數少的方案
                f[j]=min(f[j],f[j-w]+p);
        }
        if(f[M]==INF)
            puts("This is impossible.");
        else
            printf("The minimum amount of money in the piggy-bank is %d.\n",f[M]);
    }
    return 0;
}

總結:與01揹包的程式碼實現類似,但過程卻相差甚大,一定要區分程式碼的計算思路

3、多重揹包(每種物品有多個)

           與01揹包類似,只不過每種物品的數量不止一個。最直接的想法,把物品i有n個想象成,有n個物品一模一樣,再用01揹包求解即可。

51nod(優化程式碼)

基準時間限制:1 秒 空間限制:131072 KB 分值: 40 難度:4級演算法題

 收藏

 關注

有N種物品,每種物品的數量為C1,C2......Cn。從中任選若干件放在容量為W的揹包裡,每種物品的體積為W1,W2......Wn(Wi為整數),與之相對應的價值為P1,P2......Pn(Pi為整數)。求揹包能夠容納的最大價值。

Input

第1行,2個整數,N和W中間用空格隔開。N為物品的種類,W為揹包的容量。(1 <= N <= 100,1 <= W <= 50000)
第2 - N + 1行,每行3個整數,Wi,Pi和Ci分別是物品體積、價值和數量。(1 <= Wi, Pi <= 10000, 1 <= Ci <= 200)

Output

輸出可以容納的最大價值。

Input示例

3 6
2 2 5
3 3 8
1 4 1

Output示例

9

如Ci  = 14,我們可以把它化成如下4個物品:

重量是Wi,體積是Vi
重量是2 * Wi , 體積是2 * Vi
重量是4 * Wi , 體積是4 * Vi
重量是7 * Wi , 體積是7 * Vi

注意最後我們最後我們不能取,重量是8 * Wi , 體積是8 * Vi 因為那樣總的個數是1 + 2 + 4 + 8 = 15個了,我們不能多取對吧?

【程式碼】:

#include<stdio.h>
typedef long long ll;
template<class T>T max(T a,T b)
{
	return a>b?a:b;
}
ll f[60000];
ll w[2002000];
ll p[2002000];
int main()
{
	int n,W;
	scanf("%d%d",&n,&W);
	int k=0;
	for(int i=0;i<n;i++)
	{
		int w1,p1,c1;
		scanf("%d%d%d",&w1,&p1,&c1);
		int t=1;
		while(c1>0)
		{
			if(c1>t)
			{
				w[k]=t*w1;
				p[k]=t*p1;
			}
			else
			{
				w[k]=c1*w1;
				p[k]=c1*p1;
			}
			c1=c1-t;
			t=t*2;
			k++;
		}
	}
	for(int i=0;i<k;i++)
	for(int j=W;j>=w[i];j--)
	{
		f[j]=max(f[j],f[j-w[i]]+p[i]);
	}
	printf("%lld\n",f[W]);
	return 0;
}

程式碼:

//HDU 2191 多重揹包 
#include<stdio.h>
int val[110],wei[110],count[110],dp[110];//價格,重量,袋數。動態揹包 
int max(int a,int b)
{
	return a>b?a:b;
}
int main()
{
	int T;
	scanf("%d",&T);
	while(T--)
	{
		int n,m;// 錢數,種類 
		scanf("%d%d",&n,&m);
		for(int i=0;i<m;i++)
		{
			scanf("%d%d%d",val+i,wei+i,count+i);
		}
		for(int i=0;i<=n;i++) dp[i]=0;//初始為0 
		
		for(int i=0;i<m;i++) //第 i 種大米 
			for(int k=1;k<=count[i];k++) //大米 i 放入的次數 
				for(int j=n;j>=val[i];j--) // 動態規劃 
					dp[j]=max(dp[j],dp[j-val[i]]+wei[i]);
		printf("%d\n",dp[n]); //陣列dp存的是重量 
	}
	return 0;
}

4、01揹包第k個最優解

        思想與01揹包相同,不過在動態規劃過程中,不可以把前一個狀態的最優解扔掉,而要儲存下來

在01揹包中,狀態陣列是 f [ v ] ,表示容積為v時的最優決策。而現在,我不僅要知道最優決策,我還想知道稍微差一點的決策,即第2決策、第3決策....排個名。f [ v ] 我們可以看做是 f [ v ] [ 1 ] 這樣的二維陣列,他的第二維只有一個元素,也就是最優決策。現在我們增大第二維,比如 f [ v ] [ 3 ] ,意思是,不僅保留了最優解,次解也保留下來了。

也可以理解為,第二維是一個集合,集合裡就存了所有可能的決策,並按大小有序,排在第一的就是最優解。

現在問題是 第k個最優決策是多少。

           在01揹包裡,我們只保留了最優解,而把不是最優解的解直接捨棄了,即 

f[j]=max(f[j],f[j-w[i]]+v[i])

這時候,較小的那個解直接捨棄了,沒保留下來。現在我們要做的就是,藉助陣列把所有的解都保留下來。

核心程式碼:

		int i,j,t;
		for(i=0;i<n;i++) //對每個物品掃描
			for(j=v;j>=vol[i];j--) //對每個狀態進行更新 
			{
				for(t=1;t<=k;t++)
				{ //把所有可能的解都存起來
					a[t]=f[j][t];
					b[t]=f[j-vol[i]][t]+val[i];
				}
				int m,x,y;
				m=x=y=1;
				a[k+1]=b[k+1]=-1;
				//下面的迴圈相當於求a和b並集,也就是所有的可能解 
				while(m<=k && (a[x]!=-1 || b[y]!=-1))
				{
					if(a[x]>b[y])
						f[j][m]=a[x++];
					else 
						f[j][m]=b[y++];	
					if(f[j][m]!=f[j][m-1])
						m++;
				}
			}

和01揹包的程式碼比較一下。就是多了裡面一層處理比較麻煩,但思想很容易接受。就是把所有可能解先借助a,b兩個陣列存下來,然後按順序賦給狀態陣列f。

模板題

程式碼:

#include <stdio.h>  
#include<string.h>
int f[1010][33];//第一維和普通01揹包一樣。第二維存多個最優解
int val[110],vol[110];//價值和體積
int a[33],b[33];//用於暫時儲存多組最優解 
int n,v,k;
int i,j,t,T;
int main()
{
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d%d%d",&n,&v,&k);
		for(i=0;i<n;i++) scanf("%d",&val[i]);
		for(i=0;i<n;i++) scanf("%d",&vol[i]);
		memset(f,0,sizeof(f));//對狀態陣列清0 
		
		for(i=0;i<n;i++) //對每個物品掃描
			for(j=v;j>=vol[i];j--) //對每個狀態進行更新 
			{
				for(t=1;t<=k;t++)
				{ //把所有可能的解都存起來
					a[t]=f[j][t];
					b[t]=f[j-vol[i]][t]+val[i];
				}
				int m,x,y;
				m=x=y=1;
				a[k+1]=b[k+1]=-1;
				//下面的迴圈相當於求a和b並集,也就是所有的可能解 
				while(m<=k && (a[x]!=-1 || b[y]!=-1))
				{
					if(a[x]>b[y])
						f[j][m]=a[x++];
					else 
						f[j][m]=b[y++];	
					if(f[j][m]!=f[j][m-1])
						m++;
				}
			}
	printf("%d\n",f[v][k]);
	}
	return 0;
 } 

5、01揹包記錄路徑.

            用二維陣列來記錄,path[ m ] [ n ] 。其中m表示物品(m<=物品數),n表示揹包狀態(n<=揹包容量)。

比如 path [ i ] [ j ] 表示物品 i 放在了狀態 j 的揹包中。 前提條件:path陣列全部為0,

程式碼實現記錄路徑

		for(int i=0;i<n;i++)
			for(int j=V;j>=v[i];j--)
				if(f[j]<f[j-v[i]]+w[i])
				{
					f[j]=f[j-v[i]]+w[i];
					path[i][j]=1; //把裝進去的物品標記一下
				}

路徑讀取程式碼

		int i=n-1,j=V; //V:揹包容量。n個物品 
		while(i>=0&&j>=0)
		{
			if(path[i][j])//物品i在j裡 
			{
				printf("%d ",i);//把物品i的編號輸出 
				j-=v[i];  //讀完了物品i,找下一個揹包狀態 
			}
			i--; 
		}