Luogu T141708题解

题目链接

T141708 果园一日游

前置知识

背包问题:

(配合这篇博客食用效果更佳)

现有 \(n\) 种物品,第 \(i\) 种物品的重量为 \(w_i\),价值为 \(v_i\),数量为 \(num_i\)
有一个容量为 \(g\) 的背包,求背包能装下物品的最大价值和。

背包问题大致上可以分为三类:01背包,完全背包,多重背包。

一般都使用DP(动态规划)进行求解。

01背包

现有 \(n\) 种物品,第 \(i\) 种物品的重量为 \(w_i\),价值为 \(v_i\)每种物品只有 \(1\) 件(即要么装一个,要么不装)
有一个容量为 \(g\) 的背包,求背包能装下物品的最大价值和。

我们可以使用一个二维数组 \(f(i,c)\) 来表示前 \(i\) 种物品放入容量为 \(c\) 的背包所能产生的最大价值。容易得到:

\[f(i,c) = \max\{f(i - 1,c),f(i - 1,c - w_i) + v_i\} \]

(即 \(i\) 种物品放入容量为 \(c\) 的背包的最大价值 等于 \(i - 1\) 种物品放入容量为 \(c\) 的背包的最大价值(即不放第 \(i\) 种物品) 和 \(i - 1\) 种物品放入容量为 \(c - w_i\) 的背包的最大价值加第 \(i\) 种物品的价值 的最大值)

(这里可能需要理解一下)

所以,我们可以写出如下的代码:

for (int i = 1;i <= n;i++){//对n件物品进行循环
    for (int c = w[i];c <= g;c++){
        f[i][c] = max(f[i - 1][c],f[i - 1][c - w[i]] + v[i]);//这就是上面的那行方程
    }
}

最后,\(f(n,g)\) 即为所求的结果(下略)。

一个小优化:

我们注意到,方程中左边 \(f\) 第一维是 \(i\),右边两个 \(f\) 第一维都是 \(i - 1\),相当于是我们用第 \(i - 1\) 维去更新第 \(i\) 维。但事实上,我们并不关心第 \(i - 1\) 维,所以,可不可以把第一维去掉,直接覆盖更新呢?答案是可以的,但在这里,我们需要微调第二维的循环顺序:

for (int i = 1;i <= n;i++){//对n件物品进行循环
    for (int c = g;c >= w[i];c++){//注意顺序
        f[c] = max(f[c],f[c - w[i]] + v[i]);//优化后的方程
    }
}

(为什么调整顺序这里简单说两句:因为我们原来的方程右边用的是第 \(i - 1\) 维的数据,所以我们在代码中更新使用的 \(f_{c - w_i}\) 也应该是第 \(i - 1\) 维的数据。这时,如果还使用原来的顺序,则会使我们更新所用的 \(f_{c - w_i}\) 在此时已经被更新过,成了第 \(i\) 维的数据,所以要调整顺序)

(这一段看不懂也没关系)

完全背包

现有 \(n\) 种物品,第 \(i\) 种物品的重量为 \(w_i\),价值为 \(v_i\)每种物品有无穷件
有一个容量为 \(g\) 的背包,求背包能装下物品的最大价值和。

同样,我们可以考虑用一个二维数组 \(f(i,c)\) 来表示前 \(i\) 种物品放入容量为 \(c\) 的背包所能产生的最大价值。容易得到:

\[f(i,c) = \max\{f(i - 1,c),f(i,c - w_i) + v_i\} \]

