多维多重背包问题_背包问题总结( 1 ) 01 背包,完全背包,多重背包,分组背包...

本文详细梳理了背包问题的动态规划解决方案,包括01背包、完全背包、多重背包和分组背包问题。通过状态表示和状态转移方程,介绍了二维动态规划和一维动态规划(状态压缩)等优化技巧,旨在帮助读者深入理解动态规划在背包问题中的应用。
摘要由CSDN通过智能技术生成

这篇文章将背包问题和动规做一个整理复习。

背包问题的本质上是一个选择问题,即通过选择来得到最大价值或各种各样的性质。

动态规划基础知识

DP 问题我们一般的思路是先确定解法的基本形式,再在它的基础上做优化。

首先我们对DP问题的分析流程做一下定义, 对于DP问题我们要确定的要素主要为

(1) 状态表示 :dp 元素表示的是哪个集合

(以01背包问题为例: (i, j) 代表了只考虑在 1~i 范围内选择物品,并且体积小于等于 j 的所有状态的集合)

存的数是表示集合的哪一个属性(目标)

(以01背包问题为例:dp(i, j) 代表了(i, j)代表的所有状态集合中最大价值)

动态规划一般包含(MIN, MAX, COUNT)三种目标。

(2) 状态计算(状态转移方程):对应的是集合的划分,如何将当前的集合dp( i,j )划分成几个更小的子集。

(1) 01背包问题

(1.1) Solution 1 : 二维动态规划

了解了这些以后我们就可以基于这个思路对01背包问题做一些分析:

(1) 当

时:

(2) 当

时:

#include <iostream>
#include <vector>
#define N 1100
using namespace std;


int main(void){
    int n, m;
    int v[N], w[N]; 
    int dp[N][N];
    cin >> n >> m;
    for(int i = 1; i <= n; i++)  cin >> v[i] >> w[i];
    for(int i = 1; i <= n; i++){
        for(int j = 0; j <= m; j++){
            if(j-v[i] >= 0)      dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]]+w[i]);
            else                 dp[i][j] = dp[i-1][j];
        }
    }
    cout << dp[n][m];
    return 0;
}

(1.2) Solution 2: 状态压缩 : 一维动态规划

dp问题的所有优化都是在代码上做等价变形 , 和问题本身无关,只和代码逻辑有关。 01背包在时间复杂度上没法再优化, 从空间上我们其实还能做一些优化。

  1. 首先其实可以发现整个转移方程中对于 i 这一维,只用到了i -1, 所以我们其实并不需要记录所有的dp[i][..],相反只需要用单个变量记录即可(滚动数组)。这样我们可以得出
  2. 在去除dp 数组的 i 这一维后,我们碰到了一些问题:对于 j 这一维因为 j - v[i] < j , 所以实际上 dp[j-v[i]] 这个状态已经被计算过了,这代表了什么呢,每次循环开始时 dp[j] 记录的是dp[i-1][j]的信息,而循环结束更新后 dp[j] 记录的则为dp[i][j]的信息,如果我们从前往后循环,那么我们每次更新时dp[j] = dp[j - v[i]]+w[i] 相当于原先二维动态规划时的 dp[i][j] = dp[i][j-v[i]]+w[i], 然而实际的目标 为 dp[i][j] = dp[i-1][j-v[i]]+w[i], 那么怎么去修正这一点呢? 我们从大到小循环, 这样则保证了每一次的 i 都是由 i - 1 推出来的 (dp[j-v[i]]在本轮尚未更新过,所以依旧记录的是dp[i-1][j-v[i]]的数值)。
#include <iostream>
#include <vector>
using namespace std;
const int N = 1100;

int main(void){
    int n, m;
    vector<int> v(N,0), w(N,0);
    vector<int> dp(N,0);
    cin >> n >> m;
    for(int i = 1; i <= n; i++)    cin >> v[i] >> w[i];
    for(int i = 1; i <= n; i++){
        for(int j = m; j >= v[i]; j--){
            dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
        }
    }
    cout << dp[m] << endl;
    return 0;
}

(2) 完全背包问题

问题描述:
有 N 种物品和一个容量是 V 的背包, 每种物品都有无限件可用
第 i 种物品的体积是 v[i],价值是 w[i]。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

和01背包问题不同的是这里我们每种物品都可以使用无数次, 而01背包问题则将每个物品的使用限定在了一次。完全背包问题的状态表示可以与01背包问题完全一致, 即 [i][j]代表在1~i的范围内考虑物品, 体积为 j 的所有状态 集合。

(2.1) Solution 1 :

暴力解法
#include <iostream>
#include <cstdio>
#define N 1100
using namespace std;

int main(void){
    int n, m;
    int v[N], w[N];
    int dp[N][N];
    
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    for(int i = 1; i <= n; i++){
        for(int j = 0; j <= m; j++){
            for(int k = 0; k <= j/v[i]; k++)
                dp[i][j] = max( dp[i-1][j], dp[i-1][j - k*v[i]] + k*w[i] );
        }
    }
    cout << dp[n][m] << endl;
    return 0;
}

(2.2) Solution 2 : 状态压缩

优化思路:

状态转移方程的核心部分为:

将k展开后:

对于

不难发现, 因为max算子在范围内可以加减 :

,所以:

