第一章 动态规划 背包问题之01背包问题

背包问题题谱

在这里插入图片描述

1.基础01背包问题

1. 问题描述

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8

要点

  1. 每件物品只能使用一次
  2. 总体积不超过V
  3. 总价值最大

2. 分析

在这里插入图片描述

按照集合划分

  1. 最后一个不选i
    代表要从1到i - 1中选择物品,并且其体积不超过j,这其实就是f[i - 1,j]
  2. 最后一个选i
    如果要选i的话,我们可以这么考虑,我们先把i的这个空给空出来,那么剩下的内容就变成从1到i - 1的物品中选择物品,并且其体积不超过 j - vi(这样就是把这个i的空空出来),其总价值的最大值为f[i - 1, j - vi],再把i的价值加上,就得到了最后一个选i这一类的最大值f[i - 1,j - vi] + w[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 - vi]) + w[i] f[i,j]=max(f[i1,j],f[i1,jvi])+w[i]

3. 代码

#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
//v代表各种物品的体积,w代表各个物品的价值
int f[N][N],v[N],w[N];
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i = 1; i <= n; i ++)
    {
        scanf("%d%d",&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];
            //在j够减的情况下,也就是j能装得下i的情况下
            if(j >= v[i]) f[i][j] = max(f[i][j],f[i - 1][j - v[i]] + w[i]);
        }
    }
    printf("%d",f[n][m]);
}

在上面的代码里我们可以看到

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]);

这里,推导f[i][j]的时候,我们只用到了f[i - 1]这一层的信息,所以我们可以使用滚动数组将需要用到的信息存到一维数组中。所谓滚动数组,其实也就是我计算第i层的数据时,使用滚动数组的内容(就是i - 1层的数据),当我计算完之后,我的数据就存进滚动数组中,这样计算i + 1层的时候,就可以继续使用第i层的数据。
代码做了改动如下

#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 1010;
//f使用一维数组存储
int f[N],v[N],w[N];
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i = 1; i <= n; i ++)
    {
        scanf("%d%d",&v[i],&w[i]);
    }
    for(int i = 1; i <= n; i ++)
    {
    	//f[j] = max(f[j],f[j - v[i]] + w[i]);
    	//其原本的代码是f[i][j] = max(f[i - 1][j],f[i - 1][j - v[i]] + w[i])
    	//可以看到,f[j]需要的是i - 1层的f[j - v[i]进行更新
    	//如果从小到大进行遍历,由于j - v[i] < j,所以会先于j进行更新,更新后f中存储的f[j - v[i]]就是第i层的了,所以从大到小更新。更新j的时候,使用的还是i - 1层的数据。
        for(int j = m; j >= v[i]; j --)
        {
        	//去掉了f[i][j] = f[i - 1][j];
        	//因为现在f中默认存的是“上一层”的数据

            //原来是
            //f[i][j] = max(f[i - 1][j],f[i - 1][j - v[i]] + w[i]),也是由于f中存的就是上一层(i - 1)的数据,所以改成了一维
            f[j] = max(f[j],f[j - v[i]] + w[i]);
        }
    }
    printf("%d",f[m]);
}

2、01背包问题求方案数

1. 题目数字组合

给定 N 个正整数 A1,A2,…,AN,从中选出若干个数,使它们的和为 M,求有多少种选择方案。

输入格式
第一行包含两个整数 N 和 M。
第二行包含 N 个整数,表示 A1,A2,…,AN。

输出格式
包含一个整数,表示可选方案数。

数据范围
1≤N≤100,
1≤M≤10000,
1≤Ai≤1000,
答案保证在 int 范围内。

输入样例:
4 4
1 1 2 2
输出样例:
3

2. 分析

这里我们把M视为背包容量,将每个数视为一个物品,将每个ai看成体积。那么题目的目的就是求总体积恰好是M的方案数。
在这里插入图片描述

