首页 > 代码库 > 动态规划与背包问题

动态规划与背包问题

       动态规划(Dynamic Programming)是算法的设计方法之一,通常用于最优化问题,此类问题可能有多种可行解,而我们希望找出一个最优的解(最大或最小)。动态规划的设计可以分为以下几个步骤:

         1.描述最优解的结构

         2.递归的定义最优解的值

         3.按自底向上的方式计算最优解的值

         4.由计算出的结果构造一个最优解

        第一至第三步构成了动态规划解的基础,第四步在只要求最优解的值时可以省略。

       下面将动态规划应用到背包问题中。

01背包问题

        问题描述:有n个重量和价值分别为wi和vi的物品,从这些物品中挑选出总重量不超过max_w的物品,求所有挑选方案中价值总和的最大值。例如输入:n = 4;   max_w = 5;  w[n] = {2, 1, 3, 2};  v[n] = {3, 2, 4, 2};则输出为 7。

        首先,我们来描述一下最优解的结构:假设用dp(i, j)表示从前i件物品中挑选出总重量不超过j的最大价值,对于第i+1件物品我们有两种选择情况:

        1)在最优方案dp(i+1, j)中选择了第i+1件物品,此时dp(i+1, j) = dp(i, j-w[i]) + v[i];

        2)在最优方案dp(i+1, j)中没有选择第i+1件物品,此时dp(i+1, j) = dp(i, j);

        这样,我们只需要在这两种方案中选择价值总和最大的一种即为最优,于是我们可以得到一个递归定义的最优解的结构:

        dp(i+1, j) =      dp(i, j)   if w[i] > j;  第i+1件物品的总重量超过了j时,我们无法选择该物品

                              max(dp(i, j), dp(i+1, j) = dp(i, j-w[i]) + v[i])   if w[i] <= j;

        我们可以首先得到一个递归的解法,对于每一件物品总是有两种选择情况,所以时间复杂度为O(2^n);

#include<iostream>
#define max(a, b) ((a)>(b)?(a):(b))

const int n = 4;
const int max_w = 5;
int w[n] = {2, 1, 3, 2};
int v[n] = {3, 2, 4, 2};

int recursion01bag(int i, int j){
	if(i == 0)
		return 0;
	if (j<w[i-1])
		return recursion01bag(i-1, j);
	return max(recursion01bag(i-1, j), (recursion01bag(i-1, j-w[i-1])+v[i-1]));
}


int main(){
	int res = recursion01bag(n, max_w);
	printf("%d\n", res);
	system("pause");
	return 0;
}
          下面以一张图来模拟该递归过程

技术分享

        如果函数的输入参数相同,则结果也应该相同。可以发现,在递归过程中有很多重复计算的过程可以省略,基于这种思想,我们可以一个二维数组来保存结果,如果已经计算过了,就直接返回结果值。这样可以将时间复杂度降为O(n*max_w),也称为记忆搜索。

#include<iostream>
#define max(a, b) ((a)>(b)?(a):(b))

const int n = 4;
const int max_w = 5;
int w[n] = {2, 1, 3, 2};
int v[n] = {3, 2, 4, 2};

int dp[n+1][max_w+1];

int memory01bag(int i, int j){
	
	if (dp[i][j] >= 0)
		return dp[i][j];
	if(i == 0)
		return dp[i][j] = 0;
	int res;
	if (j<w[i-1])
		res = memory01bag(i-1, j);
	else
		res = max(memory01bag(i-1, j), memory01bag(i-1, j-w[i-1])+v[i-1]);
	return dp[i][j] = res;
}


int main(){
	memset(dp, -1, sizeof(dp));
	int res = memory01bag(n, max_w);
	printf("%d\n", res);
	system("pause");
	return 0;
}
       接下来我们采用自底向上的方法计算最优解的值,我们可以用二重循环来实现递归过程:
#include<iostream>
#define max(a, b) ((a)>(b)?(a):(b))

const int n = 4;
const int max_w = 5;
int w[n] = {2, 1, 3, 2};
int v[n] = {3, 2, 4, 2};

