惯用动态规划模型解决背包问题:

2 篇文章 0 订阅

一、动态规划介绍

动态规划(英语:Dynamic programming,简称 DP),是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。

简单说就是动态规划是把一个问题逐步划分为子问题,直到子问题能够被求解,在求解过程中往往有重复的计算,利用备忘录把答案记录下来,从而减少求解原问题的时间和空间。

二、核心思想、特征:

动态规划最核心的思想就是划分子问题,如何把一个问题逐步分解开来是尤为重要,该子问题需要有无后效性,这是最重要的一点。

1、多阶段最优子结构:当我们选择的结果是最优的,那么由它分解出来的子问题或子结构也应该是最优的。

2、重复子问题:动态规划问题往往有很多重复的子问题,我们可以记录这些子问题的解,加快解题速度

4、无后效性:指未来不再影响过去,在我们分解子问题时,分解出的子问题的逻辑不再受我们原问题的影响,对于一个dp数组,dp[n-1]的值不受dp[n]的影响,这就是无后效性

解题思路:

1、暴力求解(想象一下如何枚举)

2、记忆化搜索(考虑如何记录一些解)

3、考虑如何用dp解决问题(状态如何表示使得问题多阶段子结构最优,状态的边界如何定义,如何定义状态转移方程去描述状态的转移)

4、优化内存(滚动数组、二进制优化)

三、背包问题(九类)

(一)0-1背包问题

