首页 > 其他分享 >基本背包问题复习(01背包,完全背包,多重背包,分组背包)

基本背包问题复习(01背包,完全背包,多重背包,分组背包)

时间:2023-03-30 21:11:47浏览次数:58  
标签:状态 背包 复习 int 集合 01 物品 include

背包问题,本质上就是给几种物品,每个物品有体积有价值,可能有个数限制

有一个容量有限的背包,在背包能装下的前提下,能装的最大价值是多少

 

背包问题一般分为这几种:

01背包:每件物品只有一个
完全背包:每件物品有无限个
多重背包:每件物品有Si个(有限个)
分组背包:所有物品被分为多个组,每一组最多只能选一个物品

 

动态规划主要要点:

状态表示:如何表示这样的选法的值,以及要考虑多少维状态
状态计算:如何计算每一个状态
DP优化:一般来说是对dp方程或代码进行等价变形

从集合的角度理解dp(所有选法的集合)

状态表示:
1.这样的状态表示怎样的集合
2.这样的状态的值,是这样的集合的某种属性(Max,Min,数量)

集合表示状态:
1.表示所有选法  
2.(1)只从前i个物品中选 (2)总体积<=j
满足这两个条件的所有选法的集合就是f[i][j]表示的集合
属性是f[i][j]这个集合中价值的最大值

属性计算(f[i][j]可以被如何计算出来):
如果把所有状态计算出来之后,我们的答案应该是f[n][v],f[n][v]所表示的集合就是n个物品中总体积小于v的最大价值的选法,而它的值就是最大价值

状态计算:
一般对应集合的划分
例如01背包问题
把f[i][j]这样的集合分类
分为:1.所有不包含第i个物品的选法 2.所有包含第i个物品的选法
一般划分有两个原则:1.不重复(某个元素只会属于一个集合)
2.不漏(某一个元素不能不属于任何一个集合)

不重复不一定满足,比如求最值时,可以重复比较

对于左半边,为从1~i中选,但是不选i,体积<=j的选法,既然不选第i个物品,那么
可以等价为 从1~(i-1)中选,体积<=j的选法的集合,即f[i-1][j]表示的集合
而f[i-1][j]的值就是从1~(i-1)中选的最大价值

那么左边的最大值就是f[i-1][j]

对于右半边,为从1~i中选,且一定选i,体积<=j的选法,这里没法自然像左边那样得到
需要绕一下:可以先把1~i中的所有选法的第i个物品都去除掉,不考虑有第i个物品,这样是不影响最大值的选择的,最大值还是那样的选法
那么这样就等价为从1~(i-1)中选,体积<=j-Vi的选法的集合,即f[i-1][j-Vi]的集合
f[i-1][j-Vi]的值就是这些选法的最大值,但它还不是我们的右半边的最大值
直接加上Wi就是我们的右半边的最大值了

那么右边的最大值就是f[i-1][j-Vi]+Wi

那么整个f[i][j]的最大值就是这左右两边两个集合的最大值
即f[i][j]=max(f[i-1][j] , f[i-1][j-Vi]+Wi )
而这就是状态转移方程,所有的选法状态之间都是靠这个方程转移的

但是右半边可能不存在,即空集
当j<Vi时,定义上需要选择第i个物品,但是体积不够选不了第i个物品
故这种情况下,不存在选了第i个物品的选法,为空集
但是左边一定存在

01背包:
https://www.acwing.com/problem/content/submission/2/

//由于f[i][j]的状态表示
//f[0][0~m]应该都是0,表示前0个物品的选择,不超过m的体积,0个物品即一个都没选,故价值为0
//由于f[i][j]定义为全局变量,默认为0,就不用去再设置了
#include<algorithm>
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 1005;
int v[N],w[N],f[N][N];
int a,b;
int main()
{
    cin >> a >> b;
    for(int i=1;i<=a;i++)
        scanf("%d%d",&v[i],&w[i]);
    for(int i=1;i<=a;i++)
        for(int j=1;j<=b;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[a][b] << endl;
    return 0;
}

由于每一次循环我们的状态计算只会用到前一层的状态,而再往前的状态不会用到
因此可以用滚动数组来优化

而状态转移方程的j总是 <= j的,无论是j,还是j-Vi,都小于j
因此具有单调性,可以考虑优化为1维

对原来的方程进行等价变化

#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;
}

 

完全背包:
https://www.acwing.com/problem/content/3/

同样的用集合角度分析

