0/1背包问题以动态规划方法详解

动态规划

主要思想

顾名思义,就是动态地根据每个阶段的状态解决问题,而且这种解决方法是可以通过代码表达的,而能够应用这种方法的问题有以下特点——
每个阶段的状态是未知的,但是可预知且可列的,并且与前一阶段相关,这种特点常常决定了它的递推式;
同时,满足最优子结构,子问题的最优构成了整体的最优,所以递推是可行的;
在决策定下后,该决策也不会因为后续阶段的决策而受到影响。

0/1背包问题

问题描述

有n个背包,已知每个背包的重量wi和价值vi,在总重量W固定的情况下,选择背包,使总价值V最大。

递归实现

基本递推式

设f(i,y)表示总重量为y时,放入i,…n背包可以得到的最大价值,那么:

f(i, y) = max{ f(i + 1, y), F(i + 1, y - w[i]) + v[i] }, if i < n && y >= w[i];//递归式1
f(i, y) = f(i + 1, y) if i < n && y < w[i]; // 递归式2
f(i, y) = v[n], if i == n && y >= w[n]//终止条件1
else f(n, y) = 0;//终止条件2

代码实现
int RecursiveDp(int s, int w){
	if(s == n){
		if(w[n] > w){
			return 0;
		}
		else return v[n];
	}
	if(w[s] > w){
		return RecursiveDp(s + 1, w);
	}
	else return max(RecursiveDp(s + 1, w), Recursive(s + 1, w - w[s]) + v[s]);
} 
时间复杂度

T(n) = 2*T(n - 1) + c (c为常数)
则时间复杂度为O(2 ^ n)

价值为整数时的非递归实现

基本思路

设置一个二维数组dp[n][W],代替f(i,y),减少了重复调用的那部分时间(用空间换时间)

代码实现
int n,w;
int dp[MAX_N][MAX_N];

void CreateDp(){
	for(int i = 0; i <= w; i ++){
		dp[n][i] = w[n] >= i ? 0 : v[n]; 
	}
	for(int i = n - 1; i >= 1; i --){
		for(int j = 0; j <= w; j ++){
			if(w[i] > j){
				dp[i][j] = dp[i + 1][j];
			}
			else{
				dp[i][j] = max(dp[i + 1][j], dp[i + 1][j - w[i]] + v[i]);
			}
		}
	}		
}

int solve(){
	return dp[1][w];
}
复杂度

空间复杂度:dp数组大小为n*w,如果w很大,其实就有风险;
时间复杂度:创建dp数组的时间复杂度为O(n * w),如果w = 2 ^ n, 时间复杂度就很高,dp的实现的时间复杂度为O(1)

元组法

基本思想

如果总重量较大,dp数组的空间会很大,需要得到优化,通过观察,我们发现dp数组的每一行的值是重复的,直到在某个位置发生突变,而且这种突变是有规律的,那么我们只需要存入这些突变的点

通过实例理解

实例:n=5,v=[6,3,5,4,6],w=[2,2,6,5,4] 且c=10 ,求f (1,10).
在这里插入图片描述

f(1,10)=max{f(2,10),f(2,10-2)+6}=15

  • 跳跃的位置即是和是否加入该背包紧密相关,同时与前一个情况做了比较
代码实现
int n,w;
int jw[MAX_N];
int jv[MAX_N];


int Jumppoint(int num, int wei){
	jw[0] = 0;
	jv[0] = 0;
	jw[1] = w[0];
	jv[1] = v[0];//初始状态 
	int left = 0, right = 1, next = 2;//[left,right]为上一层的比较和结合范围 ,next表示构建新的元组的索引 
	for(int i = 1; i < num; i ++){//依次向上更新 和f(i,y)的含义刚好相反 
		int k = left;
		for(int j = left; j <= right; j ++){//以k为索引,依次结合 
			if(jw[j] + w[i] > wei)break;//一旦超重,就不用看后面了,因为重量递增,价值递增 
			int nw = jw[j] + w[j];
			int nv = jv[j] + v[j];//结合情况 
			for(; k <= right && jw[k] < nw; k ++, next ++){
				jw[next] = jw[k];
				jv[next] = jv[k];
			}//小于结合重量的跳跃点不受影响,但还是跳跃点 
			for(; k <= right && jw[k] == nw; k ++){
				if(jv[k] > nv)nv = jv[k];
			}//如果和结合重量相等,则价值大的优先 
			if(nv > jv[next - 1]){
				jw[next] = nw;
				jv[next] = nv;
				next ++;
			}//判断新结合的这个点是否为合格的跳跃点,做到重量递增的时候,价值也递增 
			else continue;
			for(; k <= right && jv[k] <= nv; k ++);//后面的点重量更大,如果价值更小,则无效了 
		}
		for(;k <= right; k ++, next ++){
			jw[next] = jw[k];
			jv[next] = jv[k];
		} 
		left = right + 1;
		right = next - 1;
	}
	return jv[right];//最后一个点就是所需结果
}


int solve(){
	return Jumppoint(n,w);
}
复杂度

空间复杂度:最坏为2 * (2 ^ n - 1) * 2
时间复杂度: 最坏也为 O(2 ^ n)
所以使用这种方法是不稳定的

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值