用各種方法解01揹包

Janus_V發表於2020-11-19

01揹包:

說明:

由於不同演算法解01揹包問題的耗時差異甚遠, 一些耗時較高的演算法即使結果正確也無法在OJ上通過測試, 會爆TLE

而手動輸入資料具有差異性, 所以本部分的測試使用拍題程式進行:

隨機生成至少50組資料, 將其他演算法版本的01揹包程式與使用動態規劃的01揹包程式進行答案對拍

拍題程式:

#include <iostream>
#include <ctime>
#include <cstdlib>
#include <bits/stdc++.h>

#define INF 0x3f3f3f3f
#define MOD 100000007

typedef long long ll;

using namespace std;

int myRand() {
	return rand()%100+1;
}

const int MAX=1005;
int dp[MAX][MAX]={0};

//這個是標準的正確的動態規劃01揹包演算法
void pro_AC() {
	int t=0, m=0;
	scanf("%d%d",&t,&m);
	int time[m]={0};
	int value[m]={0};
	for(int i=0;i<m;++i){
		scanf("%d%d",&time[i],&value[i]);
	}
	for(int i=0;i<=t;++i){
		if(i>=time[0]){
			dp[0][i]=value[0];
		}
	}
	//	for(int i=0;i<=t;++i){
	//		printf("%d ",dp[0][i]);
	//	}
	//	putchar('\n');
	//	int maxPrice=dp[0][t];
	for(int i=1;i<m;++i){
		for(int j=1;j<=t;++j){
			if(j<time[i]){
				dp[i][j]=dp[i-1][j];
			}else{
				dp[i][j]=max(value[i]+dp[i-1][j-time[i]],dp[i-1][j]);
			}
			//			printf("%d ",dp[i][j]);

		}
		//		putchar('\n');
	}
	printf("%d\n",dp[m-1][t]);
	return ;
}

//----------------------------------------

//各種演算法嘗試解01揹包
void pro_1() {

	return ;
}

//----------------------------------------


int main() {
	//重置隨機數
	srand(time(0));
	//將隨機資料輸入到檔案中共享
	auto pf=fopen("medic.txt","w+");
	//根據程式輸入調整隨機數生成:

	//	cin>>m>>n;
	int m=rand()%30+1;
	fprintf(pf,"%d %d\n", rand()%1000+1,m);
	for(int i=0; i<m; ++i) {
		fprintf (pf,"%d %d\n",myRand (),myRand ());
	}
	//	for(int i=0;i<2;++i){
	//		for(int j=0;j<n-1;++j){
	//			fprintf(pf,"%c",'a'+myRand());
	//		}
	//		fprintf(pf,"\n");
	//	}

	fclose(pf);

	//輸入重定向到檔案
	//根據需求選擇cin或stdin
	freopen("medic.txt","r",stdin);
	freopen("outputP1.txt","w",stdout);
	int startTime=clock ();
	pro_1 ();  //這裡將題寫成函式の形式
//	printf("\n耗時: %ld\n",clock ()-startTime);
	fclose(stdin);
	fclose(stdout);
	putchar('\n');
	printf("------------------\n");

	//同樣的, 這裡是拍題程式
	freopen("medic.txt","r",stdin);
	freopen("outputAC.txt","w",stdout);
	memset (dp,0,sizeof(dp));
	startTime=clock ();
	pro_AC ();
//	printf("\n耗時: %ld\n",clock ()-startTime);
	fclose(stdin);
	fclose(stdout);
	putchar('\n');
	printf("------------------\n");

	//	freopen("medic.txt","r",stdin);
	//	freopen("outputP2.txt","w",stdout);
	//	pro_2 ();
	//	fclose(stdin);
	//	fclose(stdout);


	//注意這裡已經破壞了stdin, 無法在正常從控制檯讀入, 所以無法使用迴圈
	return 0;
}

.bat程式:

不斷執行編譯好的拍題程式, 對兩個輸出結果進行比對, 直到答案不一樣才退出