状态表示和01背包基本一样
但是状态计算-集合的划分不一样
由于完全背包有无限个物品
可以考虑如图01背包一样,第i个物品选几个,来划分组(子集)

第i个物品选多少个,来划分

这样就把f[i][j]划分为k个子集了

再去看每一个子集如何计算
选0个,如同01背包一样,为:f[i-1][j]

通解,第i个物品选k个这样的集合计算,需要绕一下
1.去掉k个物品i,不考虑第i个物品
2.再去求Max,即f[i-1][j-k*Vi]表示的集合的最大值
3.再加回来k个物品i
即f[i-1][j-k*Vi]+k*Wi
而选0个是通解的k为0的情况

综合来看,就是
f[i][j]=f[i-1][j-k*Vi] + k*Wi

但是这样的状态转移方程复杂度较高
需要枚举i,j的状态,还需要枚举物品选多少个,最多为k*Vi<=j
需要用到三重循环

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1010;

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

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-k*v[i] ]+k*w[i]);
    cout << f[n][m] << endl;
    return 0;
}

优化状态转移方程
展开:
对于第i个物品,f[i][j]=max(一个都不选,选一个,选两个...选k个),取这些情况

对比一下f[i][j-v]

观察f[i][j-v],发现与f[i][j]对应的项只少一个w
那么f[i][j]的最大值就是f[i][j-v]的最大值,再加上一个w

即有

那么就优化成O(N^2)了

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1010;

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

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-1][j],f[i][j-v[i]]+w[i]);
        }
    cout << f[n][m] << endl;
    return 0;
}

再用滚动数组优化为1维

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1010;

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

int f[N];


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++)//枚举空间
        {
            f[j]=max(f[j],f[j-v[i]]+w[i]);//不需要倒序,符合原来的状态转移方程
        }
    cout << f[m] << endl;
    return 0;
}

 

多重背包

https://www.acwing.com/problem/content/4/

同样的是从集合角度
 

状态计算就是集合的划分

也是根据第i个物品选多少个,来划分f[i][j]类

 

如同完全背包
状态转移方程为:f[i][j] = max(f[i-1][j-k*v[i]] + k*w[i]) ; k=0,1,2,3...,s[i]

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1010;

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

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

优化:

分析转移方程

对比f[i][j-v]

发现会多出一项f[i-1][j-(s+1)v]+sw
由于这一项的存在,导致我们无法直接使用完全背包的优化方式求最大值

这里采用二进制的优化方式
即将2的整数次幂分为一组,即选1个,2个,4个,8个,...512个第i个物品,

通过选择这些组,可以凑出来选择任意个第i个物品的个数(倍增思想)
每一组可以看成是01背包的问题,每个组只能选一次

枚举每一个组选与不选,就可以拼凑所有的方案

例如s=200
那么 1, 2, 4, 8, 16, 32, 64
最多只到64,因为若到128,那么最多能凑成255个物品
但是我们的实际没有那么大,最多只能到200
从1+到64,是127,还差73
那么这些组就可以表示0~127之间任何一个数
那么再加个73就可以凑出来0~200的任何一个数了

一般来看,把它的规律抽象出来:
有一个s
1 , 2 , 4 ,8 ,...2^k,再加上个 c , c<2^(k+1)
显然用倍增思想可以知道可以凑出来0 ~ 2^(k+1)-1 的任何一个数
加上c后就是 c~2^(k+1)-1+c
即c~s
这两个区间可以证明并起来是连续的,即0~s,任何一种拼法都能被凑出来

即给定第i个物品个数为Si个,那么就把Si拆分成logSi个,再对这logSi个新物品做一遍01背包问题就ok了

原来的时间复杂度是N*V*S
现在是N*V*logS

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 25000,M=2010;//2000个物品,log2000=11,那么最大空间22000

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

int main()
{
    cin >> n >> m;
    for(int i=1;i<=n;i++)
    {
        int a,b,s;
        cin >> a >> b >> s;//体积,价值,个数
        int k=1;
        while(k<=s)//二进制优化
        {
            cnt++;
            v[cnt]=a*k;
            w[cnt]=b*k;
            s-=k;
            k*=2;
        }
        if(s>0)//此时s为剩余的c
        {
            cnt++;
            v[cnt]=a*s;
            w[cnt]=b*s;
        }
    }
    n=cnt;
    //一维01背包
    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;
}

 

分组背包问题:

https://www.acwing.com/problem/content/9/

同样的从集合的角度出发

状态计算:

枚举第i组物品选哪个,或者不选