题目:
        有N件物品(每种仅有一个)和一w的背包。第i件物品的重量费用是c[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。 

        先剖析问题结构,我们要如何设计问题使得满足dp的特征(最优子结构、无后效性),在考虑问题的时候我们大多数都会从结果出发,逆推答案,对于一个物品,我们会考虑如果我拿了它能得到多大价值,我不拿它能得到多大价值,我们会选择其中价值更大的一个,然后再考虑该问题的子问题,考虑下一个物品(这是我们平常的思维),那么如何用语言来表达呢?

        价值受什么影响?首先是物品的数量,我们要逐个考虑每个物品是否存在于背包中的价值,其次是背包能承受的重量,如果我们的背包很大,那我们可以全都要,如果背包很小,可能某些物品都装不进去,所以我们使用一个二维数组  dp[i][j]  来表示前i件物品恰放入一个容量为j的背包可以获得的最大价值

        这个方程非常重要,基本上所有跟背包相关的问题的都由它衍生来。所以有必要将它详细解释一下:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”;如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,此时能获得的最大价值就是f [i-1][w-c[i]]再加上通过放入第i件物品获得的价值w[i]。

        那么得到状态转移方程:

​​​​​​f[i][j]= max ( f[i−1][j] , f[i−1][j−c[i]]+v[i] )

for(int i=0;i<n;i++)
    for(int j=1;j<=w;j++)
        dp[i][j]=max(dp[i-1][j],dp[i−1][j−c[i]]+v[i])

边界问题:

        dp[i][0]=0        背包承重为0,那能得到的价值也为0

        dp[0][j]=0        没有物品价值也同样为0

优化空间复杂度:

        我们可以看到第i、j格的值是只和第i-i行影响,如下图:

        也就是说我们只用记录前一行的数据即可推出下一行,我们可以用滚动数组来实现空间的优化,那么我们是否要像上面一样从前往后遍历呢,我们假设有一个一维数组dp[n],从1开始逐步更新数组,这是dp[1]就变成了第i行的数组,此时新数据覆盖了后数据,使得后续的数组无法利用已知数据,但从后往前遍历就可以避开问题,结合代码理解:

for(int i=0;i<n;i++)
    for(int j=w;j>=1;j--)
        dp[j]=max(dp[j],dp[j−c[i]]+v[i])

(二)完全背包问题(二进制优化)

相比较0-1背包,完全背包问题去掉了可取物品数量的限制:

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

当作0-1背包来做:

        有了0-1背包的思路,我们知道对于一件物品我们有取和不取两种方法,那么对于完全背包问题,同一个物品我们可以拆分成多个物品,一件物品最多取w/c[i]次,我们可以把它当成存在w/c[i]个该物品,对每一个物品求取和不取的最大价值,也是类似0-1背包的。当然我们也可以进行一个性价比的优化,对于两件物品i、j满足c[i]<=c[j]且v[i]>=v[j],去掉j物品,这能减少物品的件数,但治标不治本,有可能特别设计的数据可以一件物品也去不掉。

        这是一个简单的方法,但也有缺陷,当物品的数量、背包的容量较大时,这样的方法会很耗时,效率并不高,我们需要对0-1背包进行一个提升,跳出0-1背包。

二进制优化:

        在二进制中,我们可以用2^02^12^2、……、2^n 表示2^{n+1}-1以内的任何数,在完全背包问题中我们可以把他们拆成0-1背包是指把他们拆成一个一个的物品以表示我们能购买的物品数,同样我们可以利用二进制的特点来优化,把物品分成2^02^12^2、……、2^n 件来表示能够买的物品数,也就是说对于一个2^n件数价值为v的物品,我们把它分成 价值为 2^0v 的物品、 价值为2^1v的物品…… 当我们需要购买n个物品时(如7个)就可以转化为买对应二进制拆分的物品各一个(7的二进制=111,即购买0-1背包版本的价值为 2^0v 的物品、价值为2^1v的物品、价值为2^2v的物品)这样既把复杂的问题转化为简单问题,又不至于爆空间和爆时间。

/*
多重背包问题:二进制优化
本文中某些变量和文章不太一致,结合注释来看
*/
# include<iostream>
using namespace std;
const int MAXN = 10010; //定义最大物品数量
const int MAXV = 10010; //定义最大背包容量
int N; //物品数量
int V; //背包容量
int w[MAXN];//储存每件物品的重量w[i]
int c[MAXN];//储存每件物品的价值c[i]
int s[MAXN];//储存每件物品的数量s[i]
int dp[MAXV]; //滚动dp数组
//用滚动dp数组求解0-1背包问题
void knapsack() {
    for (int i = 0; i <= V; i++)//边界处理
        dp[i] = 0;
    for (int i = 1; i <= N; i++) {//状态更新
        //倒序枚举v (V-0)
        for (int v = V; v >= w[i]; v--)
            dp[v] = max(dp[v - w[i]] + c[i], dp[v]);
    }
}
int main(){
    int tempw[MAXN], tempc[MAXN];//储存实际物品重量和价值
    int k = 0;//把背包二进制划分后得到的物品的编码
    cin >> N >> V;//物品个数 最大背包容量W
    //读取数据
    for (int i = 1; i <= N; i++)
        cin >> tempw[i];
    for (int i = 1; i <= N; i++)
        cin >> tempc[i];

    for (int i = 1; i <= N; i++) {//利用二进制优化的方法拓展行
        cin >> s[i];
        for (int j = 1; j <= s[i]; j <<= 1) {
            k++;
            w[k] = tempw[i] * j;
            c[k] = tempc[i] * j;
            s[i] -= j;
        }
        if (s[i] != 0) {
            k++;
            w[k] = tempw[i] * s[i];
            c[k] = tempc[i] * s[i];
        }
    }
    N = k;
    knapsack();
    cout << dp[V] << endl;
}
//输入数据
/* 
4 10
3 4 2 5
2 3 2 3
2 2 1 4 
*/

//输出结果:8

(三)多重背包问题

完全背包的特殊版,完全背包不限制物品的数量,多重背包限制物品数量为某一个数:

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

        直接转化为0-1背包问题和利用二进制优化都可以求解,不再赘述

分享|股票问题系列通解(转载翻译) - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/circle/discuss/qiAgHn/该帖是力扣第一的大佬转的关于股票问题的解析,本质就是动态规划的背包问题,其中121和122就是前文的两类问题,123就是本题相关内容,作者还提供了另一种解法,其他内容也很实在有用,可以跟练。
. - 力扣(LeetCode). - 备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=N7T8https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/

(四)混合背包问题

顾名思义就是前三种背包问题的结合

(1)0-1背包和完全背包混合

这个可以考虑把0-1背包和完全背包当成一个判断来做:
伪代码如下:

for i=1..N
    if 第i件物品是01背包
        for v=W..0
        f[v]=max{f[v],f[v-c[i]]+v[i]};
    else if 第i件物品是完全背包
        for v=0..W
        f[v]=max{f[v],f[v-c[i]]+v[i]};

(2)再加上多重背包:

如果再加上有的物品最多可以取有限次,那么原则上也可以给出O(VN)的解法:遇到多重背包类型的物品用单调队列解即可。但如果不考虑超过NOIP范围的算法的话,用P03中将每个这类物品分成O(log n[i])个01背包的物品的方法也已经很优了。

for (int i = 1; i <= n; i++) {
	cin >> c >> v >> p;
	if (p == 0) //完全背包
		for (int j = c; j <= W; j++)
			f[j] = max(f[j], f[j - c] + v);
	else if (p == -1) //01背包
		for (int j = V; j >= c; j--)
			f[j] = max(f[j], f[j - c] + v);
	else { //多重背包二进制优化
		int num = min(p, W / c);
		for (int k = 1; num > 0; k <<= 1) {
			if (k > num) k = num;
			num -= k;
			for (int j = V; j >= c * k; j--)
				f[j] = max(f[j], f[j - c * k] + v * k);
		}
	}
}

(五)二维费用的背包问题

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

        思想很简单,既然cost费用维度增加,那么只要我们的维度增加即可,设f [ i ] [ j ] [ k ] f[i][j][k]f[i][j][k]表示前i ii件物品付出两种代价分别最大为j jj和k kk时可获得的最大价值,那么状态转移方程:

        f[i][j][k]=max(f[i−1][j][k],f[i−1][j−c[i]][k−g[i]]+w[i])

这也是满足最优子结构的动态规划转移方程。

for (int i = 1; i <= n; i++)
    for (int j = W1; j >= c[i]; j--)
        for (int k = W2; k >= g[i]; k--)
            f[j][k] = max(f[j][k], f[j - a[i]][k - b[i]] + v[i]);

        有时,“二维费用”的条件是以这样一种隐含的方式给出的:最多只能取M件物品。这事实上相当于每件物品多了一种“件数”的费用,每个物品的件数费用均为1,可以付出的最大件数费用为M。换句话说,设f[v][m]表示付出费用v、最多选m件时可得到的最大价值,则根据物品的类型(01、完全、多重)用不同的方法循环更新,最后在f[0..V][0..M]范围内寻找答案。另外,如果要求“恰取M件物品”,则在f[0..V][M]范围内寻找答案。

(六)分组背包问题

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

        这就从0-1问题跳出来变成了组数选择问题:是选择本组某一件,还是不选。也就是说设f[k][c]表示前k组物品花费费用c能取得的最大权值,则有f[k][v]=max{f[k-1][c],f[k-1][c-c[i]]+v[i]|物品i属于第k组}。

f[k][j]=max(f[k−1][j],f[k−1][j−c[i]]+v[i]∣ 物品i⊆组k )

for (int i = 1; i <= n; i++) {
	cin >> s; // 第i组的物品数量
	for (int j = 1; j <= s; j++) cin >> c[j] >> w[j]; //组中每个物品的属性
    for (int j = V; j >= 0; j--)
        for (int k = 1; k <= s; k++)
            if (j >= c[k])
                f[j] = max(f[j], f[j - c[k]] + w[k]);
            // 由于每组物品只能选一个,所以可以覆盖之前组内物品最优解的来取最大值
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值