背包问题概述


  • 01背包:每件物品只能选一个,背包体积为V,每个物品的体积和价值分别是 v i , W i v_{i},W_{i} vi,Wi
  • 完全背包:每件物品能取无限个
  • 多重背包:每个物品个数不一样,每个物品最多有xi
  • 分组背包:物品有n组,每组物品有若干个(组里的物品的属性可能不相同),每组最多只能选一个物品

对于闫氏dp法的基本流程:

先以最朴素方式考虑

对于一个dp问题分成两种情况考虑:状态表示状态计算

对于状态表示,再从两个角度考虑:

  1. 它所表示的集合是什么(这个集合的定义是什么)
  2. 这个集合所包含的属性是什么(一般为MAX,MIN,COUNT;即题目要求的)

而这个状态表示的结合,要考虑的就是两个方面:

  1. 他的定义是什么
  2. 这个集合所满足的条件是什么

例如01背包的画图表示:
在这里插入图片描述

对于状态计算来说,就可以用ven图来进行详细表示;而对于ven图表示的集合,我们对其划分的原则是:不重,不漏


01背包

dp做法两个角度:

  1. 状态表示:用 f [ i , j ] f[i, j] f[i,j]表示所有只从前i个物品中选,且总体积不超过j的所有方案的集合

    属性:求MAX

  2. 状态计算:(1)集合的划分所有不选择第i个物品的方案 + (2)所有选择第i个物品的方案

常用集合划分依据:找最后一步或者最后一个不同点,找不到就找倒数第二步

所以说,有了上述的状态计算的公式,我们就可以推出来这样一个数学公式:
f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v [ i ] ] + w [ i ] ) f[i,j]=max(f[i-1,j], f[i - 1, j -v[i]] + w[i]) f[i,j]=max(f[i1,j],f[i1,jv[i]]+w[i])

#include <iostream>

using namespace std;

int n, m;
int v[1000 + 10];
int w[1000 + 10];
int f[1000 + 10][1000 + 10];

int main(){
    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++){
            f[i][j] = f[i - 1][j]; // 同步集合(1)的情况,因为集合(2)并不是所有情况都一定存在的,就如下一行判断条件所述
            if(j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
        }
    }
    cout << f[n][m]; // 根据集合定义,从前n个物品中选,总体积不超过m的最大价值
    return 0;
}

这时候时间复杂度应该是不能优化了,但是我们应该可以优化空间复杂度

所有的 f [ i , j ] f[i,j] f[i,j]的答案更新都是由 f [ i − 1 , j ] f[i - 1, j] f[i1,j] 得到的,即只通过上一层来更新,那么我们可以把它优化一下,只用一维状态来更新。

用一维数组来保存上一层的状态,那么:
f [ j ] = m a x ( f [ j ] , f [ j − v [ i ] ] + w [ i ] ) f[j] = max(f[j], f[j - v[i]] + w[i]) f[j]=max(f[j],f[jv[i]]+w[i])
那么我们现在来看,(1)和(2)两个式子差在了哪里:

集合(1)可以从 f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j] = f[i - 1][j] f[i][j]=f[i1][j]变成 f [ j ] = f [ j ] f[j]=f[j] f[j]=f[j];集合(2)变成 f [ j ] = m a x ( f [ j ] , f [ j − v [ i ] ] ) f[j]=max(f[j], f[j - v[i]]) f[j]=max(f[j],f[jv[i]])

