1. 程式人生 > >01揹包的四種解法詳解:動態規劃,貪心法,回溯法,優先佇列式分支限界法(C語言編寫)

01揹包的四種解法詳解:動態規劃,貪心法,回溯法,優先佇列式分支限界法(C語言編寫)

最近剛完成了演算法課程設計,題目是用多種解法解決01揹包問題,經過一番探索,終於成功的用四種方法完成了本次實驗,下面記錄分享一下成果:

首先解釋下什麼是01揹包問題:給定一組共n個物品,每種物品都有自己的重量wi, i=1~n和價值vi, i=1~n,在限定的總重量(揹包的容量C)內,如何選擇才能使得選擇物品的總價值之和最高。選擇最優的物品子集放置於給定背中,最優子集對應n元解向量(x1,…xn), xi∈{0或1},因此命名為0-1揹包問題。

輸入資料格式如下:
第一行C(揹包容量)和n(物品個數);
接下來n行:wi(第i件物品的重量,1<=i<=n)和vi(第i件物品的價值,1<=i<=n)。
輸出資料格式如下:
第一行揹包物品的最大價值和;
接下來n行:i(物品編號,表示第i件物品)和xi(該件物品選或不選的0/1分量)。

採用檔案輸入輸出方式測試資料。

以下是4種解決方法的程式碼(已附上詳細註釋):

1.動態規劃法:

