让你轻松搞懂0-1背包问题(动态规划 C语言版)

题目描述

有n个物品,它们有各自的体积和价值,现有给定容量的背包,如何让背包里装入的物品具有最大的价值总和?
举一个例子:
有一个小偷他有一个容量为8的背包
物体的体积和价值如下所示:

物体编号物体名称体积价值
1手机23
2平板34
3相机45
4电脑58

问小偷如何偷,偷的总价值最高?

思路

你站在小偷的角度思考一下,情况有这几种:

  • 第一种这个物品装不下。没法拿。
  • 第二种这个物品可以装下。但是到底拿不拿?

解决办法

用动态规划的方法来解决这个问题。
动态规划与分治法类似,都是把大问题拆分成小问题,通过寻找大问题与小问题的递推关系,解决一个个小问题,最终达到解决原问题的效果。但不同的是,分治法在子问题和子子问题等上被重复计算了很多次,而动态规划则具有记忆性,通过填写表把所有已经解决的子问题答案纪录下来,在新问题里需要用到的子问题可以直接提取,避免了重复计算,从而节约了时间,所以在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到。

最优性原理是动态规划的基础,最优性原理是指"多阶段决策过程的最优决策序列具有这样的性质:不论初始状态和初始决策如何,对于前面决策所造成的某一状态而言,其后各阶段的决策序列必须构成最优策略。"

动态规划简言之就是: 将小问题依次递推到我们的最终问题。由于小问题都是最优解。
那么最终的问题一定也是最优解。

实战分析

题目的大致分析和动态规划的原理都知道了。接下来,我们就用上面的例子来实际的分析一波。
记f(k,w): 当背包容量为w,现有k件物品可以偷。
例: f(4,8) 的意思是有4件可以偷,背包的容量为8。
从物体编号4,3,2,1依次遍历

在这里插入图片描述
首先定义一些变量:Vk表示第 k个物品的价值,Wk表示第 k个物品的体积,定义f(k,w):当前背包容量 w,现有k件物品可以偷,前 k个物品可选的最佳组合对应的价值。
面对当前商品有两种可能性

  • 包的容量比该商品体积小,装不下,此时的价值与前k-1个的价值是一样的,即f(k,w)=f(k-1,w)
    简言之就是说: 小偷偷这个东西装不下,即不能选这个物品了,那么此时价值是不会多的,且偷的选项少了一个。
  • 还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即f(k,w)=max{f(k-1,w),f(k-1,w-wk)+vk}。

由上图可以得到如下状态转移方程:
在这里插入图片描述


这里需要解释一下,为什么能装的情况下,需要这样求解(这才是本问题的关键所在!):

可以这么理解,如果要到达f(k,w)这一个状态有几种方式?

肯定是两种,第一种是第k件商品没有装进去,第二种是第k件商品装进去了。没有装进去很好理解,就是f(k-1,w);装进去了怎么理解呢?如果装进去第k件商品,那么装入之前是什么状态,肯定是f(k-1,w-w(k))。由于最优性原理(上文讲到),f(k-1,w-w(k))就是前面决策造成的一种状态,后面的决策就要构成最优策略。两种情况进行比较,得出最优。

填表,首先初始化边界条件,f(0,w)=f(k,0)=0;
然后一行一行的填表:

  • 如,k=1,w=1,w(1)=2,v(1)=3,有w<w(1),故f(1,1)=f(1-1,1)=0;
  • 又如k=1,w=2,w(1)=2,v(1)=3,有w=w(1),故f(1,2)=max{
    f(1-1,2),f(1-1,2-w(1))+v(1) }=max{0,f(0,0)+3}=max{0,0+3}=3;
  • 如此下去,填到最后一个,k=4,w=8,w(4)=5,v(4)=8,有w>w(4),故f(4,8)=max{
    f(4-1,8),f(4-1,8-w(4))+v(4) }=max{f(3,8),f(3,3)+8}=max{9,4+8}=12
    根据状态转移方程可以得出如下表
    在这里插入图片描述
    你如果仔细的看一下你会发现,表中的每一个空都是当前的最优解。
    而后面的最优解都是由前面的最优解依次递推出来的。
    由此每一个空都是当前下的最优解。
    例: f(3,5)代表的就是,当背包容量为5,前三个物品可以选时的最优解。
    f(4,8)就是当背包容量为8,有四个物品可以选时的最优解。

代码实现