@echo off
:loop
debug\JudgeKit.exe
fc outputAC.txt outputP1.txt
if not errorlevel 1 goto loop
pause

蠻力法:

蠻力法使用DFS暴力搜尋所有的物品組合來實現

搜尋到最後一層葉子節點後返回

image-20201114185916229

原始碼:

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<queue>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<bits/stdc++.h>
#include <queue>
#include <vector>
#include <set>
#include <map>

using namespace std;

#define INF 0x3f3f3f3f
#define ll long long

const int MAX=1005;
int dp[MAX][MAX]={0};

bool isUsed[MAX]={0};
int weight[MAX]={0};
int value[MAX]={0};
long long nowValue=0, maxValue=0, nowWeight=0;
int t=0, m=0;

void dfs(int goods){
	//如果最後一層已經統計完畢, 則返回
	if(goods>=m){
		maxValue=max(maxValue,nowValue);
		return ;
	}
	//是否拿當前goodss
	if(nowWeight+weight[goods]<=t){
		nowWeight+=weight[goods];
		nowValue+=value[goods];
		dfs(goods+1);
		nowWeight-=weight[goods];
		nowValue-=value[goods];
	}
	dfs(goods+1);

	return ;
}

//各種演算法嘗試解01揹包
void pro_1() {


	scanf("%d%d",&t,&m);
	for(int i=0;i<m;++i){
		scanf("%d%d",&weight[i],&value[i]);
	}

	dfs(0);
	printf("%lld\n",maxValue);

	return ;
}

int main() {

	pro_1 ();

	return 0;
}

對拍:

本部分與使用動態規劃的標準01揹包解法進行結果對拍

隨機生成多組資料, 輸入兩個程式中進行比較, 結果均無差異
(由於蠻力法耗時較長, 這裡為了比較快速, 物品數m<=30)

//重置隨機數
srand(time(0));
//將隨機資料輸入到檔案中共享
auto pf=fopen("medic.txt","w+");
//根據程式輸入調整隨機數生成:

//隨機資料生成:
int m=rand()%30+1;
fprintf(pf,"%d %d\n", rand()%1000+1,m);
for(int i=0; i<m; ++i) {
    fprintf (pf,"%d %d\n",myRand (),myRand ());
}
fclose(pf);

結果:

image-20201114190159101

分治法:

分治法與蠻力法的決策樹相似, 但蠻力法是自頂向下, 分治法是自下而上

定義子問題:

從第i個物品開始, 若只考慮第i個物品, 有兩種可能

  • 拿:
    問題轉化為前i-1個物品在容量為j-weight[i]的揹包中的最大價值
  • 不拿:
    問題轉化為前i-1個物品在容量為j的揹包中的最大值

而判斷是否拿有兩種情況:

  • 如果揹包剩餘容量不足以容納下, 則不拿
  • 如果容納的下, 則比較拿或不拿所得到的揹包中物品價值的大小, 選較大的一個

所以可以很容易的確定轉化函式:

int vTotal(int i, int j){
	if(i<0){
		return 0;
	}
	if(j<weight[i]){
		return vTotal (i-1,j);
	}else{
		return max(vTotal (i-1,j), vTotal (i-1, j-weight[i])+value[i]);
	}
}

原始碼:

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<queue>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<bits/stdc++.h>
#include <queue>
#include <vector>
#include <set>
#include <map>

using namespace std;

#define INF 0x3f3f3f3f
#define ll long long

const int MAX=1005;

int weight[MAX]={0};
int value[MAX]={0};
int t=0, m=0;

int vTotal(int i, int j){
	if(i<0){
		return 0;
	}
	if(j<weight[i]){
		return vTotal (i-1,j);
	}else{
		return max(vTotal (i-1,j), vTotal (i-1, j-weight[i])+value[i]);
	}
}

//各種演算法嘗試解01揹包
void pro_1() {
	scanf("%d%d",&t,&m);
	for(int i=0;i<m;++i){
		scanf("%d%d",&weight[i],&value[i]);
	}
	cout<<vTotal (m-1, t)<<endl;

	return ;
}

