动态规划---背包问题详解

概述

背包问题是动态规划的典型问题,掌握好对我们以后求解各类问题都能提供很好的思路,下面我将分类对各种背包问题进行详解,哪里不清楚或有错误欢迎评论留言,期待和大家共同进步。

1. 0-1背包问题

1.1问题描述:

有N件物品和一个容量为V的背包,放入第i件物品耗费的费用是Ci,得到的价值是Wi。求解将哪些物品装入背包可使价值总和最大。

1.2基本思路

解决背包问题最核心的点是要有一种动态规划的思想,即每放入一件物品得到的最优解都是建立在放置前最优解的基础上的,假设我们外出旅行,要往自己的背包(容量为6)里放面包(价值为3,费用为2)、火腿(价值为2,费用为2)、电脑(价值为5,费用为4)、小说(价值为1,费用为3),下面我们开始一件一件放,首先放面包,背包是空的放进面包的总价值肯定是要大于原先的总价值,然后放火腿,直接放就好了,这时背包已经用去了4个容量,价值为5,下面来放关键的电脑,大家注意了,放入电脑是否能使背包价值更大呢?这取决于

1.不放电脑背包的最大价值

2.给电脑腾出空间时背包的最大的价值再加上电脑价值

两者哪个更大,如果第一个所得背包价值大那么我们就不放电脑,如果第二个大那么我们就放入电脑,这里不放电脑背包的最大价值是5,给电脑腾出空间背包的最大价值是3,即取出火腿使背包空间为4(这里为什么取出火腿而不取出面包大家要明白),放入价值为5的电脑,此时背包总价值为5+3=8,8大于5,因此放入电脑,此时背包中有面包和电脑,价值为8,容量全部用完。

最后放小说,读者可通过上面的思路来计算,易知不放小说背包的价值最大。

通过上面这个例子希望能帮助大家理解这个思想,下面我们就要引入最重要的状态转移方程了,背包问题的核心式子。

F [i, v] = max { F [i - 1, v ], F [i - 1, v - Ci ] + Wi }