第一种情况即第i组物品一个都不选,等价于从前i-1个物品中选,体积<=j,即f[i-1][j]
对于通解,从第i个组选第k个物品,其集合为f[i-1][j-v[i][k] ] +w[i][k],v[i][k]为第i组物品的第k个物品,w[i][k]同理

同样的,分组背包可以优化为1维
1.转移方程用的是上一层状态转移的话,就需要从大到小枚举体积(从大到小可以保证用的还是上一层的状态,即第i-1次循环时更新的状态,这个要点主要对我来说是要明白二重循环的顺序,它是先枚举完第i-1层的所有j,即f[i-1][0~m]全都被计算过了,再去计算第i层,而j又大于j-v[i][k],因此正序的话会先计算小的体积,即f[i][j-v[i][k]),那么此时如果是一维的话就和原式不符了,倒序就会先计算大的体积,f[j-v[i][k]]此时是没有在第i层更新的,它上一次更新是在i-1层的时候,那么就符合原式了)
2.而如果是本层状态转移过来的话,就从小到大枚举体积

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 110;

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

int main()
{
    cin >> n >> m;
    for(int i=1;i<=n;i++)//组
    {
        cin >> s[i];
        for(int j=0;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=0;k<s[i];k++)
                if(v[i][k]<=j)//可以放
                    f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
    cout << f[m] << endl;
    return 0;
}

 

标签:状态,背包,复习,int,集合,01,物品,include
From: https://www.cnblogs.com/lxl-233/p/17270296.html

相关文章

  • 前端基础小复习
    目录1.HTTP协议四大特性2.HTTP协议数据传输格式3.状态码4.基本选择器会前端基础在IT行业很有帮助,无论是做爬虫数据分析,解析数据,做数据清洗都有帮助,因为我准备做数据获取过程中发现浏览器的HTML看不懂,直接影响了我的工作进度,因此直接暂停,网页结构有三个部分组成的即HTML、CSS......
  • Exp3-免杀原理 实验报告—20201229赵斌
    免杀原理与实践基础问题回答Q1:杀软是如何检测出恶意代码的?(1)基于特征码的检测特征码是一段或多段数据,如果一个可执行文件(或其他运行的库、脚本等)包含这样的数据则被认为是恶意代码。AV软件厂商要做的就是尽量搜集最全的、最新的特征码库。所以杀毒软件的更新很重要。过时的特......
  • JAVA~适合新手和复习~基础七(线程)
    Java多线程编程一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。线程和进程关系:一个进程包括由操作系统分配的内存空......
  • ISYS3401 IT Evaluation
    ISYS3401ISYS3401ITEvaluation(2023)IndividualAssignment1(30%)ThisITEvaluationassignmentfollowsa3-phaseassessmentplanintroducedinlecture5.The......
  • P4156 [WC2016]论战捆竹竿 题解
    题目链接题意描述给定一个字符串\(S\),你初始拥有一个空串\(T\),每次可以选择这个字符串的一个Border,去掉它后接在\(T\)的后面,操作后\(S\)不变,给出一个上限\(w\),求......
  • 融资侦集档案—F档案001
    在过去的一周内,雷锋网报道获得融资的公司有十三家,总融资额为三亿一千万。其中最大单笔融资额为一亿,由一家医疗技术公司Castlight获得,这也是医疗技术公司有史以来获得的最大......
  • 开关磁阻电机控制仿真(matlab 2016b版本仿真模型) 电流斩波控制、电压PWM控制
    开关磁阻电机控制仿真(matlab2016b版本仿真模型)模型包涵:开关磁阻电机传统控制:电流斩波控制、电压PWM控制、角度位置控制。智能控制:128三相开关磁阻电机有限元分析本......
  • 【题解】[HEOI2013]SAO
    题目分析:考虑这是一个树形图,所以就先直接当作树来做。这个题其实就是让我们求解有多少种拓扑序而且题目中边方向的限制其实就是在限制拓扑序的前后,而一般这种题在设计\(......
  • 力扣---面试题 01.09. 字符串轮转
    字符串轮转。给定两个字符串s1和s2,请编写代码检查s2是否为s1旋转而成(比如,waterbottle是erbottlewat旋转后的字符串)。示例1:输入:s1="waterbottle",s2="erbottlewat"......
  • [LNOI2014] LCA 树链剖分+离线处理+lca转化
    困困的开始了我的修炼树剖之旅途考虑怎么搞这个lca是说,习惯了倍增求lca,突然冒出这么一个东西还真不会搞那要么能一次性求很多个lca(?),要么把deep[lca(i,z)]这个东西转化......