int main() {
	pro_1 ();
	return 0;
}

對拍:

這裡由於分治法效能較高, 所以隨機測試資料中最多生成100個物品:

//重置隨機數
srand(time(0));
//將隨機資料輸入到檔案中共享
auto pf=fopen("medic.txt","w+");
//根據程式輸入調整隨機數生成:

//	cin>>m>>n;
int m=rand()%100+1;
fprintf(pf,"%d %d\n", rand()%1000+1,m);
for(int i=0; i<m; ++i) {
    fprintf (pf,"%d %d\n",myRand (),myRand ());
}
fclose(pf);

結果:

image-20201114201406698

動態規劃:

動態規劃是最標準的解01揹包的演算法了, 這裡直接貼上程式碼:

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<queue>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<bits/stdc++.h>
#include <queue>
#include <vector>
#include <set>
#include <map>

using namespace std;

#define INF 0x3f3f3f3f
#define ll long long

const int MAX=1005;
int dp[MAX][MAX]={0};

void pro_1() {
	int t=0, m=0;
	scanf("%d%d",&t,&m);
	int time[m]={0};
	int value[m]={0};
	for(int i=0;i<m;++i){
		scanf("%d%d",&time[i],&value[i]);
	}
	for(int i=0;i<=t;++i){
		if(i>=time[0]){
			dp[0][i]=value[0];
		}
	}
	for(int i=1;i<m;++i){
		for(int j=1;j<=t;++j){
			if(j<time[i]){
				dp[i][j]=dp[i-1][j];
			}else{
				dp[i][j]=max(value[i]+dp[i-1][j-time[i]],dp[i-1][j]);
			}
		}
	}
	printf("%d\n",dp[m-1][t]);
	return ;
}

int main() {

	pro_1 ();

	return 0;
}

結果:

在洛谷上的測試:

image-20201114191625857

貪心法:

由於01揹包問題中的物體無法分割, 所以用貪心法通常無法得到最優解, 只有可分割的揹包問題才能用貪心法

貪心法就是每次選擇單位價值最大的物品, 以最大量裝入揹包, 策略較為簡單

這裡以CUGBOJ的題為例:

http://acm.cugb.edu.cn/problem_show.php?pid=1262

#include <iostream>
#include <string>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <map>
#include <vector>
#include <queue>

#define INF 0x3f3f3f3f

using namespace std;



//---------------------------------
//貪心法:

//貪心法求解揹包問題

/*
7 15
2 10
3 5
5 15
7 7
1 6
4 18
1 3
 */

struct thing{
	int id=0;
	int w=0;
	int p=0;
	double wp=0;
	int useWeight=0;
};

bool cmp(thing &t1, thing &t2){
	return t1.wp>t2.wp;
}

bool cmp_id(thing &t1, thing &t2){
	return t1.id<t2.id;
}

void pro_1(){
	int n=0, size=0;
	scanf("%d%d",&n, &size);
	vector<thing> things(n);
	for(int i=0;i<n;++i){
		things.at (i).id=i;
		scanf("%d%d",&things.at (i).w,&things.at (i).p);
		things.at (i).wp=things.at (i).p*1.0/things.at (i).w;
	}
	sort(things.begin (), things.end (), cmp);
//	for(auto &temp: things){
//		printf("%d %d %lf\n",temp.w, temp.p, temp.wp);
//	}
	int sizeNow=0;
	double price=0;
//	vector<int> weight;
	for(int i=0;i<n; ++i){
		if(sizeNow+things.at (i).w<=size){
			sizeNow+=things.at (i).w;
			things.at (i).useWeight=things.at (i).w;
			price+=things.at (i).p;

		}else{
			things.at (i).useWeight=size-sizeNow;
			price+=(size-sizeNow)*things.at (i).wp;
			break;
		}
	}
	sort(things.begin (), things.end (), cmp_id );
	printf("%.2lf\n",price);
	printf("%d",things.at (0).useWeight);
	for(int i=1;i<n;++i){
		printf(" %d",things.at (i).useWeight);
	}

	return ;
}

