01背包

引用一下大佬的解答
https://www.acwing.com/solution/content/1374/

01背包问题

这里是2023/2/26日更新的,因为发现之前的理解稍微有点绕
新的:
从集合的角度出发,题目要求用背包对每个物品进行选择装入,并且还要判断是否满足体积要求
集合:每个物品都有装与不装的两种状态,可以用二进制表示,n个物品: 011011....这样表示第i(i为二进制位数)个物品是否装入
那么这样一共有2^n种状态(选法),如果用dfs暴搜的话无疑会超时,因为数据量到达1e3了
dp的意义就在于一个dp数组能够表示一个集合,而不是单单一种状态,或者说,一个集合能够表示多数的状态
我们只需要计算一个集合的属性,就能得到多种状态的最佳解
例如:以最后一个物品是否装入为标准,区分这2^n个状态,结果为两个集合(设n为3)
未装       装了
000        001
010        011
100        101
110        111
我们的要求的解就在这个两个集合中取最大值,而这个两个集合的解要如何计算?
实际上,每个集合都可以继续用dp数组的概念表示
也能继续以倒数第二个物品是否装入为标准继续划分,如此就递归了

那么你会发现,这个问题的最关键的地方是:
1.dp数组怎么表示所有的状态
2.以最后一个物品是否装入来划分集合
3.如何计算dp数组的解

这些问题闫氏dp法已经给了答案
我们只需要给dp数组一个定义:f[i][j]表示所有只考虑前i个物品,总体积<=j的所有选法的集合
满足不重复,不漏掉,但是考虑解为最值的时候重复计算也是可以的,因为重复计算最值不会改变
而f[i][j]本身的解就是这个集合的最佳值(可能是最值,也可能是数量)
如何计算f[i][j]?
就是以最后一个物品是否选入为标准划分两个集合
不选:从概念定义上不选的解就是f[i-1][j]的值,即f[i][j]=f[i-1][j]
选了:将集合分为两部分,一份是1~i-1,另一份是单独一个i,而第i个是必选的,也就是总价值要加上wi,而总体积要减去vi
1~i-1这些选法集合可以用定义表示为f[i-1][j-vi],即f[i][j]=f[i-1][j-vi]+w[i]
剩余体积不够:当j<vi时,无法选取第i个物品,则第二个集合不存在,需要特判是否存在第二个集合,
在这两者之间取最值即可,由于定义是递归的,那么计算就相反是迭代的
空间优化:由于公式中只有i和i-1,那么实际上在循环中,利用滚动数组的原理,我们就不需要用到i,可以优化一维空间
要点:逆序计算即可,简单的说
如果是正序的时候,计算顺序是由f[0~i][0~m],
由于公式是:f[i][j]=max( f[i-1][j] ,f[i-1][j-vi]+w[i]),那么在i循环时,i-1的循环就已经算过了,根据公式,将f[i][j]改为f[i-1][j],可得f[i-1][0~m]都算过了,这样在双重循环的时候是没有问题的
优化后,如果逆序,在i循环算f[j]时,公式是:f[j]=max( f[j] , f[j-vi] ) ,同样的原理带入,由于是逆序的j循环,所以f[j-vi]还未更新,所以f[j-vi]还是上一层的i循环的值,即f[i-1][j-vi],如此满足公式,优化等价
如果正序,j-vi<j,f[j]=max( f[j] , f[j-vi] ),由公式知f[j-vi]与f[j]都在第i层循环,但是j-vi<j,所以j-vi先算出来,所以是f[i][j-vi]而不是f[i-1][j-vi]




 

 

旧的:
这里定义f[i][j]为当前对于前i个物品进行决策是否选取,且当前背包容积为j的最大价值
可以将定义套入到两层循环中的f[i][j],或者一下解释中,加深理解
即从f[0~n][0~m]中取出最大值,每种组合一一尝试比对取最值
即枚举计算所有状态取最值
其中,计算状态可以知道对于f[i][j],其状态只能是由选了第i个和没选第i个物品的集合组成的
没选,意味着背包价值没变,与求前i-1个物品最大价值,最大容量为j的状态相同,则有:f[i][j]=f[i-1][j]
选了,意味着背包价值增加了w[i],即求决策了前i个物品,最大容量为j的最大价值,如此不好解答
我们借用只考虑前i-1个物品的最大价值的状态来求解
即已经是不选第i个物品,前i-1个物品,最大空间相应的由于不选第i个,则需减去v[i],借用这样的状态
即选了第i个物品,与只考虑了前i-1个物品,空间去除v[i],相当于j-v[i]的状态的最大价值仅仅相差了选了第i个物品的价值w[i],即有:f[i][j]=f[i-1][j-v[i]]+w[i] 

关于优化:
利用了二重循环的特性,二重循环可以枚举所有状态
取消一个维度,我们可以用循环的i,j的信息来补充
并且不需要所有状态,我们只需要前一个状态即可一一推出求最值(滚动数组)
i循环意味着决策考虑了前i个物品,j循环意味着决策考虑了v[i]~m的空间(0~v[i]之间的空间无意义)
因此可以用f[j]代替f[i][j]
但是有一个问题在j循环中,从小到大枚举会改变之前的状态(因为j-v[i]一定小于j,对于f[j]=max(f[j],f[j-v[i])来说,从小到大枚举,小的一定先算,所以实际上与原式不同,是f[i][j-v[i]]而不是f[i-1][j-v[i]],改为从大到小枚举,计算f[j]=max(f[j],f[j-v[i]),其中f[j-v[i]]还未计算,因此它还是f[i-1][j-v[i]]的值,具体了解请看引用),于是改为从m~v[i]枚举

优化前:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N =1005;
int n,m;
int w[N],v[N];
int f[N][N];
int main()
{
    cin >> 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];
            if(j>=v[i])f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
        }
    cout << f[n][m] << endl;
    return 0;
}

优化后:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N =1005;
int n,m;
int w[N],v[N];
int f[N];
int main()
{
    cin >> 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=m;j>=v[i];j--)
            f[j]=max(f[j],f[j-v[i]]+w[i]);
    cout << f[m] << endl;
    return 0;
}
posted @ 2022-09-24 16:17  风乐  阅读(111)  评论(0)    收藏  举报