首页 > 代码库 > 动态规划与背包问题
动态规划与背包问题
动态规划(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; }
动态规划与背包问题