其中F[i,v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。“将前i 件物品放入容量为v 的背包中”这个子问题,若只考虑第i 件物品的策略(放或不放),那么就可以转化为一个只和前i - 1 件物品相关的问题。如果不放第i 件物品,那么问题就转化为“前i - 1 件物品放入容量为v 的背包中”,价值为F [i - 1, v];如果放第i 件物品,那么问题就转化为“前i - 1 件物品放入剩下的容量为v - Ci 的背包中”,此时能获得的最大价值就是F [i - 1, v - Ci ] 再加上通过放入第i 件物品获得的价值Wi 。

1.3代码实现

上面的一段话很关键,如果不理解请结合我在前面具体的例子,下面我贴出用c++实现的具体代码。

#include<iostream>
using namespace std;
const int MAXLENGTH=10000;
int F[MAXLENGTH][MAXLENGTH];
int main(){
	int N,V;				//N代表要放入的物品数量,V代表背包的容量
	cin>>N>>V;
	int Ci[N+1],Wi[N+1];	                //Ci表示第i件物品的费用,Wi表示第i件物品的价值 
	for(int i=1;i<=N;i++){
		cin>>Ci[i]>>Wi[i];
	}
	for(int i=0;i<=N;i++){
		for(int j=0;j<=V;j++){
			F[i][j]=0;
		}
	}
	for(int i=1;i<=N;i++){
		for(int j=1;j<=V;j++){
			if(j>=Ci[i]){	        //容量为j时第i件物品可放 
				F[i][j]=max(F[i-1][j],F[i-1][j-Ci[i]]+Wi[i]);
			}else{
				F[i][j]=F[i-1][j];
			}
		}
	}
	cout<<F[N][V]<<endl;
} 
/*
测试数据:
4 6
2 3
2 2
4 5
3 1
输出:8
*/ 

这个代码我觉得应该不难理解,主要就是一个填充数组的过程。以开头举得例子来填充一下这个数组F[i][v]。

F[i][j]数组
0123456
00000000
1.面包0033333
2.火腿0033555
3.电脑0033558
4.小说0033558

1.4优化

时间复杂度为O(VN)已经无法优化,但空间复杂度还可以优化,通过观察上面的表格我们可以发现,每次放入一件物品都是对原来内容的更新,因此我们完全可以将内容覆盖,用一个一维数组来解决问题。这里注意:第二层for循环V一定要是递减的,这是为了保证第i 次循环中的状态F [i, v ] 是由状态F [i - 1, v - Ci ] 递推而来,否则过早的覆盖导致原状态的丢失。想象如果顺着来,那么F[i-1,v-Ci]的值已经不是我们上一次数组中的值了,而是我们刚刚填进去的,造成了覆盖,状态丢失。

下面贴出代码,注意与上面的小区别。

#include<iostream>
using namespace std;
const int MAXLENGTH=10000;
int F[MAXLENGTH];
int main(){
	int N,V;
	cin>>N>>V;
	int Ci[N+1],Wi[N+1];
	for(int i=1;i<=N;i++){
		cin>>Ci[i]>>Wi[i];
	}
	for(int i=0;i<=V;i++){
		F[i]=0;
	} 
	for(int i=1;i<=N;i++){
		for(int j=V;j>=Ci[i];j--){
			F[j]=max(F[j],F[j-Ci[i]]+Wi[i]);
		}
	}
	cout<<F[V]<<endl;
	return 0;
} 

/*
测试数据:
4 6
2 3
2 2
4 5
3 1
输出:8
*/ 

2.完全背包问题

2.1问题描述

有N 种物品和一个容量为V 的背包,每种物品都有无限件可用。放入第i 种物品的费用是Ci ,价值是Wi 。求解:将哪些物品装入背包,可使这些物品的耗费的费用总和不超过背包容量,且价值总和最大。

2.2基本思路

可以发现,该问题与基本背包问题的区别就是每种物品有无限件可用,但是根据刚才的思路,我们可以轻松的得到状态转移方程:

F [i, v] = max{ F [i - 1, v - kC i ] + kW i | 0 ≤kC i ≤ v}

无非就是多加了一个参数k而已,再刚刚的思路上多加一个对k的for循环就行了,当然根据刚刚对空间复杂度的优化我们可以直接用一维数组来解决问题,理解了0-1背包问题,这个很好写出来。

2.3代码实现

根据刚刚的状态转移方程,再加上对空间复杂度的优化,很容易便可得到下面的代码。

#include<iostream>
using namespace std;
const int MAXLENGTH=10000;
int F[MAXLENGTH];
int main(){
	int N,V;
	cin>>N>>V;
	int Ci[N+1],Wi[N+1];	//ci代表费用,wi代表价值 
	for(int i=1;i<=N;i++){
		cin>>Ci[i]>>Wi[i];
	}
	for(int i=0;i<=V;i++){
		F[i]=0; 
	}
	for(int i=1;i<=N;i++){
		for(int j=V;j>=Ci[i];j--){
			for(int k=1;k*Ci[i]<=j;k++){
				F[j]=max(F[j],F[j-k*Ci[i]]+k*Wi[i]);
			}
		}
	}
	cout<<F[V]<<endl;
	return 0;
} 
/*
测试数据:
输入:
6 10
3 6
2 5
5 10
1 2
6 16
4 8 
输出:
26
*/ 

2.4优化

2.4.1转换为0-1背包问题

这一点其实主要是想强调思路而非优化,我们可以将完全背包问题转换为0-1背包问题,也就是把k件商品和k件商品的价值也当作一件商品放入数组中再进行遍历,k需要满足条件k*Ci<=V,这里有一个很好的思路就是把第i种商品拆成费用为Ci*2^k、价值为Wi*2^k的若干件物品,其中k取遍满足Ci*2^k<=V的非负整数。

这是二进制的思想。因为,不管最优策略选几件第i 种物品,其件数写成二进制后,总可以表示成若干个2^k件物品的和。这样一来就把每种物品拆成O( log V / Ci ) 件物品,是一个很大的改进。

下面我直接贴出源码,因为数量不固定,我使用了vector动态数组,这也是和上面的实现上不同的地方,但是在思路上完全是转换为0-1背包问题了。

#include<iostream>
#include<vector>
using namespace std;
const int MAXLENGTH=10000;
int F[MAXLENGTH];
int main(){
	int N,V;
	cin>>N>>V;
	vector<int> Ci;
	vector<int> Wi;				//ci代表费用,wi代表价值 
	Ci.push_back(0);Wi.push_back(0);
	for(int i=1;i<=N;i++){
		int tc,tw;
		cin>>tc>>tw;
		Ci.push_back(tc);
		Wi.push_back(tw);
	}
	for(int i=1;i<=N;i++){				//第i种商品取k件也看作一件商品 
		for(int k=2;k*Ci[i]<=V;k*=2){
			Ci.push_back(k*Ci[i]);
			Wi.push_back(k*Wi[i]);
		}
	}
	N=Ci.size()-1; 						//更新数量N 
	for(int i=0;i<=V;i++){
		F[i]=0; 
	}
	for(int i=1;i<=N;i++){
		for(int j=V;j>=Ci[i];j--){
			F[j]=max(F[j],F[j-Ci[i]]+Wi[i]);
		}
	}
	cout<<F[V]<<endl;
	return 0;
} 
/*测试数据:
6 10
3 6
2 5
5 10
1 2
6 16
4 8 
输出:26*/ 

2.4.2 O(VN)的算法

我们在解决0-1背包问题的时候,用一维数组时第二层for循环是从V递减到Ci,让V递减是为了保证第i 次循环中的状态F [i, v ] 是由状态F [i - 1, v - Ci ] 递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i 件物品”这件策略时,依据的是一个绝无已经选入第i 件物品的子结果F [i - 1, v - Ci ]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i 种物品”这种策略时,却正需要一个可能已选入第i 种物品的子结果F [i, v - Ci ],所以就可以并且必须采用V递增的顺序循环。因此我们可以直接得到一种简单的求解完全背包问题的方法。

#include<iostream>
using namespace std;
const int MAXLENGTH=10000;
int F[MAXLENGTH];
int main(){
	int N,V;
	cin>>N>>V;
	int Ci[N+1],Wi[N+1];
	for(int i=1;i<=N;i++){
		cin>>Ci[i]>>Wi[i];
	}
	for(int i=0;i<=V;i++){
		F[i]=0;
	} 
	for(int i=1;i<=N;i++){
		for(int j=Ci[i];j<=V;j++){
			F[j]=max(F[j],F[j-Ci[i]]+Wi[i]);
		}
	}
	cout<<F[V]<<endl;
	return 0;
} 
/*测试数据:
6 10
3 6
2 5
5 10
1 2
6 16
4 8 
输出:26*/ 

这个代码我是直接将0-1背包的代码ctrl v过来的,然后改了第二个for循环,将v递减改为v递增,仅此而已。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值