那么我们要注意一下了,我们希望的是通过 f [ i − 1 ] f[i-1] f[i1]来更新,如果这时候还用从0到m的体积循环的话,此时的 f [ j − v [ i ] ] f[j-v[i]] f[jv[i]]对应的是 f [ i ] [ j − v [ i ] ] f[i][j-v[i]] f[i][jv[i]]而不是 f [ i − 1 ] [ j − v [ i ] ] f[i - 1][j - v[i]] f[i1][jv[i]]。因为 j − v [ i ] j-v[i] jv[i]一定比j小,又因为我们是从小到大循环的,所以 j − v [ i ] j-v[i] jv[i]的更新要比 j j j要早,所以已经是第i个了。而如果我们是从大到小循环,我们用到了 j − v [ i ] j-v[i] jv[i]此时就是上一层还没更新过的,而得到此时的答案,因为不存在加法操作使得答案的来源是通过 j + v [ i ] j+v[i] j+v[i]来得到的,所以通过从大到小的循环也是不会影响当前层的答案

要解决这种情况我们需要从大到小枚举,这就就能保证了全部的元素都是从上一层来的而非当前的层

#include <iostream>

using namespace std;

int n, m;
int v[1000 + 10];
int w[1000 + 10];
int f[1000 + 10];

int main(){
    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--){
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }

    cout << f[m];
    return 0;
}

我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。

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

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

为什么呢?可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。

这个小技巧完全可以推广到其它类型的背包问题,后面也就不再对进行状态转移之前的初始化进行讲解。

转载至https://blog.csdn.net/u013445530/article/details/40210587


完全背包

对于每个物品可以取无限次

对于每一种物品,我们同样用01背包的方法考虑,但此时我们要加上对个数的循环考虑

即对于每一种情况 f [ i , j ] f[i,j] f[i,j]为只考虑前i个物品,且总体积不大于j的所有选法

曲线救国的方法就是:

  1. 去掉k个物品i
  2. 求MAX,f[i-1][j-k*v[i]]
  3. 再加回来k个物品i

那么方程就是 f [ i , j ] = f [ i − 1 , j − v [ i ] ∗ k ] + w [ i ] ∗ k f[i,j] = f[i-1, j-v[i]*k] + w[i] * k f[i,j]=f[i1,jv[i]k]+w[i]k

#include <iostream>

using namespace std;

const int N = 1000 + 10;

int f[N][N];
int v[N];
int w[N];
int n, m;

int main(){
    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 * v[i] <= j; k++){
                f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);
            }
        }
    }
    cout << f[n][m];
    return 0;
}

因为上式的时间复杂度较高,我们可以根据答案,来进行下面的化简,就可以得到时间优化的做法

在这里插入图片描述

#include <iostream>

using namespace std;

const int N = 1000 + 10;

int f[N][N];
int v[N];
int w[N];
int n, m;

int main(){
    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++){
            f[i][j] = f[i - 1][j];
            if(j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
            /*
            这里别与01背包的混了
            这里max后面的f是i不是i-1
            因为我们推出来的式子是根据i一定选择的是根据右边的式子加上一个w得到的
            不是根据原来的基础方程得到的
            */
        }
    }
    cout << f[n][m];
    return 0;
}

这里可以和01背包的递推式对比一下
f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v [ i ] ] + w [ i ] ) / / 01 背 包 f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i , j − v [ i ] ] + w [ i ] ) / / 完 全 背 包 f[i,j]=max(f[i-1,j], f[i - 1, j -v[i]] + w[i]) //01背包 \\\\ f[i,j]=max(f[i-1,j], f[i, j -v[i]] + w[i]) //完全背包 f[i,j]=max(f[i1,j],f[i1,jv[i]]+w[i])//01f[i,j]=max(f[i1,j],f[i,jv[i]]+w[i])//
同样的,我们也可以对这个空间进行优化

#include <iostream>

using namespace std;

const int N = 1000 + 10;

int f[N];
int v[N];
int w[N];
int n, m;

int main(){
    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++){ //这里要从v[i]开始
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    cout << f[m];
    return 0;
}

为什么这里第二层循环要从v[i]开始而不是向01背包一样从m倒着来呢?

因为我们这里的递推式是从f[i]来的,而不是像01背包从f[i-1]得到的(记得回看为什么f[i-1]是要倒着来的)