int main() {

	pro_1 ();

	return 0;
}

結果:

image-20201114203307669

回溯法:

回溯法與蠻力法類似, 只不過多了個剪枝策略

噹噹前狀態所能達到的最大價值<已知的最大價值時, 表明後頭無論咋樣也沒法獲得更優解, 此時剪枝:

bool checkBound(int g){
	double tempWeight=nowWeight, tempValue=nowValue;
	int i=g;
	for(;i<m;++i){
		if(tempWeight+vg[i].w<=t){
			tempValue+=vg[i].v;
			tempWeight+=vg[i].w;
		}else{
			break;
		}
	}
	tempValue+=(1.0*vg[i].v/vg[i].w)*(t-tempWeight);
	if(tempValue<=(double)maxValue){
		return true;
	}else{
		return false;
	}
}
//	//剪枝: 如果當前能夠達到的最好情況<maxValue, 直接return
if(checkBound (g)){
    return ;
}

原始碼:

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<queue>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<bits/stdc++.h>
#include <queue>
#include <vector>
#include <set>
#include <map>

using namespace std;

#define INF 0x3f3f3f3f
#define ll long long

const int MAX=1005;
int dp[MAX][MAX]={0};

bool isUsed[MAX]={0};

long long nowValue=0, maxValue=0, nowWeight=0;
int t=0, m=0;

struct goods{
	int w;
	int v;
	goods(int ww, int vv):w(ww),v(vv){}
};
vector<goods> vg;

bool cmp(goods &g1, goods &g2){
	return (1.0*g1.v/g1.w) > (1.0*g2.v/g2.w);
}

bool checkBound(int g){
	double tempWeight=nowWeight, tempValue=nowValue;
	int i=g;
	for(;i<m;++i){
		if(tempWeight+vg[i].w<=t){
			tempValue+=vg[i].v;
			tempWeight+=vg[i].w;
		}else{
			break;
		}
	}
	tempValue+=(1.0*vg[i].v/vg[i].w)*(t-tempWeight);
	if(tempValue<=(double)maxValue){
		return true;
	}else{
		return false;
	}
}

void dfs(int g){
	//如果最後一層已經統計完畢, 則返回
	if(g>=m){
		maxValue=max(maxValue,nowValue);
		return ;
	}
//	//剪枝: 如果當前能夠達到的最好情況<maxValue, 直接return
	if(checkBound (g)){
		return ;
	}

	//是否拿當前物品
	if(nowWeight+vg[g].w<=t){
		nowWeight+=vg[g].w;
		nowValue+=vg[g].v;
		dfs(g+1);
		nowWeight-=vg[g].w;
		nowValue-=vg[g].v;
	}
	dfs(g+1);

	return ;
}


//各種演算法嘗試解01揹包
void pro_1() {
	scanf("%d%d",&t,&m);
	int tw, tv;
	for(int i=0;i<m;++i){
		scanf("%d%d",&tw,&tv);
		vg.push_back (goods(tw, tv));
	}
	sort(vg.begin (), vg.end (), cmp);
	dfs(0);
	printf("%lld\n",maxValue);

	return ;
}

int main() {

	pro_1 ();

	return 0;
}

結果:

image-20201114221731708

拍題結果也對:

image-20201114221747626

分支限界法:

分支限界法求揹包問題在最後一次上機課中已經完成過了,

這裡以CUGBOJ中的題為例, 直接貼上程式碼:

http://acm.cugb.edu.cn/problem_show.php?pid=1003

#include <iostream>
#include <queue>
#include <algorithm>
#define N 105 //物品數為N
using namespace std;

