算法與數據結構-揹包問題

01揹包問題

題目

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

分析

這是最基礎的揹包問題,特點是:每種物品僅有一件,可以選擇放或不放。用子問題定義狀態:即f[i][j]表示前i件物品恰放入一個容量爲j的揹包可以獲得的最大價值。則其狀態轉移方程便是:

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

優化空間複雜度

即改用一維數組f[j]存儲第i個物品時剩餘空間爲j時的揹包的最大價值。

注意到j-c[i]<j這個關係,當j=0……M順序推f[j],則後面的到的f[j]將會使用到當前i狀態下新生成的f[j-c[i]],而不是我們所需的i-1狀態時的f[j-c[i]]。

因此,在每次主循環中我們以j=M……0順序推f[j],這樣才能保證推f[j]時f[j-c[i]]保存的是狀態f[i-1][j-c[i]]的值。

參考代碼

/*
 * n:物品種類 每種只能選取一種
 * capacity:揹包容量
 * c[i]:第i種物品的花費 cost
 * v[i]:第i種物品的價值 value
 * f[j]:i狀態下容量爲j時揹包可獲得的最大價值
 */
int getMaxValue(int n,int capacity){
	for(int i=0;i<n;i++)
		for(int j=capacity;j>=0;j--)
			if(i==0){
				if(j>=c[i])f[j]=v[i];
				else f[j]=0;
			}
			else if(j>=c[i])f[j]=max(f[j],f[j-c[i]]+v[i]);
	return f[capacity];
}

 

完全揹包問題

題目

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

分析

類似01揹包問題,但與它相關的策略已並非取或不取兩種,而是有取0件、取1件、取2件……。如果仍然按照解01揹包時的思路,令f[i][j]表示前i種物品恰放入一個容量爲j的揹包的最大權值。仍然可以按照每種物品不同的策略寫出狀態轉移方程:

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

優化時間複雜度

若兩件物品i、j滿足c[i]<=c[j]且w[i]>=w[j],則將物品j去掉,不用考慮。

將費用大於M的物品去掉,然後使用類似計數排序的做法,計算出費用相同的物品中價值最高的是哪個。

轉化爲01揹包問題求解

最簡單的想法是:考慮到第i種物品最多選M/c[i]件,於是可以把第i種物品轉化爲M/c[i]件費用及價值均不變的物品,然後求解這個01揹包問題。這樣完全沒有改進基本思路的時間複雜度,但這畢竟給了我們將完全揹包問題轉化爲01揹包問題的思路:將一種物品拆成多件物品。

更高效的轉化方法是:把第i種物品拆成費用爲c[i]*2^k、價值爲v[i]*2^k的若干件物品,其中k滿足c[i]*2^k<=M。這是二進制的思想,因爲不管最優策略選幾件第i種物品,總可以表示成若干個2^k件物品的和。這樣把每種物品拆成O(logM/c[i])件物品,是一個很大的改進。

最簡O(MN)算法:首先想想爲什麼01揹包問題中爲何要按照j=M..0的逆序來循環。這是因爲要保證第i次循環中的狀態f[i][j]是由狀態f[i-1][j-c[i]]遞推而來。換句話說,這正是爲了保證每件物品只選一次,保證在考慮“選入第i件物品”這件策略時,依據的是一個絕無已經選入第i件物品的子結果f[i-1][j-c[i]]。而現在完全揹包的特點恰是每種物品可選無限件,所以在考慮“加選一件第i種物品”這種策略時,卻正需要一個可能已選入第i種物品的子結果f[i][j-c[i]],所以就可以並且必須採用j=0..M的順序循環。

參考代碼

/*
 * n:物品種類 每種只能選取一種
 * capacity:揹包容量
 * c[i]:第i種物品的花費 cost
 * v[i]:第i種物品的價值 value
 * f[j]:i狀態下容量爲j時揹包可獲得的最大價值
 */
int getMaxValue(int n,int capacity){
	for(int i=0;i<n;i++)
		for(int j=0;j<=capacity;j++)
			if(i==0){
				if(j>=c[i])f[j]=v[i]*(j/c[i]);
				else f[j]=0;
			}
			else if(j>=c[i])f[j]=max(f[j],f[j-c[i]]+v[i]);
	return f[capacity];
}



多重揹包問題

題目

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

分析

這題目和完全揹包問題很類似。基本的方程只需將完全揹包問題的方程略微一改即可,因爲對於第i種物品有n[i]+1種策略:取0件,取1件……取n[i]件。令f[i][v]表示前i種物品恰放入一個容量爲v的揹包的最大權值,則有狀態轉移方程:
 f[i][j]=max{ f[i-1][j-k*c[i]]+k*v[i] | 0<=k<=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種物品。

參考代碼

/**
 * 多重揹包決策:遍歷到第i種物品(i狀態)時的決策
 * cost:i狀態時的花費c[i]
 * weight:i狀態時的價值v[i]
 * amount:i狀態時物品的最大數量n[i]
 * CompletePack:多重揹包決策
 * ZeroOnePack:01揹包決策
 */
void MultiplePack(cost,weight,amount){
	if(cost*amount>=capacity){
		CompletePack(cost,weight);return;
	}
	int k=1;
	while(k<amount){ 
		ZeroOnePack(k*cost,k*weight);
		amount=amount-k;
		k=k*2;
	}
	ZeroOnePack(amount*cost,amount*weight);
}


題目推薦:

ZOJ Problem Set - 1149 Dividing

http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=149

題意:

給出序號爲1、2、3、4、5、6的6種彈珠(好像是彈珠吧⊙﹏⊙b汗),其價值等於其編號,給出每種彈珠的數目,求能否將彈珠分兩堆,使兩堆的價值和相等。

分析:

多重揹包問題,只是所謂的花費cost等於價值,且揹包容量爲總和的一半。

代碼:

#include <iostream>
#define MAX 210002
#define max(a,b) (a)>(b)?(a):(b)
using namespace std;
long nums[6],total,f[MAX];

int input(){
	for(int i=total=0;i<6;i++){
		cin>>nums[i];
		total+=nums[i]*(i+1);
	}
	if(total)return 1;
	return 0;
}

void zeroOnePack(int cost,int value,long capacity){
	for(int j=capacity;j>-1;j--)
		if(j>=cost)f[j]=max(f[j],f[j-cost]+value);
}
void completePack(int cost,int value,long capacity){
	for(int j=0;j<=capacity;j++)
		if(j>=cost)f[j]=max(f[j],f[j-cost]+value);
}
void multiplePack(int cost,int value,int count,long capacity){
	if(cost*count>=capacity){
		completePack(cost,value,capacity);
		return;
	}
	int k=1;
	while(k<count){
		zeroOnePack(cost*k,value*k,capacity);
		count-=k;
		k*=2;
	}
	zeroOnePack(cost*count,value*count,capacity);
}

int judge(long capacity){
	for(int i=0;i<6;i++){
		if(i==0){
			for(int j=0;j<=capacity;j++)
				if(j<i+1)f[j]=0;
				else{
					int amount=j/(i+1);
					f[j]=(nums[i]>=amount ? (i+1)*amount : nums[i]*(i+1));
				}
		}
		else multiplePack(i+1,i+1,nums[i],capacity);
	}
	if(f[capacity]==capacity)return 1;
	return 0;
}

int main()
{
	int T=1;
	while(input()){
		printf("Collection #%d:\n",T++);
		if(total&1)cout<<"Can't be divided."<<endl<<endl;
		else if(judge(total/2))cout<<"Can be divided."<<endl<<endl;
		else cout<<"Can't be divided."<<endl<<endl;
	}
	return 0;
}


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章