因为j < j - v[i],所以j - v[i]得到的答案是在j之前的,这样我们在遍历同一层的时候,就能够使用这一层的数据对其进行更新


多重背包

每件物品的个数有限

状态表示,状态计算

状态表示(f[i][j]);集合定义,集合属性

集合定义:从前i个物品里选,最大体积不超过j的选法的价值

属性:max

状态转移方程:
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j − v [ i ] ∗ k ] + w [ i ] ∗ k ) f[i][j] = max(f[i - 1][j - v[i] * k] + w[i] * k) f[i][j]=max(f[i1][jv[i]k]+w[i]k)
暴力写法

#include <iostream>

using namespace std;

int n, m;
int f[110][110];
int v[110];
int w[110];
int s[110];

int main(){
    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++){
                f[i][j] = max(f[i][j] , f[i - 1][j - v[i] * k] + w[i] * k);
            }
        }
    }
    cout << f[n][m];
    return 0;
}

对于这个方程,我们可以优化一下

这里采用二进制的优化方式(不是我想的,别问我为什么用二进制

我们考虑2的幂次

对于2的幂次而言,可以想象成对第 2 i 2^i 2i个物品进行打包,对于打包的每组我们只能选一个,

2的幂次有0,1,2,4,8…很多

那么我们先枚举1次,那么我们就能枚举到了0~1的每个物品,枚举到2次就可以把0~1+2,同理到0~3+4=7个物品都可以通过前面2的幂次的拼凑可以得到;根据这个性质,后面的所有的数都可以通过前面的2的幂次来拼凑出来

如果是一个一般的数

我们可以枚举到最大的小于等于它的幂次,到这时候就需要把差值补上就可以凑出来的

例如我们前面证明了整次幂的情况,我们要对200进行操作,我们可以枚举到0~127的每一种拼法,然后对里面每一个数+73就可以拼凑出来73~200内的每个数,那么连接起来就能拼凑出全部的可能性

这样时间复杂度降到了 N ∗ l o g 2 N ∗ V N*log_2N*V Nlog2NV

这就转换成了01背包的问题

#include <iostream>

using namespace std;

int n, m;
int f[2010];
int v[12010];
int w[12010]; // 1000 * log2000 左右

int main(){
    cin >> n >> m;
    int cnt = 0;
    for(int i = 1; i <= n; i++){
        int a, b, s; //体积 价值 个数   
        cin >> a >> b >> s;
        int k = 1;
        while(k <= s){
            cnt++;
            v[cnt] = k * a;
            w[cnt] = k * b;
            s -= k;
            k <<= 1;
        }
        if(s > 0){ // 把剩下的补上
            cnt++;
            v[cnt] = s * a;
            w[cnt] = s * b;
        }
    }
    
    n = cnt; // 打包之后还剩下这些
    
    for(int i = 1; i <= n; i++){
        for(int j = m; j >= v[i]; j--){
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    cout << f[m];
    return 0;
}

分组背包

每组物品有若干个,同一组物品内最多只能选一个

状态表示:从前i组物品里选,且总体积不超过j的所有选法

属性:最大值(集合当中所有选法的最大值)

状态计算:集合划分
f [ i − 1 ] [ j − v [ i , k ] ] + w [ i , k ] f[i - 1][j - v[i, k]] +w[i, k] f[i1][jv[i,k]]+w[i,k]

#include <iostream>

using namespace std;

int f[110];
int v[110][110];
int w[110][110];
int s[110];

int n, m;

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i++){
        cin >> s[i];
        for(int j = 1; 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 = 1; k <= s[i]; k++){ // 看这一组有多少个,注意每次只能选一个
                if(j >= v[i][k]) f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
            }
        }
    }
    
    cout << f[m];
    
    return 0;
}

简单总结方法:

  • 答案是从上一层转移过来的可以枚举体积从大到小
  • 答案是从这一层来的可以枚举体积从小到大
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值