int dp[n+1][max_w+1];

int dp01bag(){
	for (int i = 0; i < n; i++)
		for (int j = 0; j <= max_w; j++)
			if (j<w[i])
				dp[i+1][j] = dp[i][j];
			else
				dp[i+1][j] = max(dp[i][j], dp[i][j-w[i]]+v[i]);
	return dp[n][max_w];
}


int main(){
	int res = dp01bag();
	printf("%d\n", res);
	system("pause");
	return 0;
}
       在这里我们使用了一个二维数组来存储中间结果,实际上我们完全可以使用一个一维的dp[max_w+1]的数组,并通过不断的重用来完成求解,从而优化内存的空间。   

#include<iostream>
#define max(a, b) ((a)>(b)?(a):(b))

const int n = 4;
const int max_w = 5;
int w[n] = {2, 1, 3, 2};
int v[n] = {3, 2, 4, 2};

int dp[max_w+1];

int dp01bag_optimize(){
	for (int i = 0; i < n; i++)
		for (int j = max_w; j >=w[i]; j--)
				dp[j] = max(dp[j], dp[j-w[i]]+v[i]);
	return dp[max_w];
}

int main(){
	int res = dp01bag_optimize();
	printf("%d\n", res);
	system("pause");
	return 0;
}    

至此我们就完成了对01背包问题的求解。

                                                                              完全背包问题

       问题描述:有n个重量和价值分别为wi和vi的物品,从这些物品中挑选出总重量不超过max_w的物品,求所有挑选方案中价值总和的最大值。在这里,每种物品可以被挑选多次。例如输入 n = 3;  max_w = 7;  w[n] = {3,4,2};  v[n] = {4,5,3};  输出应为10(选择0号物品1个, 2号物品2个)。

       基于01背包的求解,我们做出如下假设:假设用dp(i, j)表示从前i件物品中挑选出总重量不超过j的最大价值,对于第i+1件物品我们有两种选择情况:

        1)在最优方案dp(i+1, j)中选择了第i+1件物品,假设该件物品被挑选k次(0<k<max_w),此时dp(i+1, j) = dp(i, j-k*w[i]) + k*v[i];

        2)在最优方案dp(i+1, j)中没有选择第i+1件物品,此时dp(i+1, j) = dp(i, j);

       和01背包相比,最棘手的是多了一个k,如果使用一个三重循环的话时间复杂度为O(n*max_w^2)。那么我们是否可以不使用这个k值,基于此思路我们可以获得如下的思路:  假设用dp(i, j)表示从前i件物品中挑选出总重量不超过j的最大价值,对于第i+1件物品是否被选中有两种情况:

        1)在最优方案dp(i+1, j)中选择了第i+1件物品,此时dp(i+1, j) = dp(i+1, j-w[i]) + v[i],在这里我们并不用k来标记次数,而是将第i+1件物品仍然留下,以作下次循环;

        2)在最优方案dp(i+1, j)中没有选择第i+1件物品,此时dp(i+1, j) = dp(i, j);

       于是我们得到了问题的解:dp(i+1, j) =      dp(i, j)   if w[i] > j;  第i+1件物品的总重量超过了j时,我们无法选择该物品

                                                                    max(dp(i, j), dp(i+1, j) = dp(i+1, j-w[i]) + v[i])   if w[i] <= j;

        利用自底向上的方法求解:

#include<iostream>
#define max(a, b) ((a)>(b)?(a):(b))

const int n = 3;
const int max_w = 7;
int w[n] = {3,4,2};
int v[n] = {4,5,3};

int dp[n+1][max_w+1];

int dpbag(){
	for (int i = 0; i < n; i++)
		for (int j = 0; j <= max_w; j++)
			if (j<w[i])
				dp[i+1][j] = dp[i][j];
			else
				dp[i+1][j] = max(dp[i][j], dp[i+1][j-w[i]]+v[i]);
	return dp[n][max_w];
}

int main(){
	int res = dpbag();
	printf("%d\n", res);
	system("pause");
	return 0;
}



动态规划与背包问题