(注意 \(max\) 中第二个 \(f\) 的第一维变成了 \(i\)

这里请同学们照着上面01背包方程下的注释,自己分析一下。

代码:

for (int i = 1;i <= n;i++){
    for (int c = w[i];c <= g;c++){
        f[i][c] = max(f[i - 1][c],f[i][c - w[i]] + v[i]);
    }
}

同样,我们也可以使用上面的小优化。但这里由于第二个 \(f\) 的第一维为 \(i\),用的是更新后的数据,所以顺序不用改变:

for (int i = 1;i <= n;i++){
    for (int c = w[i];c <= g;c--){//注意顺序
        f[c] = max(f[c],f[c - w[i]] + v[i]);
    }
}

可以发现,这一段代码和01背包的代码只有顺序不同。

多重背包

现有 \(n\) 种物品,第 \(i\) 种物品的重量为 \(w_i\),价值为 \(v_i\)共有 \(num_i\)
有一个容量为 \(g\) 的背包,求背包能装下物品的最大价值和。

我们可以考虑把多重背包转化为01背包进行求解。

一个比较朴素的想法就是把 \(num_i\) 件物品拆成 \(i\) 件,对每一件使用01背包。但这样的时间复杂度为 \(O(v \sum_{i = 1}^n num_i)\),一般而言无法承受。

所以,我们需要寻找一种更好的拆解方案,并且还要使拆解后的物品正好能够凑成取 \(0,1 \ldots num_i\) 件物品的情况。二进制的思想是一个不错的选择。

考虑将 \(num_i\) 件物品拆分,使得每个物品都有一个系数 \(k\)

(其实系数 \(k\) 就相当于是把 \(k\) 个物品当成一个物品,重量为 \(w_i * k\),价值为 \(v_i * k\)

我们不妨令拆分后的物品系数分别是(也就是分别等价于原来的多少件物品):

\[1,2,2^2,2^3 \ldots 2^{n-1},num_i - 2^n + 1 \]

其中 \(n\) 是满足 \(num_i - 2^n + 1 > 0\) 的最大正整数。

这样的话,我们就把原来的 \(num_i\) 件物品拆成了 \(O(\log \ num_i)\) 件物品,时间复杂度变为 \(O(v \sum_{i = 1}^n \log \ num_i)\),一般可以接受。

并且我们可以验证,这 \(O(\log \ num_i)\) 件物品一定能够凑成取 \(0,1 \ldots num_i\) 件物品的情况。对于拆分后的每一件物品,使用01背包求解即可。

代码如下:

for (int i = 1;i <= n;i++){
    for (int k = 1;k <= num[i];k <<= 1){//这里相当于是拆分的过程。k <<= 1相当于k *= 2
        for (int j = k * w[i];j <= g;j++){//别忘了乘上系数k
            f[i][j] = max(f[i - 1][j],f[i - 1][j - k * w[i]] + k * v[i]);//01背包
        }
        num[i] -= k;
    }
    if (num[i] != 0){//单独处理系数为num[i] - 2^k + 1的情况
        for (int j = num[i] * w[i];j <= g;j++){
            f[i][j] = max(f[i - 1][j],f[i - 1][j - num[i] * w[i]] + num[i] * v[i]);//此时系数就是num[i]
        }
    }
}

同样可以使用优化,代码如下:

for (int i = 1;i <= n;i++){
    for (int k = 1;k <= num[i];k <<= 1){
        for (int j = g;j >= k * w[i];j--){
            f[j] = max(f[j],f[j - k * w[i]] + k * v[i]);
        }
    }
    if (num[i] != 0){
        for (int j = g;j >= num[i] * w[i];j--){
            f[j] = max(f[j],f[j - num[i] * w[i]] + num[i] * v[i]);
        }
    }
}

二维费用

我们在处理背包问题时,可以把重量当成是一种“费用”。那么,如果费用不只有一种,应该怎么办呢?(这里以二维费用的01背包为例,其他背包类似)

现有 \(n\) 种物品,第 \(i\) 种物品的重量为 \(w_i\),体积为 \(h_i\),价值为 \(v_i\)。每种物品只有 \(1\) 件(即要么装一个,要么不装)。
有一个最大载重为 \(g\),最大体积为 \(p\) 的背包,求背包能装下物品的最大价值和。

我们只需要使用一个三维数组 \(f(i,c,d)\) 来表示前 \(i\) 种物品放入最大载重为 \(c\) ,最大体积为 \(d\) 的背包所能产生的最大价值。类似地,我们有:

\[f(i,c,d) = \max\{f(i - 1,c,d),f(i - 1,c - w_i,d - h_i) + v_i\} \]

代码如下:

for (int i = 1;i <= n;i++){
    for (int c = w[i];c <= g;c++){
    	for (int d = h[i];d <= p;d++){
    	    f[i][c][d] = max(f[i - 1][c][d],f[i - 1][c - w[i]][d - h[i]] + v[i]);//这就是上面的那行方程
        }
    }
}

使用优化如下:

for (int i = 1;i <= n;i++){
    for (int c = g;c >= w[i];c--){
    	for (int d = p;d >= h[i];d--){
    	    f[c][d] = max(f[c][d],f[c - w[i]][d - h[i]] + v[i]);
        }
    }
}

审题

有了上面的前置知识,我们可以发现,这道题目实际上就是一个有 \(n\) 种物品的三种背包混合的二维费用问题。其中第 \(i\) 种物品有 \(num_i\) 种,花费 \(t_i \times e\) 元钱和 \(p_i\) 点体力,价值为 \(h_i\)。我们一共有 \(m\) 元钱,\(s\) 点体力。

我们对每一件物品进行判断:

  • \(num_i = -1\)\(t_i \times e \times num_i > m\)\(p_i \times num_i > s\)时(后两种情况即该种物品的总费用大于最大值的情况),当作完全背包处理

  • 否则,当作多重背包处理(01背包可以看作是特殊的多重背包)

最后,输出 \(f(m,s)\) 即可。

至于题目中说的取模操作,由余数的可加性、可乘性等一系列性质,我们在每次操作的时候都取模,结果和最后取模是一致的,同时还能防止数据溢出。

(视频讲解这里有口误,这里用的是两数取模后的最大值和两数最大值取模的结果是相等的这个性质,视频中讲了一下可加性和可乘性是因为这两个性质比较常用,虽然这里没有用到)

(其实Subtask1就是没有体力费用,即一维费用的情况。Subtask2就是除了01背包就是完全背包的情况)

代码

(这是简化版本,需要开启O2,真正的std在最后):

#include <bits/stdc++.h>
using namespace std;

int n,m,s,e,num[101],t[101],p[101],h[101],f[1001][1001];

int main(){
	scanf("%d\n%d %d %d",&n,&m,&s,&e);
	for (int i = 1;i <= n;i++) scanf("%d %d %d %d",&num[i],&t[i],&p[i],&h[i]),t[i] *= e;//读入+预处理 
    for (int i1 = 1;i1 <= n;i1++){//循环n种物品 
        if (num[i1] == -1 || t[i1] * num[i1] > m || p[i1] * num[i1] > s){//判断是否使用完全背包 
            for (int i = t[i1];i <= m;i++){
                for (int j = p[i1];j <= s;j++) f[i][j] = max(f[i][j],(f[i - t[i1]][j - p[i1]] + h[i1]) % 1000000007);
            }//完全背包 
        }else{
            for (int k = 1;k <= num[i1];k *= 2){
                for (int i = m;i >= t[i1] * k;i--){
                    for (int j = s;j >= p[i1] * k;j--) f[i][j] = max(f[i][j],(f[i - t[i1] * k][j - p[i1] * k] + h[i1] * k) % 1000000007);
                }
                num[i1] -= k;
            }
            if (num[i1] != 0){
                for (int i = m;i >= t[i1] * num[i1];i--){
                    for (int j = s;j >= p[i1] * num[i1];j--) f[i][j] = max(f[i][j],(f[i - t[i1] * num[i1]][j - p[i1] * num[i1]] + h[i1] * num[i1]) % 1000000007);
                }
            }
        }//多重背包 
    }
    printf("%d",f[m][s]);
}

STD:

#define MOD 1000000007

#include <cstdio>
#include <stdlib.h>

using namespace std;

inline int read(){
    int t = 0,flag = 1;
    register char c = getchar();
    while (c < 48 || c > 57) {if (c == '-') flag = -1;c = getchar();}
    while (c >= 48 && c <= 57) t = (t << 1) + (t << 3) + (c ^ 48),c = getchar();
    return t * flag;
}

inline int max(int a,int b){return a > b ? a : b;}

int n,m,s,e,num,t,p,h,f[1001][1001];

int main(){
    n = read(),m = read(),s = read(),e = read();
    while (n--){
        num = read(),t = read() * e,p = read(),h = read();
        if (num == -1 || t * num > m || p * num > s){
            for (register int i = t;i <= m;i++){
                for (register int j = p;j <= s;j++) f[i][j] = max(f[i][j],(f[i - t][j - p] + h) % MOD);
            }
        }else{
            for (register int k = 1;k <= num;k <<= 1){
                for (register int i = m;i >= t * k;i--){
                    for (register int j = s;j >= p * k;j--) f[i][j] = max(f[i][j],(f[i - t * k][j - p * k] + h * k) % MOD);
                }
                num -= k;
            }
            if (num){
                for (register int i = m;i >= t * num;i--){
                    for (register int j = s;j >= p * num;j--) f[i][j] = max(f[i][j],(f[i - t * num][j - p * num] + h * num) % MOD);
                }
            }
        }
    }
    printf("%d",f[m][s]);
}

完结撒花✿✿ヽ(°▽°)ノ✿!

posted @ 2020-08-19 12:51  PYD1  阅读(101)  评论(0)    收藏  举报