动态规划初步—— 背包问题

5 篇文章 0 订阅

动态规划初步

动态规划认知


​ 在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到。

01 背包


问题

有n件物品和一个容量为m的背包,放入第i件物品所需要的空间为wi,第i件物品的价值为vi,问背包可以放入物品的最大价值为多少?

  1. 此问题不可用贪心算法,贪心法得到的往往不是最优解。
  2. 直接暴力穷举n件物品,时间复杂度是O(2^n),效率过低,且存在大量重复运算

算法

​ 综上我们采用两重循环的方式来解决此问题,避免了复杂的递归,并且采用数组存储数据,使得计算次数大大减少,复杂度由O(2^n)优化到了O(V*W)

for(int i=1; i<=n; i++){   		//物品 
    for(int j=V; j>=0; j--){    //容量 
        if(j >= w[i])	//放得下物品,使用状态转移方程
            dp[i][j] = max(dp[i-1][j-w[i]]+v[i], dp[i-1][j]);
        else      		//放不下物品,直接跳过,沿承 i-1 个物品的价值
            dp[i][j] = dp[i-1][j];           
    }
}

优化

空间优化

​ 实际上,二维数组的空间复杂度过高,有时候也不现实,所以有时采用状态压缩,将二维数组压缩至一维。

压缩原理:实质上,i本身无实质性意义,在第 i+1 次循环尚未开始之前,第 i 行已经保存了当前的数据,且 0 到 i-1 行就没有使用价值了,故可以将二维数组压缩为一维数组,从后向前遍历,状态转移方程中的 [i-1] 就无存在价值了。

​ 对于外层循环中的每一个 i 值,其实都是不需要记录的,在第 i-1 次循环已结束且第 i 次循环未开始,dp数组还未更新时,**dp[j]**还记录着前 i-1 个物品在容量为 j 时的最大价值,这样就相当于还记录着 dp[i-1][j]dp[i-1][j-vol[i]]+val[i]

状态转移方程修改为 dp[j] = max(dp[j], dp[j - w[i]] + v[i])

恰好装满

如果要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1…V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。

如果没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0…V]全部设为0。 ——《背包九讲》

​ 可以将其理解为不能把背包填满的解都是非法(价值小于零)的,从而在计算中排除这些解。或者说当前的合法解一定是从之前的合法状态推得的。

​ 这个技巧可以推广到其他背包问题,不仅仅限于01背包。

常数优化

​ 经过图表可知,并不需要将表中的数据全部算出,只需要算一部分即可。因为最后所求结果为dp[n][v],其在 n-1 行所需的最小的 jv-w[n-1],同理,对于第 i 行, j 最小循环至 v- sum{w[n…i-1]} ,综上所述我们可以优化第二层的循环次数,代码如下:

for(int i=1;i<=n;i++)
    bound=max{V-sum{w[i..n]},w[i]};
    for(int j=V;j>=bound;j--)

​ 因为若是 V-sum{w[i…n]} < w[i] 的话,则无法取到第 **i **个物品了,所以就把下限定为 w[i],进一步减少第二层循环次数,这种方法在V很大的情况下会节省时间。

完全背包


问题

有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

思考方向还是先从二维数组下手,得到转移方程,再向一维数组优化


算法

此处采用和01背包相似的思考方式,01背包每个物品都拿一次,所以对于同一个 i 下的 j 都应该是独立的,即每个 j 中至多包含一个当前物品,所以由后向前遍历,但是01背包中,每个 i 不止用一次,可能用多次,所以偏大的 j 中的情况就不应从i-1 那一行转移,而应该是从同一行中转移,这样才能保证可以出现多个 i