#include<stdio.h>
#include "stdlib.h" 
#include "time.h"
int c,n;
int w[5100],v[5100],f[61000],x[5100][61000]; //f(i)表示揹包容量為i的最優值,x(i)(j)表示揹包容量為j時第i件物品是否放入(1/0)
int main(){
	//計算執行時間
	clock_t start, finish;  /*精確到ms(毫秒)級的時間*/ 
	double duration;  /*測量一個事件持續的時間*/
	start = clock();

	FILE *fp1,*fp2;
	fp1=fopen("in.txt","r");
	fp2=fopen("動態規劃out.txt","w");
	int num=1;
	while(!feof(fp1)){
		fscanf(fp1,"%d%d",&c,&n);
		int i,j,k;
		for(i=1;i<=n;i++){
			fscanf(fp1,"%d%d",&w[i],&v[i]);
			for(j=0;j<=c;j++)
				x[i][j]=0; //初始化,一開始每件物品都未放入
		} 
		for(i=0;i<=c;i++){
			f[i]=0; //初始化,一開始揹包為空
		}
		for(i=1;i<=n;i++){
			for(j=c;j>=w[i];j--){
				if(f[j]<f[j-w[i]]+v[i]){ //揹包容量為j時,若第i件物品放入比不放人價值大,則放入,更新f(j)的值
					f[j]=f[j-w[i]]+v[i];
					x[i][j]=1;  
					for(k=1;k<i;k++) //用前i-1件物品放入與否的情況更新當前揹包情況
						x[k][j]=x[k][j-w[i]];
				}
			}
		}
		fprintf(fp2,"第%d組資料:\n",num++);
		fprintf(fp2,"%d\n",f[c]); //揹包容量為c時的最優值
		for(i=1;i<=n;i++)
			fprintf(fp2,"%d %d\n",i,x[i][c]); //揹包容量為c時每件物品放入與否
	}
	finish = clock(); 
	duration = (double)(finish - start) / CLOCKS_PER_SEC;
	fprintf( fp2,"%f seconds\n", duration );  /*此duration單位為秒*/

	fclose(fp1);
	fclose(fp2);
}
2.貪心法:
#include<stdio.h>
#include "stdlib.h" 
#include "time.h"
int c,n,w[5100],v[5100],x[5100];
struct A{
	double avg; //物品單位重量價值
	int index;  //物品下標
}a[5100];
void exchange(A &x1,A &x2){   //交換兩結構體變數值
	double temp1=x1.avg;
	x1.avg=x2.avg;
	x2.avg=temp1;
	int temp2=x1.index;
	x1.index=x2.index;
	x2.index=temp2;
}
int main(){
	clock_t start, finish;  /*精確到ms(毫秒)級的時間*/ 
	double duration;  /*測量一個事件持續的時間*/
	start = clock(); 

	FILE *fp1,*fp2;
	fp1=fopen("in.txt","r");
	fp2=fopen("貪心法out.txt","w");
	int num=1;
	while(!feof(fp1)){
		fscanf(fp1,"%d%d",&c,&n);
		int i,j;
		for(i=1;i<=n;i++){
			fscanf(fp1,"%d%d",&w[i],&v[i]);
			a[i].avg=v[i]*1.0/w[i];
			a[i].index=i;
			x[i]=0;
		}
		for(i=n;i>0;i--){  //將物品按單位重量價值降序排序
			for(j=1;j<i;j++){
				if(a[j].avg<a[j+1].avg){
					exchange(a[j],a[j+1]);
				}
			}
		}
		int sum=0,ans=0;
		for(i=1;i<=n;i++){
			if(sum+w[a[i].index]<=c){ //按序放入物品直到放不下即可
				sum+=w[a[i].index];
				ans+=v[a[i].index];
				x[a[i].index]=1;
			}
		}
		fprintf(fp2,"第%d組資料:\n",num++);
		fprintf(fp2,"%d\n",ans);
		for(i=1;i<=n;i++){
			fprintf(fp2,"%d %d\n",i,x[i]);
		}
	}
	finish = clock(); 
	duration = (double)(finish - start) / CLOCKS_PER_SEC;
	fprintf(fp2,"%f seconds\n", duration );  /*此duration單位為秒*/

	fclose(fp1);
	fclose(fp2);
}
3.回溯法:
#include<stdio.h>
#include "stdlib.h" 
#include "time.h"
int c,n,bestv,cv,cw; //bestv表示最優值,cv表示當前揹包價值,cw表示當前揹包重量
int w[5100],v[5100],x[5100],ax[5100]; //x(i)和ax(i)都表示第i種物品是否放入(1/0),其中x(i)為遍歷過程中臨時記錄每條路徑的值,ax(i)記錄最優路徑的值
struct A{
	double avg;  //物品單位重量價值
	int index;  //物品編號,即下標
}a[5100];
void exchange(A &x1,A &x2){  //交換兩結構體型別變數的值
	double temp1=x1.avg;
	x1.avg=x2.avg;
	x2.avg=temp1;
	int temp2=x1.index;
	x1.index=x2.index;
	x2.index=temp2;
}
int bound(int i){ //計算價值上界
	int left=c-cw;  //剩餘容量
	int b=cv;   //當前揹包價值
	while(i<=n&&left>=w[i]){  //將能整個裝入的前幾個物品裝入揹包
		b+=v[i];
		left-=w[i];
		i++;
	}
	if(i<=n)  //若揹包容量有剩餘,裝入下一個物品的部分
		b+=(int)(v[i]*left*1.0/w[i]);
	return b;
}
void getbestv(int i){  //回溯遍歷求最優解
	if(i>n){  //此路徑結束
		if(cv>bestv){ //若當前價值大於最優值
			bestv=cv;  //更新最優值
			for(int j=1;j<=n;j++){ //ax記錄最優路徑
				ax[j]=x[j];
			}
		}
		return;
	}
	if(cw+w[i]<=c){ //左子樹,第i件放得下
		x[a[i].index]=1; //記錄路徑
		//放入揹包
		cv+=v[i];  
		cw+=w[i];
		getbestv(i+1);//搜尋下一節點
		//回退
		cv-=v[i];
		cw-=w[i];
	}
	x[a[i].index]=0; //回退
	if(bound(i+1)>=bestv) //右子樹,若預計往下走能得到的價值上界不小於當前最優值才執行
		getbestv(i+1);  //搜尋下一節點
}
int main(){
	clock_t start, finish;  /*精確到ms(毫秒)級的時間*/ 
	double duration;  /*測量一個事件持續的時間*/
	start = clock(); 

	FILE *fp1,*fp2;
	fp1=fopen("bigdata.txt","r");
	fp2=fopen("test.txt","w");
	int num=1;
	while(!feof(fp1)){
		fscanf(fp1,"%d%d",&c,&n);
		int i,j,ww[5100],vv[5100];
		for(i=1;i<=n;i++){
			fscanf(fp1,"%d%d",&ww[i],&vv[i]);
			a[i].index=i;
			a[i].avg=1.0*vv[i]/ww[i];
			x[i]=0;
		}
		cv=0;cw=0;bestv=0;
		for(i=n;i>0;i--){ //按單位重量價值降序排序
			for(j=1;j<i;j++){
				if(a[j].avg<a[j+1].avg){
					exchange(a[j],a[j+1]);
				}
			}
		}
		for(i=1;i<=n;i++){ //物品重量價值也按單位重量價值降序排序
			w[i]=ww[a[i].index];
			v[i]=vv[a[i].index];
		}
		getbestv(1);  //回溯遍歷獲取最優值
		fprintf(fp2,"第%d組資料:\n",num++);
		fprintf(fp2,"%d\n",bestv);
		for(i=1;i<=n;i++){
			fprintf(fp2,"%d %d\n",i,ax[i]);
		}
	}
	finish = clock(); 
	duration = (double)(finish - start) / CLOCKS_PER_SEC;
	fprintf( fp2,"%f seconds\n", duration );  /*此duration單位為秒*/
	printf("%f seconds\n",duration);
	fclose(fp1);
	fclose(fp2);
}
4.優先佇列式分支限界法:
#include<stdio.h>
#include "stdlib.h" 
#include "time.h"
int c,n,bestv,cv,cw,up,qlen; //bestv表示最優值,cv和cw分別表示當前揹包價值和重量,up表示當前節點往下走預計能得到的價值上界,qlen表示優先佇列長度
int w[5100],v[5100],x[5100]; 
bool vis[5100]; //vis(i)標記佇列中第i個元素是否被刪除
struct A{
	int index;  //表示物品下標
	double avg;  //表示物品單位重量價值
}a[5100];
struct B{
	int upvalue; //節點的價值上界
	int weight;  //到此節點對應的重量
	int value;   //到此節點對應的價值
	int level;   //節點所在層次
	int islchild;  //是否為左節點
	int parent; //父節點在佇列陣列中的下標
}q[5100];
void exchange(A &x1,A &x2){ //交換兩個結構體A變數值
	double temp1=x1.avg;
	x1.avg=x2.avg;
	x2.avg=temp1;
	int temp2=x1.index;
	x1.index=x2.index;
	x2.index=temp2;
}
int bound(int i){ //計算右子樹價值上界
	int left=c-cw; //剩餘容量
	int b=cv;  //當前揹包價值
	while(i<=n&&left>=w[i]){ //將可以完整放入的前幾個物品放入揹包
		b+=v[i];
		left-=w[i];
		i++;
	}
	if(i<=n){  //放入下一個揹包的部分
		b+=(int)(v[i]*left*1.0/w[i]);
	}
	return b;
}
int deleteMax(){ //刪除佇列中價值上界最大的節點並返回
	int max=0;
	int i;
	for(int j=1;j<qlen;j++){ //求價值上界最大的節點
		if(vis[j]&&q[j].upvalue>max){
			max=q[j].upvalue;
			i=j;
		}
	}
	vis[i]=false; //標誌為已刪除
	return i;
}
void getbestv(){  //求最優值
	int i=1;
	up=bound(1); //從第1個節點開始計算的價值上界
	int father=0; //初始化父節點
	while(i!=n+1){ //共n層,i=n+1表示已遍歷完畢
		if(cw+w[i]<=c){ //放的下第i件物品
			if(cv+v[i]>bestv) //更新最優值
				bestv=cv+v[i];
			vis[qlen]=true; //設定即將新新增節點為未訪問
			//將第i+1個節點新增到優先佇列中
			q[qlen].upvalue=up; 
			q[qlen].weight=cw+w[i];
			q[qlen].value=cv+v[i];
			q[qlen].level=i+1;
			q[qlen].islchild=1; 
			q[qlen++].parent=father;
		}
		up=bound(i+1);//求出從第i+1個節點開始計算的價值上界,即不取第i個節點
		if(up>=bestv){ //若可能得到更優解,新增此節點到佇列中
			vis[qlen]=true; 
			q[qlen].upvalue=up;
			q[qlen].weight=cw;
			q[qlen].value=cv;
			q[qlen].level=i+1;
			q[qlen].islchild=0;
			q[qlen++].parent=father;
		}
		father=deleteMax(); //得到佇列中價值上界最大的節點,即在加入i與不加人i中選擇一種較優的方式,作為解樹的一個節點
		//從此節點往下求值
		up=q[father].upvalue;
		cw=q[father].weight;
		cv=q[father].value;
		i=q[father].level;
	}
	for(i=n;i>0;i--){ //從下往上得到最優解節點
		x[a[i].index]=q[father].islchild;
		father=q[father].parent;
	}
}
int main(){
	clock_t start, finish;  /*精確到ms(毫秒)級的時間*/ 
	double duration;  /*測量一個事件持續的時間*/
	start = clock(); 

	FILE *fp1,*fp2;
	fp1=fopen("in.txt","r");
	fp2=fopen("分支限界法out.txt","w");
	int num=1;
	while(!feof(fp1)){
		fscanf(fp1,"%d%d",&c,&n);
		int i,j,ww[5100],vv[5100];
		for(i=1;i<=n;i++){
			fscanf(fp1,"%d%d",&w[i],&v[i]);
			ww[i]=w[i];vv[i]=v[i];
			a[i].index=i;
			a[i].avg=1.0*v[i]/w[i];
			x[i]=0;
		}
		qlen=1;cv=0;cw=0;bestv=0;
		for(i=n;i>1;i--){ //按單位重量價值排序
			for(j=1;j<i;j++){
				if(a[j].avg<a[j+1].avg){
					exchange(a[j],a[j+1]);
				}
			}
		}
		for(i=1;i<=n;i++){ //物品也按單位重量價值排序
			w[i]=ww[a[i].index];
			v[i]=vv[a[i].index];
		}			
		getbestv();

		fprintf(fp2,"第%d組資料:\n",num++);
		fprintf(fp2,"%d\n",bestv);
		for(i=1;i<=n;i++){
			fprintf(fp2,"%d %d\n",i,x[i]);
		}
	}
	finish = clock(); 
	duration = (double)(finish - start) / CLOCKS_PER_SEC;
	fprintf(fp2,"%f seconds\n", duration );  /*此duration單位為秒*/

	fclose(fp1);
	fclose(fp2);
}