按照这种划分我们来进行计算。
不含i物品,按照定义可以得到其表示为f[i - 1,j],也就是从前i - 1个物品中选,体积恰好为j的集合
含i物品,按照定义可以得到表示为f[i - 1,j - ai],也就是从前i - 1个物品中选,体积恰好为 j - ai的集合
又因为属性为数量,所以f[i][j]里存的是集合元素的数量。
所以 f [ i , j ] = f [ i − 1 ] [ j ] + f [ i − 1 ] [ j − a i ] f[i,j] = f[i - 1][j] + f[i -1][j - ai] f[i,j]=f[i1][j]+f[i1][jai]
初始化
f[0][i]表示一个都不选,体积恰好为i的所有选法的集合,除了i = 0的情况,其他情况均为空集(注意,我们的集合是选法的集合,i=0时f[0][0]代表一个都不选,且体积恰好为0的所有选法的集合,那么有一个选法就是什么都不选,所以集合不是空集)。由于属性为集合元素的数量,所以初始化f[0][0]=1.

3. 代码

下面代码还是使用的降维的方式写的。

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110;
int a[N],f[10010];
int main()
{
    int n,m;
    cin >> n >> m;
    for(int i = 1; i <= n; i ++){
         cin >> a[i];
         
    }
    f[0] = 1;
    for(int i = 1; i <= n; i ++)
    {
        for(int j = m; j >= a[i]; j --)
        {
            f[j] += f[j - a[i]];
        }
    }
    cout << f[m];
}

3、 二维费用的01背包问题(属性为max)

1. 题目宠物小精灵之收服

宠物小精灵是一部讲述小智和他的搭档皮卡丘一起冒险的故事。一天,小智和皮卡丘来到了小精灵狩猎场,里面有很多珍
贵的野生宠物小精灵。小智也想收服其中的一些小精灵。然而,野生的小精灵并不那么容易被收服。对于每一个野生小精
灵而言,小智可能需要使用很多个精灵球才能收服它,而在收服过程中,野生小精灵也会对皮卡丘造成一定的伤害(从而
减少皮卡丘的体力)。当皮卡丘的体力小于等于0时,小智就必须结束狩猎(因为他需要给皮卡丘疗伤),而使得皮卡丘体
力小于等于0的野生小精灵也不会被小智收服。当小智的精灵球用完时,狩猎也宣告结束。我们假设小智遇到野生小精灵时
有两个选择:收服它,或者离开它。如果小智选择了收服,那么一定会扔出能够收服该小精灵的精灵球,而皮卡丘也一定
会受到相应的伤害;如果选择离开它,那么小智不会损失精灵球,皮卡丘也不会损失体力。小智的目标有两个:主要目标
是收服尽可能多的野生小精灵;如果可以收服的小精灵数量一样,小智希望皮卡丘受到的伤害越小(剩余体力越大),因
为他们还要继续冒险。现在已知小智的精灵球数量和皮卡丘的初始体力,已知每一个小精灵需要的用于收服的精灵球数目
和它在被收服过程中会对皮卡丘造成的伤害数目。请问,小智该如何选择收服哪些小精灵以达到他的目标呢?

输入格式
输入数据的第一行包含三个整数:N,M,K,分别代表小智的精灵球数量、皮卡丘初始的体力值、野生小精灵的数量。
之后的K行,每一行代表一个野生小精灵,包括两个整数:收服该小精灵需要的精灵球的数量,以及收服过程中对皮卡丘造成的伤害。

输出格式
输出为一行,包含两个整数:C,R,分别表示最多收服C个小精灵,以及收服C个小精灵时皮卡丘的剩余体力值最多为R。

数据范围
0<N≤1000,
0<M≤500,
0<K≤100
输入样例1:
10 100 5
7 10
2 40
2 50
1 20
4 20
输出样例1:
3 30
输入样例2:
10 100 5
8 110
12 10
20 10
5 200
1 110
输出样例2:
0 100

01背包问题最重要的是找到体积和数量,这里有两个"体积"花费,第一个是精灵球的数量,第二个是皮卡丘的体力消耗值。价值就是小精灵的数量。

2.分析

