动态规划与回溯法解决0-1背包问题

问题描述:

0-1背包: 有N件物品和一个重量为M的背包。(每种物品均只有一件)第i件物品的重量是w[i],价值是p[i]。求解将哪些物品装入背包可使价值总和最大。

动态规划:

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。

动态规划算法可分解成从先到后的4个步骤:                                                    

1. 描述一个最优解的结构,寻找子问题,对问题进行划分。

0-1背包问题具有最优子结构的性质。设(y1,y2,y3,……,yn)是所给的0-1背包问题的一个最优解,则是下面相应的子问题的一个最优解。

《动态规划与回溯法解决0-1背包问题》

公式1-1

《动态规划与回溯法解决0-1背包问题》

公式1-2

     公式1-1和公式1-2的n指的是出项的背包的容量n,m是背包的最大容纳的质量,这可以看见,原问题的最优解出现在子问题的最优解之中,所以我们可以同过记录的形式把子问题的最优解记录现在,这真是我们动态规划中最常看见的打表的思想,通过增减多一个矩阵的空间复杂度来提高程序的时间复杂度。

2. 定义状态。往往将和子问题相关的各个变量的一组取值定义为一个状态。某个状态的值就是这个子问题的解。

假设我们输入的值如下,背包的容量是N = 3,背包的容重 M = 10。我们有三组货物可供选择分别是P[1] = 4,P[2] = 5,P[3] = 6。W[1] = 3, W[2] = 4, W[3] = 5.

3. 找出状态转移方程。一般是从一个状态到另一个状态时变量值改变。

《动态规划与回溯法解决0-1背包问题》

公式1-3

根据公式1-3我们很容易得到如下表格:

《动态规划与回溯法解决0-1背包问题》图1-1

4. 以“自底向上”的方式计算最优解的值。

同过分析我打出了最优子结构的表格现在我们就可以调用这个表格通过自下而上的序列对最优子结构序列进行输出。从最下边的ZYJ[3][10]开始,判断和ZYJ[2][10]的大小,显然比ZYJ[2][10]说明是通过调用子结构加上p[i]得到的,现在我们给它减去w[i]。得到下一个位置ZYJ[2][5]和上面同理推出结果序列。

5. 从已计算的信息中构建出最优解的路径。

源代码如下:写得不好,见笑了

/**
01背包:用动态规划解决01背包,先绘表,按表输出
*/
#include <stdio.h>
#include "timex.h"
#define MAX 32
////////////函数声明/////////////
void printZYJ(int ZYJ[MAX][MAX],int row,int col);//输出最优解表
void printJG(int ZYJ[MAX][MAX],int p[MAX],int n,int m);//输出结果表
void createMatrix(int ZYJ[MAX][MAX],int w[MAX],int p[MAX],int JG[MAX],int row,int col);//打表记录最优解

int main()
{
	//printMyTime();//自己名和时间的函数

	int m,n;
	int i;
	int a; //表示有a组货物供选择
	int p[MAX] = {0};//价格数组
	int w[MAX] = {0};//重量数组
	int ZYJ[MAX][MAX] = {0}; // zyj[i][j]表示装入货物为1<i<n时,质量为1<j<m是,最大的价zyj[i][j]//待优化
	int JG[MAX] = {0};//输出结果序列

	printf("请输入背包的容重和背包的容量\t");
	scanf("%d  %d",&m,&n);
	printf("背包最多容纳的质量是:%d\n背包最大容纳的数量是:%d\n",m,n);
	printf("输入需要进行选择的货物个数\t");
	scanf("%d",&a);	
	printf("输入需要进行选择货物的质量价格序列\n");
	for(i = 1; i <= a; i++){
		printf("第%d货物是:\t",i);
		scanf("%d%d",&w[i],&p[i]);
	}
	createMatrix(ZYJ,w,p,JG,a,m);
	printZYJ(ZYJ,a,m);
	printf("正确的输出序列式:\n");
	printJG(ZYJ,JG,n,m);

	
	return 0;
}
void createMatrix(int ZYJ[MAX][MAX],int w[MAX],int p[MAX],int JG[MAX],int row,int col){
	int i, j;
	for(i = 1; i <= row; i++){
		for(j = 1; j <= col; j++){
			if(w[i]>j){
				ZYJ[i][j] = ZYJ[i-1][j];//质量不允许的情况下,还是上面的最优解
			}
			else{
				if(ZYJ[i-1][j] > ZYJ[i-1][j-w[i]]+p[i]){
					ZYJ[i][j] = ZYJ[i-1][j];
				}
				else{
					ZYJ[i][j] = ZYJ[i-1][j-w[i]]+p[i];
				}
				
			}
		}
	}
	j = col;
		for(i = row;i >= 1; i--){
			if(ZYJ[i][j] > ZYJ[i-1][j]){
				JG[i] = 1;
				j = j - w[i];
			}
			else{
				JG[i] = 0;
			}
		}
}
void printZYJ(int ZYJ[MAX][MAX],int row,int col){
	int i, j;
	for(i = 1; i <= row; i++){
		for(j = 1; j <= col; j++){
			printf("%3d",ZYJ[i][j]);
		}
		printf("\n");
	}
}
void printJG(int ZYJ[MAX][MAX],int p[MAX],int n,int m){
	int i;
	for(i = 1;i <= n; i++){
		printf("%3d",p[i]);
	}
	printf("\n最后的最大质量是:%d\n",ZYJ[n][m]);
}