#include<stdio.h>
#define N 5
#define W 9
int B[N][W]={0};
int w[5]={0,2,3,4,5};
int v[5]={0,3,4,5,8};
void max()
{
	int k,c;
	int value1,value2;
	for(k=1;k<N;k++)//1-4
	{
		for(c=1;c<W;c++)//1-8
		{
			if(w[k]>c)//如果说当前商品的重量大于背包剩余的数量,即:无法偷
			{
				B[k][c]=B[k-1][c];//价值等于上次的价值
			}
			else//可以偷
			{
				value1=B[k-1][c-w[k]]+v[k];//偷的话的价值
				value2=B[k-1][c];//不偷的话的价值
				if(value1>value2)//看哪个最优
				{
					B[k][c]=value1;
				}
				else
				{
					B[k][c]=value2;
				}
			}
		}
	}
}
int main()
{
	max();
	//printf("%d\n",B[4][8]);
	int i,j;
	for(i=0;i<N;i++)
	{
		for(j=0;j<W;j++)
		{
			printf("%d ",B[i][j]);
		}
		printf("\n");
	}
}

背包问题最优解回溯

通过上面的方法可以求出背包问题的最优解,但还不知道这个最优解由哪些商品组成,故要根据最优解回溯找出解的组成,根据填表的原理可以有如下的寻解方式:

f(k,w)=f(k-1,w)时,说明没有选择第k个商品,则回到f(k-1,w);
f(k,w)=f(k-1,w-w(k))+v(k)时,说明装了第k个商品,该商品是最优解组成的一部分,随后我们得回到装该商品之前,
即回到f(k-1,w-w(k));
一直遍历到k=0结束为止,所有解的组成都会找到。


就拿上面的例子来说吧:
在这里插入图片描述

最优解为f(4,8)=12,而f(4,8)!=f(3,8)却有f(4,8)=f(3,8-w(4))+v(4)=f(3,3)+8=4+8=12,所以第4件商品被选中,并且回到f(3,8-w(4))=f(3,3);
有f(3,3)=f(2,3)=4,所以第3件商品没被选择,回到f(2,3);
而f(2,3)!=f(1,3)却有f(2,3)=f(1,3-w(2))+v(2)=f(1,0)+4=0+4=4,所以第2件商品被选中,并且回到f(1,3-w(2))=f(1,0);
有f(1,0)=f(0,0)=0,所以第1件商品没被选择。
在这里插入图片描述

最终代码实现

#include<stdio.h>

#define N 5   
//N代表共有几件可选的数量加1,因为数组的下标是从0开始的
//为了方便所以为 4+1 为5
#define W 9 
//W为当前背包的容量,同样的为了方便加1


int B[N][W]={0};//需要填的表
int intem[N]={0};//存放最优解的情况
int w[5]={0,2,3,4,5};//物体体积的数组
int v[5]={0,3,4,5,8};//物体价值的数组


void max();
void print();
void find(int k,int c);

void max()
{
	int k,c;//k代表有几个可以选,c代表当前的背包体积
	int value1,value2;
	for(k=1;k<N;k++)//1-4
	{
		for(c=1;c<W;c++)//1-8
		{
			if(w[k]>c)//如果说当前商品的重量大于背包剩余的数量,即:无法偷
			{
				B[k][c]=B[k-1][c];//价值等于上次的价值
			}
			else//可以偷
			{
				value1=B[k-1][c-w[k]]+v[k];//偷的话的价值
				value2=B[k-1][c];//不偷的话的价值
				if(value1>value2)//看哪个最优
				{
					B[k][c]=value1;
				}
				else
				{
					B[k][c]=value2;
				}
			}
		}
	}
}
void  find(int k,int c)//找最优解
{
	if(k>0)
	{
		if(B[k][c]==B[k-1][c])//说明该物品没选
		{
				intem[k]=0;
				find(k-1,c);//往下接着递归查找
		}
		if( (c-w[k]>=0) && (B[k][c]==B[k][c-w[k]]+v[k]) )//说明该物体选了
		//
		{
			intem[k]=1;
			find(k-1,c-w[k]);
		}
	}
}
void print()
{
	int i;
	printf("最大价值为:%d\n",B[N-1][W-1]);
	printf("选的物体的编号为:");
	for(i=1;i<N;i++)
	{
		if(intem[i]==1)
		{
			printf("%d ",i);
		}
	}
	printf("\n");
}
int main()
{
	max();
	find(4,8);
	print();
}

效果图如下:
在这里插入图片描述

实战