struct Node
{
	Node()
	{
		value = 0;
		weight = 0;
		level = 0;
		parent = 0;
		bound = 0;
	}
	int value;           //搜尋到該節點時的價值
	int weight;          //搜尋到該節點時的總重量
	float bound;         //搜尋以該節點為根的子樹能達到的價值上界
	int level;           //該節點所處層次,從0開始
	struct Node *parent; //指向父節點
};
struct cmp
{
	bool operator()(Node *a, Node *b)
	{
		return a->bound < b->bound;
	}
};
struct Item
{
	int ItemID;  //物品編號
	int value;   //物品價值
	int weight;  //物品重量
	float ratio; //價值/重量
};
bool ItemCmp(Item item1, Item item2)
{
	return item1.ratio > item2.ratio;
}
int maxSize = 0;
int searchCount = 0;
int w[N], v[N];
Item items[N];
int maxVlue;
int c, n;
//限界函式
float maxBound(Node *node, Item items[], int c)
{ //求以該節點為根的子樹能達到的價值上界
	float maxValue;
	int restCapacity; //擴充套件到該節點時的剩餘容量
	int i;

	maxValue = node->value;
	restCapacity = c - node->weight;
	i = node->level;
	while (i < n && restCapacity > items[i].weight)
	{
		maxValue += items[i].value;
		restCapacity -= items[i].weight;
		i++;
	}
	if (restCapacity != 0)
	{
		maxValue += restCapacity * items[i].ratio;
	}
	return maxValue;
}
//bfs
int branchAndBound(Item items[], int c)
{
	int x[n + 1] = {0};
	int maxValue;                                         //儲存當前搜尋到的最大價值
	Node *maxNode = new Node();                           //儲存當前最大價值節點(葉節點)
	priority_queue<Node *, vector<Node *>, cmp> maxQueue; //最大價值優先佇列
	Node *firstNode, *curNode;

	searchCount = 1;
	firstNode = new Node();
	firstNode->bound = maxBound(firstNode, items, c);
	firstNode->parent = NULL;
	maxQueue.push(firstNode); //入隊第一個結點
	maxValue = 0;
	maxNode = firstNode;
	while (!maxQueue.empty())
	{
		curNode = maxQueue.top();
		maxQueue.pop();
		//擴充套件左孩子結點
		if (curNode->weight + items[curNode->level].weight <= c)
		{ //最大重量限界
			Node *leftNode = new Node();
			leftNode->value = curNode->value + items[curNode->level].value;
			leftNode->weight = curNode->weight + items[curNode->level].weight;
			leftNode->level = curNode->level + 1;
			leftNode->parent = curNode;
			leftNode->bound = curNode->bound;
			if (leftNode->level < n)
			{
				maxQueue.push(leftNode);
				searchCount++;
			}
			if (maxValue < leftNode->value)
			{
				maxValue = leftNode->value;
				maxNode = leftNode;
			}
		}
		//擴充套件右孩子結點
		if (maxBound(curNode, items, c) > maxValue)
		{ //最大價值上界限界
			Node *rightNode = new Node();
			rightNode->value = curNode->value;
			rightNode->weight = curNode->weight;
			rightNode->level = curNode->level + 1;
			rightNode->parent = curNode;
			rightNode->bound = maxBound(rightNode, items, c);
			if (rightNode->level < n)
			{
				maxQueue.push(rightNode);
				searchCount++;
			}
		}
		//跟蹤佇列大小
		if (maxQueue.size() > maxSize)
			maxSize = maxQueue.size();
	}
	curNode = maxNode;
	while (curNode)
	{
		int tempValue = curNode->value;
		curNode = curNode->parent;
		if (curNode && curNode->value != tempValue)
			x[items[curNode->level].ItemID] = 1;
	}
	return maxValue;
}
int main()
{
	cin >> c >> n;
	for (int i = 0; i < n; ++i)
	{
		cin >> w[i] >> v[i];
	}
	for (int i = 0; i < n; i++)
	{
		items[i].ItemID = i;
		items[i].value = v[i];
		items[i].weight = w[i];
		items[i].ratio = (float)v[i] / w[i];
	}
	sort(items, items + n, ItemCmp); //按價值率排序
	cout << branchAndBound(items, c) << endl;
	return 0;
}

結果:

image-20201114211938363

對於隨機生成的資料, 拍題也正確:

image-20201114212001002

相關文章