回溯法:

回溯法中,首先需要明确下面三个概念:

1.约束函数:约束函数是根据题意定出的。通过描述合法解的一般特征用于去除不合法的解,从而避免继续搜索出这个不合法解的剩余部分。因此,约束函数是对于任何状态空间树上的节点都有效、等价的。

2.态空间树:状态空间树是一个对所有解的图形描述。树上的每个子节点的解都只有一个部分与父节点不同。

3.展节点、活结点、死结点:所谓扩展节点,就是当前正在求出它的子节点的节点,在DFS中,只允许有一个扩展节点。活结点就是通过与约束函数的对照,节点本身和其父节点均满足约束函数要求的节点;死结点反之。由此很容易知道死结点是不必求出其子节点的(没有意义)。

利用回溯法解题的具体步骤

首先,要通过读题完成下面三个步骤:

(1)    描述解的形式,定义一个解空间,它包含问题的所有解。

假设我们的输入和上面是一样的背包的容量是N = 3,背包的容重 M = 10。我们有三组货物可供选择分别是P[1] = 4,P[2] = 5,P[3] = 6。W[1] = 3,W[2] = 4, W[3] = 5。假设最后的JG序列如下{x1,x2,……xn}则每种x[i]都有两种可能{0,1},表示x是否进入背包,那么这个0-1背包问题就可以形式化的描述成:

《动态规划与回溯法解决0-1背包问题》公式1-4

《动态规划与回溯法解决0-1背包问题》公式1-5

也就是说0-1背包问题实质上就是求这样一个n维德向量{x1,x2,……xn},x属于{0,1},1<=i<=n,使得的值最大,的同时满足。

 

(2)构造状态空间树。

是向量{x1,x2,x3}的解空间树左箭头表示0右箭头表示1,自然{x1,x2,……xn}其中包涵2n中子树,所以只要搜索这个些可能就可以找到最优解。为了降低空间复杂度,我们只记录一次最优的子序列,这就要求我们对这个解空间树进行两次搜索。第一次找到最大的价值,最后在根据最大价值找出结果序列。

(3)构造约束函数(用于杀死节点)。

递归实现杀死节点,每次判断是不是超出背包的重量如果是的话直接截取。

源代码如下:写得不好,见笑了

/**
程序说明:用回溯法解决01背包
1)建立解空间树找出 
最优条件MAX((i = 1-n)p[i]JG[i]), 
结束条件(i = 1-n)w[i]JG[i]<=m JG[i]={0,1};
*/
#include <stdio.h>
#include "timex.h"
#define MAX 32
////////////函数声明/////////////
void printJG(int p[MAX],int n,int m);//输出结果表
void blJKJ(int count,int m,int n,int *price);//遍历解空间树
void blJKJ_2(int count,int m,int n,int price);//第二次遍历找出最优方案
int isOver(int count,int m);//判断是不是超出边界
int getJG(int count);//获取一组可能的解的价格

int p[MAX] = {0};//价格数组
int w[MAX] = {0};//重量数组
int JG[MAX] = {0};//输出结果序列

int main()
{
	//printMyTime();//调用自己的函数,输出时间,和姓名

	int m,n;
	int i;
	int a; //表示有a组货物供选择
	int price = 0; //表示最优价格

	printf("请输入背包的容重和背包的容量\t");
	scanf("%d  %d",&m,&n);
	printf("背包最多容纳的质量是:%d\n背包最大容纳的数量是:%d\n",m,n);
	printf("输入需要进行选择的货物个数\t");
	scanf("%d",&a);	
	printf("输入需要进行选择货物的质量价格序列\n");
	for(i = 1; i <= a; i++){
		printf("第%d货物是:\t",i);
		scanf("%d%d",&w[i],&p[i]);
	}
	blJKJ(0,m,n,&price);
	printf("最优的价格是:\t");
	printf("%d\n",price);
	printf("正确的输出序列式:\n");
	blJKJ_2(0,m,n,price);
	
	return 0;
}
int isOver(int count,int m){
	int i,j = 0;
	for(i = 1;i <= count; i++){
		j = j + JG[i]*w[i];
	}
	if(j > m)return 1;
	else return 0;
}
int getJG(int count){
	int i , j = 0;
	for( i = 1; i <= count; i++){
		j = j +JG[i]*p[i];
	}
	return j;
}
void blJKJ(int count,int m,int n,int *price){
	int j = 0,k;
	if(isOver(count,m))return ;
	if(count == n){
		j = getJG(count);
		if(j > *price)
			*price = j;
		return ;
	}
	for(k = 1; k >= 0; k--){
		JG[count+1] = k;
		blJKJ(count + 1,m,n,price);
	}
}
void blJKJ_2(int count,int m,int n,int price){
	int i,j = 0,k;
	if(isOver(count,m))return ;
	if(count == n){
		j = getJG(count);
		if(price == j){
			for(i = 1; i <= n; i++ ){
				printf("%3d",JG[i]);
			}
			printf("\n");
			return ;
		}
		return ;
	}
	for(k = 1; k >= 0; k--){
		JG[count+1] = k;
		blJKJ_2(count + 1,m,n,price);
	}
}

 

源代码如下:写得不好,见笑了

    原文作者:动态规划
    原文地址: https://blog.csdn.net/u012222078/article/details/26268537
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