在这里插入图片描述
最后一个不选a[i]可以表示为f[i - 1,j,k]
最后一个选a[i]可以表示为f[i - 1,j - v1,k - v2] + 1
据此我们可以得出状态计算表达为
f [ i , j , k ] = m a x ( f [ i − 1 , j , k ] , f [ i − 1 , j − v 1 , k − v 2 ] + 1 f[i,j,k]=max(f[i - 1,j,k],f[i - 1,j - v1,k - v2] + 1 f[i,j,k]=max(f[i1,j,k],f[i1,jv1,kv2]+1
与一维01背包问题一样,可以省略i的那一维

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int v[N],x[N];
//第一维代表精灵球个数,第二维代表消耗的体力值
int f[N][N];
int main()
{
    int n,m,k;
    cin >> n >> m >> k;
    for(int i = 1; i <= k; i ++)
    {
        cin >> v[i] >> x[i];
    }
    //dp
    for(int i = 1; i <= k; i ++)
    {
        for(int j = n; j >= v[i]; j --)
        {
            //体力值不能为0
            for(int t = m - 1; t >= x[i]; t --)
            {
                f[j][t] = max(f[j][t],f[j - v[i]][t - x[i]] + 1);
            }
        }
    }
    //体力值不能为0
    cout << f[n][m - 1] << " ";
    int s = m;
    for(int i = 0; i <= m - 1; i ++)
    {
        if(f[n][m - 1] == f[n][i])
        s = min(i,s);
    }
    cout << m - s << endl;
}

4、二维费用的01背包问题(属性为min)

1. 题目潜水员

潜水员为了潜水要使用特殊的装备。
他有一个带2种气体的气缸:一个为氧气,一个为氮气。
让潜水员下潜的深度需要各种数量的氧和氮。
潜水员有一定数量的气缸。
每个气缸都有重量和气体容量。
潜水员为了完成他的工作需要特定数量的氧和氮。
他完成工作所需气缸的总重的最低限度的是多少?
例如:潜水员有5个气缸。每行三个数字为:氧,氮的(升)量和气缸的重量:
3 36 120
10 25 129
5 50 250
1 45 130
4 20 119
如果潜水员需要5升的氧和60升的氮则总重最小为249(1,2或者4,5号气缸)。
你的任务就是计算潜水员为了完成他的工作需要的气缸的重量的最低值。

输入格式
第一行有2个整数 m,n。它们表示氧,氮各自需要的量。
第二行为整数 k 表示气缸的个数。
此后的 k 行,每行包括ai,bi,ci,3个整数。这些各自是:第 i 个气缸里的氧和氮的容量及气缸重量。

输出格式
仅一行包含一个整数,为潜水员完成工作所需的气缸的重量总和的最低值。

数据范围
1≤m≤21,
1≤n≤79,
1≤k≤1000,
1≤ai≤21,
1≤bi≤79,
1≤ci≤800
输入样例:
5 60
5
3 36 120
10 25 129
5 50 250
1 45 130
4 20 119
输出样例:
249

2. 分析

在这里插入图片描述

计算跟之前的01背包有所区别。

  1. 从1到i的物品中不选i,跟原来分析一样,是f[i - 1,j,k]
  2. 而选i,按照原来的分析是f[i - 1,j - v1[i],k - v2[i]] + w,但是注意,我们这里是不小于j,不是原来题目中的不超过。也就是说v[i] > j或者v2[i] > k都是可以的。我们之前剪掉这个v1[i],v2[i]就是相当于给这个必选的i流出一个坑来,然后再用i这个物品填上。不超过的时候,我们需要让j > v1[i],k > v2[i], 否则v1[i],v2[i]就会超出这个坑。而此时我们是不小于,也就是说,v1[i],v2[i]可以超过j,k,坑不够大是坑的问题,你够大我们也允许。在下面的代码中,可以看到这个处理。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 110;
//i代表
int f[N][N];
int main()
{
    int n,m,k;
    cin >> m >> n >> k;
    //f[j][t]初始化代表f[0][j][t]
    //而根据我们的定义,f[0][j][t]代表什么都不选,并且氧气不少于j,氮气不少于t,这种情况下只有f[0][0][0]有效,其他的所有情况都是非法情况
    //我们要在初始化的时候,将非法的情况初始化为一个负权益,从而引导后面的状态变更中不选这个状态
    //因为要求最小值,我们将除了f[0][0][0]之外的所有节点都初始化为很大的整数,f[0][0][0]按照定义初始化为0(因为什么都不选重量为0)
    memset(f,0x3f,sizeof f);
    f[0][0] = 0;
    for(int i = 1; i <= k; i ++)
    {
        int a,b,c;
        cin >> a >> b >> c;
        //这里就是我们上面说的问题,j,t可以遍历到0,由于是压缩到2维,所以从大到小遍历
        for(int j = m; j >= 0; j --)
        {
            for(int t = n; t >= 0; t --)
            {
            	//j - a小于0的时候,实际上就等于什么都不选,写成0
                f[j][t] = min(f[j][t],f[max(j - a, 0)][max(t - b,0)] + c);
            }
        }
    }
    //按照定义输出答案
    cout << f[m][n];
}

3. 小总结

在这里插入图片描述
图片来源
在这里插入图片描述
图片来源

5、01背包问题求具体方案

1.题目背包问题求具体方案

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出 字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 1…N。

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式
输出一行,包含若干个用空格隔开的整数,表示最优解中所选物品的编号序列,且该编号序列的字典序最小。
物品编号范围是 1…N。

数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 6
输出样例:
1 4

2.分析

动态规划的状态转移,其实可以跟最短路问题匹配起来。
在这里插入图片描述
状态都是在如上的图中进行转移的。
而求具体方案,就好像最短路问题求最短路径一样。由于求具体方案的时候需要用到所有的状态,所以不能用压缩状态。我们要将每一步走的路都记录下来,然后从后向前看看这条路到底是怎么走过来的。首先不考虑字典序我们看一下如何处理这种比较一般的情况。

我们从终点f[n][m]出发,看看终点f[n][m]是从哪个分支过来的,有可能有多种情况,有多种情况时(也就是可选可不选,f[i - 1][j] == f[i - 1][j - v[i]] + w[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 - vi]) + w[i] f[i,j]=max(f[i1,j],f[i1,jvi])+w[i]转移的,所以我们只需要看是否 f [ i ] [ j ] = = f [ i − 1 ] [ j − v [ i ] ] + w [ i ] f[i][j] == f[i - 1][j - v[i]] + w[i] f[i][j]==f[i1][jv[i]]+w[i]即可,如果不相等,则代表没有选择i这个物品,那就是从f[i - 1][j]这个状态走过来的。依次类推。只有某个i下上面的式子成立的时候,该物品i才被选择。

int j = m;
for(int i = n; i > 0; i --)
{
  if(j >= v[i] && f[i][j] == f[i - 1][j - v[i]] + w[i])
  {
    //这里写具体要做的操作,存储或者输出
 
    j -= v[i];
  }
}

那么接下来是如何求最小字典序,我们将选用贪心的方法进行解决。由于要求最小字典序,现在的状态是,我们求具体方案的时候,要从终点开始倒着向前捋。而遇到可选可不选的情况时,也就是遇到 f [ i − 1 ] [ j ] = = f [ i − 1 ] [ j − v [ i ] ] + w [ i ] f[i - 1][j] == f[i - 1][j - v[i]] + w[i] f[i1][j]==f[i1][jv[i]]+w[i]的时候,我们无法有效做出抉择,因为按照字典序排序的话,我们需要从1开始确定,可不可以选择1号。获得字典序最小值要从1到n判断,而获取路径要从n到1。这时候,我们需要做出一些改变。我们不从1号到n号取了,我们从n号到1号取物品,相当于把上面的图的起点和终点调换了一下,这样最短路还是不变的,而且获取路径也变成了从1到n,获取字典序最小值也从1到n,这样就可以顺利实现题目。

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

const int N = 1010;
int f[N][N],v[N],w[N];
int main()
{
    int n,m;
    cin >> n >> m;
    for(int i = 1; i <= n; i ++) cin >> v[i] >> w[i];
    for(int i = n; i >= 1; i --)
    {
        for(int j = 0; j <= m; j ++)
        {
        	//上一层变成了 i + 1层
            f[i][j] = f[i + 1][j];
            if(j >= v[i]) f[i][j] = max(f[i + 1][j],f[i + 1][j - v[i]] + w[i]);
        }
    }
    int j = m;
    for(int i = 1; i <= n; i ++)
    {
    	//能选就一定选
        if(j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i])
        {
            cout << i << ' ';
            j -= v[i];
        }
    }
}

6、贪心+01背包问题

1. 题目能量石

岩石怪物杜达生活在魔法森林中,他在午餐时收集了 N 块能量石准备开吃。
由于他的嘴很小,所以一次只能吃一块能量石。
能量石很硬,吃完需要花不少时间。
吃完第 i 块能量石需要花费的时间为 Si 秒。
杜达靠吃能量石来获取能量。
不同的能量石包含的能量可能不同。
此外,能量石会随着时间流逝逐渐失去能量。
第 i 块能量石最初包含 Ei 单位的能量,并且每秒将失去 Li 单位的能量。
当杜达开始吃一块能量石时,他就会立即获得该能量石所含的全部能量(无论实际吃完该石头需要多少时间)。
能量石中包含的能量最多降低至 0。
请问杜达通过吃能量石可以获得的最大能量是多少?

输入格式
第一行包含整数 T,表示共有 T 组测试数据。
每组数据第一行包含整数 N,表示能量石的数量。
接下来 N 行,每行包含三个整数 Si,Ei,Li。

输出格式
每组数据输出一个结果,每个结果占一行。
结果表示为 Case #x: y,其中 x 是组别编号(从 1 开始),y 是可以获得的最大能量值。

数据范围
1≤T≤10,
1≤N≤100,
1≤Si≤100,
1≤Ei≤10^5,
0≤Li≤10^5
输入样例:
3
4
20 10 1
5 30 5
100 30 1
5 80 60
3
10 4 1000
10 3 1000
10 8 1000
2
12 300 50
5 200 0
输出样例:
Case #1: 105
Case #2: 8
Case #3: 500

2. 分析

在这里插入图片描述
图片来源
如果不解决这个次序问题,那么符合条件的状态就会过于庞大,我们利用贪心,将枚举物品的次序进行固定,这时所有符合条件的状态就会被限制在一个比较小的范围内。
那么我们的目标是什么呢?换句话说就是我们需要一个什么样的次序呢?也就是所有石头在被吃到的时候的剩余能量之和是最大的(当然根据题意,最小值就是0,不会减成负数)。表示如下
m a x ( ( E 1 ) + ( E 2 − S 1 ∗ L 2 ) + ( E 3 − ( S 1 + S 2 ) ∗ L 3 ) + . . . + ( E i − ( S 1 + S 2 + . . . + S i − 1 ) ∗ L i ) + ( E i + 1 − ( S 1 + S 2 + . . . + S i ) ∗ L i + 1 ) + . . . ) max((E_1) + (E_2 - S_1 * L_2)+(E_3-(S_1 + S_2)*L_3) + ...+(E_i - (S_1+S_2+...+S_{i-1})*L_i)+(E_{i+1} - (S_1+S_2+...+S_{i})*L_{i+1})+ ...) max((E1)+(E2S1L2)+(E3(S1+S2)L3)+...+(Ei(S1+S2+...+Si1)Li)+(Ei+1(S1+S2+...+Si)Li+1)+...)
也就是我们要确定一个顺序,使得上面的式子取到最大值。
为了求排序的方案,可以交换第i项和第i+1项的位置,看看满足什么等价条件,就可以使得交换之后可以取到更大的值。表示成式子就是:

( E 1 ) + ( E 2 − S 1 ∗ L 2 ) + ( E 3 − ( S 1 + S 2 ) ∗ L 3 ) + . . . + ( E i − ( S 1 + S 2 + . . . + S i − 1 ) ∗ L i ) + ( E i + 1 − ( S 1 + S 2 + . . . + S i ) ∗ L i + 1 + . . . ) < = ( E 1 ) + ( E 2 − S 1 ∗ L 2 ) + ( E 3 − ( S 1 + S 2 ) ∗ L 3 ) + . . . + ( E i + 1 − ( S 1 + S 2 + . . . + S i − 1 ) ∗ L i + 1 ) + ( E i − ( S 1 + S 2 + . . . + S i − 1 + S i + 1 ) ∗ L i ) + . . . (E_1) + (E_2 - S_1 * L_2)+(E_3-(S_1 + S_2)*L_3) + ...+(E_i - (S_1+S_2+...+S_{i-1})*L_i)+(E_{i+1} - (S_1+S_2+...+S_{i})*L_{i+1}+ ...) <= (E_1) + (E_2 - S_1 * L_2)+(E_3-(S_1 + S_2)*L_3) + ...+(E_{i+1} - (S_1+S_2+...+S_{i - 1})*L_{i+1})+(E_i - (S_1+S_2+...+S_{i-1} + S_{i + 1})*L_i)+ ... (E1)+(E2S1L2)+(E3(S1+S2)L3)+...+(Ei(S1+S2+...+Si1)Li)+(Ei+1(S1+S2+...+Si)Li+1+...)<=(E1)+(E2S1L2)+(E3(S1+S2)L3)+...+(Ei+1(S1+S2+...+Si1)Li+1)+(Ei(S1+S2+...+Si1+Si+1)Li)+...
约掉相同的项有
( E i − ( S 1 + S 2 + . . . + S i − 1 ) ∗ L i ) + ( E i + 1 − ( S 1 + S 2 + . . . + S i ) ∗ L i + 1 < = ( E i + 1 − ( S 1 + S 2 + . . . + S i − 1 ) ∗ L i + 1 ) + ( E i − ( S 1 + S 2 + . . . + S i − 1 + S i + 1 ) ∗ L i ) (E_i - (S_1+S_2+...+S_{i-1})*L_i)+(E_{i+1} - (S_1+S_2+...+S_{i})*L_{i+1} <= (E_{i+1} - (S_1+S_2+...+S_{i - 1})*L_{i+1})+(E_i - (S_1+S_2+...+S_{i-1} + S_{i + 1})*L_i) (Ei(S1+S2+...+Si1)Li)+(Ei+1(S1+S2+...+Si)Li+1<=(Ei+1(S1+S2+...+Si1)Li+1)+(Ei(S1+S2+...+Si1+Si+1)Li)
再约掉相同的项
S i ∗ L i + 1 < = S i + 1 ∗ L i S_{i}*L_{i+1} <= S_{i + 1}*L_i SiLi+1<=Si+1Li也就是 S i L i < = S i + 1 L i + 1 \frac{S_i}{L_i}<=\frac{S_{i+1}}{L_{i+1}} LiSi<=Li+1Si+1
所以只要保证 S i L i < = S i + 1 L i + 1 \frac{S_i}{L_i}<=\frac{S_{i+1}}{L_{i+1}} LiSi<=Li+1Si+1,则交换第i项和第i + 1项可以让我们的目标变大。
我们设贪心解为按照 S i L i \frac{S_i}{L_i} LiSi的值排列的顺序。下面我们证明他是最优解。
假设有一个顺序是最优解(max值最大)。那么我们可以将所有元素按照 S i L i \frac{S_i}{L_i} LiSi的值跟其相邻的元素进行交换,这样的交换不会使得max值变小,所以最优解<=贪心解。而对于最优解,一定是最优解>=贪心解。所以有最优解==贪心解。
后面的问题就是一个01背包问题了
在这里插入图片描述
这里的“体积”是时间。之所以定为恰好是因为原来朴素的背包问题的f[0][j]的j从多少开始都行,这里的j就不一样了,不同的j开始其结果也是不一样的。所以这里用恰好。
那么不选i的可以表示为f[i][j] = f[i - 1][j]
选i的可以表示为f[i][j] = f[i - 1][j - s] + e - (j - s) * l

3. 代码

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 110, M = 10010;
struct node{
    int s,e,l;
    bool operator < (const node& n) const{
        return s * n.l < l * n.s;
    }
}all[N];
int f[M];
int main()
{
    int T;
    cin >> T;
    for(int p = 1; p <= T; p ++)
    {
        int n,m = 0;
        cin >> n;
        for(int i = 1; i <= n; i ++)
        {
            int s,e,l;
            cin >> s >> e >> l;
            all[i] = {s,e,l};
            m += s;
        }
        //贪心排序
        sort(all + 1, all + n + 1);
        //“恰好”的初始化,因为除了f[0][0]是合理的,其他的都应初始化为负收益,以避免不合法的状态转移到别的状态
        memset(f,-0x3f,sizeof f);
        f[0] = 0;
        for(int i = 1; i <= n; i ++)
        {
            for(int j = m; j >= all[i].s; j --)
            {
                int s = all[i].s, e = all[i].e, l = all[i].l;
                //j-s 是前面的总耗时
                f[j] = max(f[j],f[j - s] + max(0,e - (j - s) * l));
            }
        }
        int res = -0x3f;
        //由于是恰好,所以结果要遍历寻找
        for(int i = 0; i <= m; i ++) res = max(res,f[i]);
        cout << "Case #" << p << ": " << res << endl;
    }
}

参考资料

Acwing用户 ITNXD 题解
Acwing用户 merge题解
Acwing用户 彩色铅笔题解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值