【AcWing算法提高课】1.3.3背包模型(三)

一、潜水员

1020.潜水员 题目链接

与之前的背包问题要求体积恰好是不超过某一定值都不同,本题要求体积不小于某一定值,需要重新分析。

闫氏 DP 分析法:

  1. 状态表示: f [ i , j , k ] f[i,j,k] f[i,j,k]
    (1) 集合:所有只从前 i i i 个物品中选,且氧气含量至少 j j j,氮气含量至少 k k k 的选法的集合
    (2) 属性:Min
  2. 状态计算:
    (1) 所有不含物品 i i i 的选法: f [ i − 1 , j , k ] f[i-1,j,k] f[i1,j,k]
    (2) 所有包含物品 i i i 的选法: f [ i − 1 , j − v 1 , k − v 2 ] f[i-1,j-v_1,k-v_2] f[i1,jv1,kv2]
    因此有 f [ i , j , k ] = m i n { f [ i − 1 , j , k ] , f [ i − 1 , j − v 1 , k − v 2 ] + w } f[i,j,k]=min\{f[i-1,j,k],f[i-1,j-v_1,k-v_2]+w\} f[i,j,k]=min{f[i1,j,k],f[i1,jv1,kv2]+w}

注意到当 j < v 1 j<v_1 j<v1 k < v 2 k<v_2 k<v2 时,表示所有包含物品 i i i 的选法的状态中至少有一维体积是负数,无法在数组中表示。

但是,所有物品 (气缸) 中氧气和氮气含量一定是正整数,这使得当 j < v 1 j<v_1 j<v1 时, f [ i − 1 , j − v 1 , k − v 2 ] = f [ i − 1 , 0 , k − v 2 ] f[i-1,j-v_1,k-v_2]=f[i-1,0,k-v_2] f[i1,jv1,kv2]=f[i1,0,kv2] (因为只从前 i i i 个物品中选,且氧气含量不少于 j − v 1 ( ≤ − 1 ) j-v_1(\le -1) jv1(1) 且小于 0 0 0,氮气含量不少于 k k k 的选法不存在);当 k < v 2 k<v_2 k<v2 时同理。

状态初始化: f [ 0 , 0 , 0 ] = 0 , f [ 0 , i , j ] = + ∞   ( i , j ≠ 0 ) f[0,0,0]=0,f[0,i,j]=+\infty\ (i,j\ne 0) f[0,0,0]=0,f[0,i,j]=+ (i,j=0)

在这里插入图片描述
代码实现:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 22, M = 80;

int V1, V2, n;
int f[N][M];

int main(){
    scanf("%d %d %d", &V1, &V2, &n);
    
    memset(f, 0x3f, sizeof f);
    f[0][0] = 0;
    
    for (int i = 1; i <= n; i ++){
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        
        for (int j = V1; j >= 0; j --)
            for (int k = V2; k >= 0; k --)
                f[j][k] = min(f[j][k], f[max(j - a, 0)][max(k - b, 0)] + c);
    }
    
    printf("%d", f[V1][V2]);
    
    return 0;
}

至此,背包模型中关于体积的三种要求 (不超过、恰好是、不少于) 均已出现,在此总结一下三种要求的不同之处:

  1. 不超过:全部初始化为 0 0 0,枚举体积时要求 j − v ≥ 0 j-v\ge 0 jv0
  2. 恰好是: f [ 0 ] = 0 f[0]=0 f[0]=0,其余均初始化为 + / − ∞ +/-\infty +/,枚举体积时要求 j − v ≥ 0 j-v\ge 0 jv0
  3. 不少于: f [ 0 ] = 0 f[0]=0 f[0]=0,其余均初始化为 + / − ∞ +/-\infty +/,体积可以小于零。
    其中正/负无穷视题目所求为最小值还是最大值而定。

二、背包问题求具体方案

以01背包问题求具体方案为例。
12.背包问题求具体方案 题目链接

求具体方案时不能将二维空间优化成一维。

在求解后逆序比较 f [ i , j ] f[i,j] f[i,j] f [ i − 1 , j ] f[i-1,j] f[i1,j] f [ i − 1 , j − v [ i ] ] f[i-1,j-v[i]] f[i1,jv[i]],若等于前者,则不选第 i i i 个物品;若等于后者,则选第 i i i 个物品。 j j j 初始值为背包总体积,当等于后者时,下次比较时 j = j − v [ i ] j=j-v[i] j=jv[i]

此外,题目还要求输出字典序最小的答案,从前往后对于每一个物品,若可以选则一定选。可以从后往前进行求解,然后再从前往后判断是否选物品。

代码实现:

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

const int N = 1010;

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

int main(){
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= n; i ++)
        scanf("%d %d", &v[i], &w[i]);
        
    for (int i = n; i; 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 + 1][j - v[i]] + w[i]);
        }
    
    int k = m;
    for (int i = 1; i <= n; i ++)  //找具体方案时就可从前往后,保证字典序最小
        if (k >= v[i] && f[i][k] == f[i + 1][k - v[i]] + w[i])
            k -= v[i], printf("%d ", i);  //如果能选编号小的物品,就一定选
    
    return 0;
}