光看不练假把式,我们看一道题实战一下。
在这里插入图片描述
看了题目你会发现这就是一个0/1背包问题。不过和我们上面讲的好像有点不一样。
但其实大同小异。
总结我们上面所学的动态规划(DP) 无非这几步:

  • 分析状态方程
  • 画表的边界
  • 画表

分析状态方程
我们上面的f(k,w)代表的是最大的价值。
而本问题问的是总的方法数,即最大的方法数。
故设 f(k,w): k代表有几个可选 w代表背包容量
f(k,w)代表当有k个物品可以选时,凑成体积为w的方法数
设Vk表示第 k个物品的体积

通过分析你会发现面对当前商品只有两种可能性:
拿与不拿

  • 不拿:包的容量比该商品体积小,装不下,此时的方法数与前k-1个的方法数是一样的,即f(k,w)=f(k-1,w)
    简言之就是说: 这个东西装不下,即不能选这个物品了,那么此时方法数是不会多的,且能选的物体数少了一个。
  • 拿:还有足够的容量可以装该商品.所以它的方法数等于拿的方法数+不拿的方法总数
    即f(k,w)=f(k-1,w)+f(k-1,w-v[k])

表的边界
当包的体积为0时啥也装不下。只有一种情况
故 f(k,0)=1;
当物体可选的为0时,一种情况也没有
故 f(0,w)=0;

最后画表的过程我这里就省略了。
下面直接上代码

#include<stdio.h>
#define N 21
#define W 41

int f[N][W]={0};
int a[N]={0};
int main(void)
{
	int m;
	int k,w;
	while( scanf("%d",&m) != EOF )
	{
		for(k=1;k<=m;k++)
		{
			scanf("%d",&a[k]);
		}
		for(w=0;w<W;w++)//初始化边界
		{
			f[0][w]=0;
		}
		for(k=0;k<N;k++)//初始化边界
		{
			f[k][0]=1;
		}
		for(k=1;k<=m;k++)
		{
			for(w=1;w<W;w++)
			{
				if(a[k]>w)//不能拿
				{
					f[k][w]=f[k-1][w];
				}
				else//可以拿
				{
					f[k][w]=f[k-1][w]+f[k-1][w-a[k]];//不拿的方法数+拿的方法数
				}
			}
		}
		printf("%d\n",f[m][40]);
	}
	return 0;
}

最后

本篇文章借鉴于这个大佬写的文章https://blog.csdn.net/qq_38410730/article/details/81667885
这个大佬写的很好。
我是按照我的理解在这位大佬的基础上做了补充。

  • 19
    点赞
  • 87
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在C语言中,可以使用动态规划算法来解决0-1背包问题动态规划算法的基本思想是将问题分解为子问题,并利用子问题的解来构建原问题的解。 下面是使用动态规划算法求解0-1背包问题的C语言代码示例: ```c #include <stdio.h> // 定义最大物品数量和背包容量 #define MAX_N 100 #define MAX_W 1000 // 物品的重量和价值 int weight[MAX_N]; int value[MAX_N]; // 动态规划表格 int dp[MAX_N][MAX_W]; // 求解0-1背包问题 int knapsack(int n, int W) { // 初始化第一行和第一列为0 for (int i = 0; i <= n; i++) { dp[i][0] = 0; } for (int j = 0; j <= W; j++) { dp[0][j] = 0; } // 填充动态规划表格 for (int i = 1; i <= n; i++) { for (int j = 1; j <= W; j++) { if (weight[i] <= j) { // 当前物品的重量小于等于背包容量,可以选择放入或不放入背包 dp[i][j] = (value[i] + dp[i - 1][j - weight[i]]) > dp[i - 1][j] ? (value[i] + dp[i - 1][j - weight[i]]) : dp[i - 1][j]; } else { // 当前物品的重量大于背包容量,只能选择不放入背包 dp[i][j] = dp[i - 1][j]; } } } // 返回最优解 return dp[n][W]; } int main() { int n; // 物品数量 int W; // 背包容量 printf("请输入物品数量和背包容量:"); scanf("%d %d", &n, &W); printf("请依次输入每个物品的重量和价值:\n"); for (int i = 1; i <= n; i++) { scanf("%d %d", &weight[i], &value[i]); } int max_value = knapsack(n, W); printf("背包中物品的最大总价值为:%d\n", max_value); return 0; } ``` 以上代码使用二维数组`dp`来表示动态规划表格,其中`dp[i][j]`表示前`i`个物品放入背包容量为`j`时的最大总价值。通过填充动态规划表格,最终得到背包中物品的最大总价值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值