for(int i = 1; i <= n; i++)			//正序
    for(int j = 0; j <= V; j++){	//正序
        if(j >= w[i]){			//放得下
        	dp[i][j] = max{dp[i-1][j] , dp[i][j-w[i]] + v[i];
        } else{				   //放不下
            dp[i][j] = dp[i-1][j];         
        }
    }

思维陷阱

背包问题其实有一个隐藏的条件,就是包的体积和物品的质量其实都是整数,虽说是无限多的物品,但是实质上还是枚举出来了,在 i 行的遍历一定能枚举所有含 i 情况

完全背包每次做出选择是取决于当前这一步的,而01背包每次做出选择是取决于上一步的。主要就是因为当前这一步一个物品放过以后,表格逐渐向右填写,随着可放空间的增加,可以判断这一步是否还可以再放一个当前的物品。

多重背包

题目

有N种物品和一个容量为V的背包。第i种物品最多有num[i]件可用,每件费用是w[i],价值是c[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

思路

此题显然有朴素算法,即将所有的物品一个一个地进行验证,也就是转化成01背包,但是这样的复杂度达到了O(V*Σnum[i]),复杂度很高.

我们考虑一下,发现对于任意一个num[i],我们都可以按照二进制拆成ceil(log(num[i]))个物品,每个新物品分别代表着 1,2,4...2^k,num[i]-2^k个物品,同理价值和重量也是按比例增加的,然后采用01背包处理,这样就把复杂度降到了 O(V*Σ(log num[i])),有了很大的改善.(看博客说可以用单调队列优化到O(V*N)),此点后续补充

然后我们还发现了一点优化空间,如果背包无法装下某物品的总量,那么此物品相对于背包就是无限的,此处可以用完全背包来解决.

代码

综上所述,我们有如下的算法

int n, V; // n为物品个数,V为背包总体积

void ZeroOnePack(int weight, int value) { // 01背包
    for (int i = V; i >= weight; i--) {
        dp[i] = max(dp[i], dp[i - weight] + value);
    }
}

void CompletePack(int weight, int value) { // 完全背包
    for (int i = weight; i <= V; ++i) {
        dp[i] = max(dp[i], dp[i - weight] + value);
    }
}

void MultiplePack(int weight, int value, int number) { // 多重背包
    if (number * weight >= V) { // 对于总容量来说此物品是无限的,所以采用完全背包处理
        CompletePack(weight, value);
    } else { //否则需要特殊处理
        // 分解成01背包问题(采用二进制优化背包个数到o(求和(lgC[i]))
        for (int k = 1; k <= number; k <<= 1) { // 因为是二进制,所以起始位置一定是1,终止位置一定是number
            ZeroOnePack(k * weight, k * value);
            number -= k;
        }
        ZeroOnePack(number * weight, number * value); // 将为二进制分完之后的部分01背包处理
    }
}

混合三种背包

题目

给出n个物品,有的是只有一个,有的是无限的,有的是有限个,求最大价值.

思路

根据从多重背包最后的代码中得到的启示,我们完全可以将三个背包函数全部封装好,然后直接调用即可.这里也体现了封装的重要性.

代码

for i=1..N
    if 第i件物品属于01背包
        ZeroOnePack(c[i],w[i])
    else if 第i件物品属于完全背包
        CompletePack(c[i],w[i])
    else if 第i件物品属于多重背包
        MultiplePack(c[i],w[i],n[i])

二维费用背包问题

题目

对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为w[i]。

思路

费用只加了一维,自然dp也要增加一维

然后在转移时注意维护两个费用即可,至于循环方向和之前的相同

dp[i][v][u]=max{dp[i-1][v][u],dp[i-1][v-a[i]][u-b[i]]+w[i]}

当然,此处表示个数的第一维也是可以省略的.

总个数的限制

有的背包问题除了对体积有限制之外还对总共的个数有限制,我们发现这就是加了一个限制条件,也就是加了一维费用,每个物品都要占据’一个位子’,也就是上文中的b[i]恒等于1.

分组背包

问题

有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

思路

背包九讲中给出了这样的算法

这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设f[k][v]表示前k组物品花费费用v能取得的最大权值,则有:f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于组k}

也就是对于同一个v,我们必须完成当前组里面所有的物品的加入

for 所有的组k
    for v=V..0
        for 所有的i属于组k
            f[v]=max{f[v],f[v-c[i]]+w[i]}

切记二三层循环顺序不能反过来,如果反过来就等价于我们对所有的物品(无论他们是不是在一组)进行了01背包.

尾声

后续的 "依赖背包"和"泛化背包"目前还没有好的理解方式,日后再做研究

to be continue

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值