三、回顾分组背包问题

闫氏 DP 分析法:

  1. 状态表示: f [ i , j ] f[i,j] f[i,j]
    (1) 集合:所有只从前 i i i 组物品中选,且总体积不超过 j j j 的选法的集合
    (2) 属性:Max
  2. 状态计算:
    (1) 第 i i i 组物品中一个都不选: f [ i − 1 , j ] f[i-1,j] f[i1,j]
    (2) 第 i i i 组物品中选第 1 1 1 个: f [ i − 1 , j − v [ i , 1 ] ] + w [ i , 1 ] f[i-1,j-v[i,1]]+w[i,1] f[i1,jv[i,1]]+w[i,1]
    (3) 第 i i i 组物品中选第 2 2 2 个: f [ i − 1 , j − v [ i , 2 ] ] + w [ i , 2 ] f[i-1,j-v[i,2]]+w[i,2] f[i1,jv[i,2]]+w[i,2]

    (s[i]+1) 第 i i i 组物品中选第 s [ i ] s[i] s[i] 个: f [ i − 1 , j − v [ i , s [ i ] ] ] + w [ i , s [ i ] ] f[i-1,j-v[i,s[i]]]+w[i,s[i]] f[i1,jv[i,s[i]]]+w[i,s[i]]
    上述取最大值即是 f [ i , j ] f[i,j] f[i,j]

可以发现,相比于01背包、完全背包问题等两重循环,分组背包问题还需要额外一重循环来枚举组内的决策。

多重背包问题可以看作一种特殊的分组背包问题。

四、机器分配

1013.机器分配 题目链接

可以把每个分公司看作一个物品组,每个物品组内均有 M M M 个物品,分别是 1 1 1 台机器、 2 2 2 台机器、…、 M M M台机器,且每一个物品组中至多选一个物品,背包体积是机器数量,价值即盈利,这样题目就转化为了分组背包问题。

同时本题还需输出任意一种方案,按照上面背包问题求具体方案,倒叙求每个物品组选第几个物品即可。

代码实现:

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

const int N = 12, M = 17;
int n, m;
int f[N][M];
int w[N][M];
int path[N];

int main(){
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= n; i ++)
        for (int j = 1; j <= m; j ++)
            scanf("%d", &w[i][j]);
    
    for (int i = 1; i <= n; i ++)
        for (int j = 0; j <= m; j ++)
            for (int k = 0; k <= j; k ++)
                f[i][j] = max(f[i][j], f[i - 1][j - k] + w[i][k]);
    printf("%d\n", f[n][m]);
    
    int k = m;
    for (int i = n; i; i --)
        for (int j = 0; j <= m; j ++)
            if (k >= j && f[i][k] == f[i - 1][k - j] + w[i][j]){
                path[i] = j;
                k -= j;
                break;
            }
        
    for (int i = 1; i <= n; i ++)
        printf("%d %d\n", i, path[i]);
        
    return 0;
}

五、金明的预算方案

487.金明的预算方案 题目链接

可以将每个主件看作一个物品组,若第 i i i 个主件有 s [ i ] s[i] s[i] 个附件,则该物品组下有 2 s [ i ] 2^{s[i]} 2s[i] 种选择,用二进制数中的位代表相应附件的选择与否 (决策),这样就转化为分组背包问题。

代码实现:

#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;

#define v first
#define w second
const int N = 70, M = 32010;
typedef pair <int, int> PII;

int n, m;
PII a[N];
vector <PII> b[N];
int f[M];

int main(){
    scanf("%d %d", &m, &n);
    for (int i = 1; i <= n; i ++){
        int v, p, q;
        scanf("%d %d %d", &v, &p, &q);
        if (!q) a[i] = {v, v * p};
        else b[q].push_back({v, v * p});
    }
    
    for (int i = 1; i <= n; i ++)
        if (a[i].v){  //数组a[i]有可能没有物品,要特判
            for (int j = m; j >= 0; j --)
                for (int k = 0; k < 1 << b[i].size(); k ++){  //二进制数枚举集合选择
                    int v = a[i].v, w = a[i].w;
                    for (int u = 0; u < b[i].size(); u ++)  //判断二进制数中各位对应物品是否被选
                        if (k >> u & 1){
                            v += b[i][u].v;
                            w += b[i][u].w;
                        }
                    if (j >= v) f[j] = max(f[j], f[j - v] + w);
                    }
        }
    
    printf("%d", f[m]);
    
    return 0;
}

六、开心的金明

426.开心的金明 题目链接

金明的预算方案一题的弱化版本,只有主件,无附件,简单的01背包问题。

代码实现:

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

const int N = 50010;

int n, m;
int f[N];

int main(){
    scanf("%d %d", &m, &n);
    for (int i = 1; i <= n; i ++){
        int v, p;
        scanf("%d %d", &v, &p);
        for (int j = m; j >= v; j --)
            f[j] = max(f[j], f[j - v] + v * p);
    }
    printf("%d", f[m]);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值