赞
踩
经典问题描述:有n 个物品,它们有各自的重量,现有给定容量w的背包,如何让背包里装入的物品具有最大的重量?
回溯算法解背包问题
假设我们有五个物品,重量分别为2,3,3,7,5,背包所能容纳的最大重量为10.怎么解呢?最笨的方法就是使用回溯算法穷举所有的情况,直到遇到临界情况,递归树如下:
其中每个节点f(i,cw),代表决策第i个物品是否放入,并且此时背包里所有物品总重为cw。因此,回朔算法的时间复杂度成指数增长,并且包含了打量了重复运算(比如两个f(2,2))。如果我们的物品i遍历完毕,或者cw超出背包的总重,即临界退出条件,因此我们可以给出回溯算法的代码(物品重量用数组items表示,w代表背包容量,maxW存储当前最大重量,这些自己设置):
- void knapsackProblem::backTracing(int i, int cw)
- {
- if (i == n || cw == w) {//i==n代表所有物品加载完毕,cw==w代表背包填满,则直接跳出
- if (cw > maxW) maxW = cw;
- return;
- }
- //装与不装第i个物品两种情况
- backTracing(i + 1, cw);//选择不装第i个物品
- if (cw + items[i] <= w) {//如果第i个物品能放进去,就选择装第i个物品
- backTracing(i + 1, cw + items[i]);
- }
- }
可以看出,回溯算法对决策时的每种情况都进行了分析,时间复杂度为2^n,这实在太多庞大(比如n为10000时)。我们分析复杂度如此高的原因是回朔算法进行了大量重复的运算,如果我们能设置一个备忘录来避免这些重复的运算,那岂不是妙哉?既然每个节点的两个参数是物品次序和当前背包重量,我们可以采用一个bool类型的二维数组来记忆是否已经进行了相关运算。
- bool** mem = new bool*[n];
- for (int i = 0; i != n; ++i) {
- mem[i] = new bool[w + 1];
- }
-
- for (int i = 0; i != n; ++i) {
- for (int j = 0; j != w + 1; ++j) {
- mem[i][j] = false;
- }
- }
然后给出加载了备忘录的回溯算法:
- void knapsackProblem::backTracing(int i, int cw)
- {
- if (i == n || cw == w) {//i==n代表所有物品加载完毕,cw==w代表背包填满,则直接跳出
- if (cw > maxW) maxW = cw;
- return;
- }
- if (mem[i][cw]) return;//如果这个节点运算过,则直接返回
- mem[i][cw] = true;//否则将这个节点的运算状态设为true
- //装与不装第i个物品两种情况
- backTracing(i + 1, cw);//选择不装第i个物品
- if (cw + items[i] <= w) {//如果第i个物品能放进去,就选择装第i个物品
- backTracing(i + 1, cw + items[i]);
- }
- }
这样就避免了那些复杂的运算,用空间换来了时间,这其实就有点类似于动态规划的思想方法了。但是回溯算法尚且有局限性(比如节点有三个参数或者四个参数时很难使用备忘录),接下来我们看看动态规划如何实现背包问题。
动态规划解背包问题
其实上面带有备忘录的回朔算法已经十分接近动态规划了,动态规划中我们通过上一层的状态来决策下一层的状态,并且只统计那些不重复的节点,例如我们由f(1,0)和f(1,2)推出f(2,0),f(2,2),f(2,5).这样可以保证每层的节点数永远不会超过cw的最大值,即w(背包容量).整个过程依然可以通过一个表格来进行表示:
第一次决策 第二次决策
第三次决策 第四次决策
第五次决策
注意:2列应当置1
分析:第一次决策是否装第一个物品,其重量为2,所以决策完毕有总重0和2两种情况。
第二次决策第二个物品,其重量为3,我们只需要从第一行分析即可,不装第二个物品时,摘抄第一行即可,装第二个物品时就分别加3.
以此类推~~
决策完毕只需要取出最后一行对应的最大重量即可。
因此,动态规划算法也需要一个辅助的二维数组来存储节点状态。
- bool** states = new bool*[n];
- for (int i = 0; i != n; ++i) {
- states[i] = new bool[w + 1];
- }
- for (int i = 0; i != n; ++i) {
- for (int j = 0; j != w + 1; ++j) {
- states[i][j] = false;
- }
- }
- //第一行需要特殊处理,设置为true;
- states[0][0] = true;
- states[0][items[0]] = true;
因此动态规划的代码如下,不是很复杂:
- int knapsackProblem::dynamicProgramming(int * items, int n, int w)
- {
- for (int i = 1; i != n; ++i) {
- //不装第i个物品
- for (int j = 0; j <= w; ++j) {
- if (states[i - 1][j] == true) states[i][j] = true;
- }
- //装第i个物品
- for (int j = 0; j <= w - items[i]; ++j) {
- if (states[i - 1][j] == true) states[i][j + items[i]] = true;
- }
- }
- //输出可装最大重量,即在最后一行从后往前遍历
- for (int i = w + 1; i >= 0; --i) {
- if (states[n - 1][i] == true)return i;
- }
- return 0;
- }
此算法的时间复杂度为n*w,空间复杂度也为n*w。
有没有办法对空间复杂度进行改进呢,答案是有的,因为此二维数组有很多重复的地方,我们完全可以用一维数组代替。
- int knapsackProblem::dynamicProgramming1(int * items, int n, int w)
- {
- bool* states = new bool[w + 1];
- for (int i = 0; i != w + 1; ++i) {
- states[i] = false;
- }
- states[0] = true;
- for (int i = 1; i != n; ++i) {
- //因为如果不装第i个物品,此一维数组不会有任何变动,所以我们直接考虑装第i个物品
- for (int j = w - items[i]; j >= 0; --j) {
- //为什么要从大往小便利呢?因为下面的循环体是前值影响后值
- if (states[j] == true) states[j + items[i]] = true;
- }
- }
- for (int i = w; i >= 0; --i) {
- if (states[i] == true)return i;
- }
- return 0;
- }
背包问题的升级
对于上面的背包问题已经解决,可如果再增加一个限制条件呢?如:刚刚的问题只涉及背包容量和物品重量,可如果我们引入物品价值这一变量,对一组不同重量不同价值的物品,我们选择一些物品加入背包,在不超出背包最大容量的限制下,可装入背包物品的总价值最大是多少呢?
当然,我们依然可以使用笨重的回溯算法来解决这个问题,此时每个节点就有了三个参数分别为i(物品),cw(当前总重量),cv(当前总价值),具体实现代码如下:
- void knapsackProblem::backTracing(int i, int cw, int cv)
- {
- if (i == n || cw == w) {//i==n代表所有物品加载完毕,cw==w代表背包填满,则直接跳出
- if (cw > maxW) maxW = cw;
- return;
- }
- //装与不装第i个物品两种情况
- backTracing(i + 1, cw, cv);//选择不装第i个物品
- if (cw + items[i] <= w) {//如果第i个物品能放进去,就选择装第i个物品
- backTracing(i + 1, cw + items[i], cv + value[i]);
- }
- }
假设物品价值为 int* value ={3,7,5,2,6},照样画出递归树示意图如下(每个节点代表物品,目前总重,目前总价):
我们可以发现虽然暂时没见到重复的节点,可是对比f(2,2,7)和f(2,2,3),相同重量必然选择较高价值的,因此f(2,2,7)可以直接忽略不计。可是这又很难用备忘录来实现,那么动态规划如何解决这个问题呢?
依然借助二维数组,不过此时二维数不再用bool来表示,而用int代表每个状态的value值,初始化为-1.如下:
- v_states = new int*[n];
- for (int i = 0; i != n; ++i) {
- v_states[i] = new int[w + 1];
- }
- for (int i = 0; i != n; ++i) {
- for (int j = 0; j != w + 1; ++j) {
- v_states[i][j] = -1;
- }
- }
- states[0][0] = 0;
- states[0][items[0]] = value[0];
动态规划本身则和之前的算法大同小异,具体如下:
- int knapsackProblem::dynamicProgramming(int * items, int * value, int n, int w)
- {
- for (int i = 1; i != n; ++i) {
- //选择不装第i个物品
- for (int j = 0; j != w; ++j) {
- if (v_states[i - 1][j] >= 0) v_states[i][j] = v_states[i - 1][j];
- }
- //选择装第i个物品
- for (int j = 0; j <= w - items[i]; ++j) {
- if (v_states[i - 1][j] >= 0) {
- int v = v_states[i - 1][j] + value[i];
- if (v > v_states[i][j + items[i]]) {
- v_states[i][j + items[i]] = v;
- }
- }
- }
- }
- //寻找最大值
- int maxV;
- for (int j = 0; j <= w; ++j) {
- if (v_states[n - 1][j] > maxV) maxV = v_states[n - 1][j];
- }
- return maxV;
- }
当然,此种方法也可以优化一下空间复杂度,留给读者自己分析。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。