这样我们就化简掉 k 的使用,发现状态转移方程与01背包问题非常相似除了第二项 i 与 i -1的区别,然后用与01背包问题相似的方法我们可以将dp数组再次缩减为一维的。(注: 由于这里需要计算的是dp[i][j-v[i]] 根据我们前一节得出的结论,这里只需要从前往后更新便能符合要求。)

#include <iostream>
#include <cstdio>
using namespace std;

#define N 2010

int main(void){
    int n, m;
    int v[N], w[N];
    int dp[N];

    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    for(int i = 1; i <= n; i++){
        for(int j = v[i]; j <= m; j++){
            dp[j] = max( dp[j], dp[j - v[i]] + w[i]);
        }
    }
    cout << dp[m] << endl;
    return 0;
}

(3) 多重背包问题

有 N 种物品和一个容量是 V 的背包。

第 i 种物品最多有

件,每件体积是
,价值是

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

( 3.1 ) Solution 1 : 二维动态规划暴力解法

#include <iostream>
#include <cstdio>
#define N 110
using namespace std;

int main(void){
    int n, m;
    int v[N], w[N], s[N];
    int dp[N][N];
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        cin>> v[i] >> w[i] >> s[i];
    for(int i = 1; i <= n; i++)
        for(int j = 0; j <= m; j++)
            for(int k = 0; k <= s[i] && k*v[i] <= j; k++)
                dp[i][j] = max(dp[i][j], dp[i-1][j-k*v[i]]+k*w[i]);
    cout << dp[n][m] << endl;
    return 0;
}

( 3.2 ) Solution 2 : 状态压缩,二进制优化

多重背包问题没办法用完全背包问题的思路优化。

这里引出一种新的思路: 将一个多重背包问题化为01背包问题, 即如果一个物品有s个,那么实际上我们可以将其拆为s份,每一份为一个新的物品,物品的重量与价值保持不变,对于每件物品只有装入和不装入两种选择,这样再重新组织后,一个多重背包问题就被转化为01背包问题。

然而直接拆分成s份的复杂度实际上是很高的:即 S x N。

下一步就引出了我们要使用的方法,用二进制的方法来拆分物品。

我们的目标实际上是想要用最少的数,组合出

以内的所有数( 因为对于物品 i 我们可以选择装入 0~
个),拆分的下界是将
个物品i , 拆分成
个物品。
这里以7 为例, 从0 到 7 一共有8个数, 每个数选和不选有两种选择, 也就是说如果我么有3个状态即可以表示8个数 ,那么这个问题的优化下界应当等于
向上取整。

这代表了什么呢, 我们可以用二进制表示来压缩拆分状态, 用3个数组合出0~7的任意一个数:

这样我们将原先个数为

,总重量为
,总价值为
的物品i,分解成了3个物品,并且它们可以以01(选或不选)的放法,组合成原先的物品i的任意一种状态。这样我们在对价值取max时实际上是等价于原物品i 的。

然而这里还有一个问题是对于非二的整次幂的数该怎么办, 以数字10 为例子, 我们不能将10 分解成 1,2,4 或1,2,4,8 (包含了所有15以内数,超过我们的物品数量范围) 。

,同时
为小于s二次幂的最大, 即组合项的前k个数为
我们的最后一个数即为

因为前k个数能表示的状态为0 ~ 2^k, 加上最后一个数后能表示的状态即为

这样我们的优化步骤就完成了,剩下的我们已经将多重背包问题转化为01背包问题所以用01背包问题的解法即可。

代码如下:

#include <iostream>
#include <cstdio>
#include <algorithm>

#define N 20100
using namespace std;

int main(void){
    int n, m, v_t, w_t, s_t;
    vector<int> v, w;
    vector<int> dp(N);
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++){
        scanf("%d%d%d", &v_t, &w_t, &s_t);
        for(int t = 1; s_t >= t; t *= 2){
            v.push_back(t*v_t);
            w.push_back(t*w_t);
            s_t -= t;
        }
        if(s_t > 0){
            v.push_back(s_t*v_t);
            w.push_back(s_t*w_t);
        }
    }
    for(int i = 0; i < v.size(); i++)
        for(int j = m; j >= v[i]; j--)
            dp[j] = max(dp[j], dp[j - v[i]]+w[i]);
    cout << dp[m] << endl;
    return 0;
}

(4)分组背包问题

有 N 组物品和一个容量是 V 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是

,价值是
,其中 i 是组号,j 是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。


分组背包问题实际上与上面的01背包十分相似, 区别在于分组背包DP状态数组第一维表示的是, 只考虑第 1 ~ i 组。

Solution :

#include <iostream>
#include <algorithm>
#define N 110
using namespace std;

int main(void){
    int n, m;
    int v[N][N], w[N][N], s[N];
    int dp[N];
    cin >> n >> m;
    for(int i = 1; i <= n; i++){
        cin >> s[i] ;
        for(int j = 0; j < s[i]; j++)
            cin >> v[i][j] >> w[i][j];
    }
    
    for(int i = 1; i <= n; i++)
        for(int j = m; j >= 0; j--)
            for(int k = 0; k < s[i]; k++)
                if(j-v[i][k] >= 0)
                    dp[j] = max( dp[j], dp[j-v[i][k]] + w[i][k]);
    cout << dp[m] << endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值