背包DP大全

01背包问题

朴素01背包

#include<iostream>
#include<algorithm>
using namespace std;
int n, m;
int v[1001];//这个东西是体积
int w[1001];//这个东西是收益
int dp[1001][1001];//定义dp表
int main()
{
    cin >> n >> m;//n表示有n个物品可以取,m表示有物体的总体积是m
    for(int i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i];
    }
    //下标为0的时候表示目前没有物品可以取,所以能得到的最大价值就是0
    for(int i = 1; i <= n; i++)
    {
        for(int j = 0; j <= m; j++)
        {
            //第一种可能性,我不要这个位置的背包
            dp[i][j] = dp[i - 1][j];
            //第二种可能性,我要这个背包
            if(j - v[i] >= 0)
                dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
        }
    }
    cout << dp[n][m];
    return 0;
}

我们仔细观察上面的式子可以发现,这个朴素背包依赖关系只有前面一行,所以我们是不是只需要两排就可以了??

优化1代码如下:

dp[2] [n]优化

#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
int n, m;
int v[1001];//这个东西是体积
int w[1001];//这个东西是收益
int f[1001];
int g[1001];
int main()
{
    cin >> n >> m;//n表示有n个物品可以取,m表示有物体的总体积是m
    for(int i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i];
    }
    for (int i = 0; i <= n; ++i)
    {
        for(int j = 0; j <= m; j++)
        {
            //第一种可能我不要这个物品
            f[j] = g[j];//g表示上一个状态的东西,f表示当前状态的东西
            //第二种可能,我需要这个背包
            if(j - v[i] >= 0)
            {
                f[j] = max(f[j], g[j - v[i]] + w[i]);//这里要取的是要这个物品和不要这个物品的最大值,所以我们需要取max
            }
        }
        memcpy(g, f, sizeof(f));//g是上一个状态的数组
    }
    cout << g[m];
    return 0;
}

我们现在还是有两条数组,我们能不能得寸进尺,把它优化成只有一条数组呢??

终极背包压缩

#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
int n, m;
int v[1001];//这个东西是体积
int w[1001];//这个东西是收益
int dp[1001];
int main()
{
    cin >> n >> m;//n表示有n个物品可以取,m表示有物体的总体积是m
    for(int i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i];
    }
    for(int i = 1; i <= n; i++)
    {
        for(int j = m; j >= v[i]; j--)
        {
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    cout << dp[m];
    return 0;
}

我们发现的是,在同一条数组中,我们依赖的东西在前面,所以我们只需要从后往前遍历就不会影响到数据,从而完成一条数组的优化。

完全背包问题

有n种物品要放到一个袋子里,袋子的总容量为m,第i种物品的体积为vi,把它放进袋子里会获得wi的收益,每种物品能用无限多次,问如何选择物品,使得在物品的总体积不超过m的情况下,获得最大的收益?请求出最大收益。

输入格式
第一行两个整数n,m。

接下来n行,每行两个整数vi,wi。

输出格式
一个整数,表示答案。

完全背包问题和01背包问题的对比

问题对比

完全背包问题相比01背包问题是同一个物品可以使用无限多次

解题策略对比

因为可以使用无限多次,所以状态转移方程发生了变化,因为是无限多次而不是有限次,所以我们通过观察空间关系可以发现状态转移方程

在这里插入图片描述

代码如下:

#include<iostream>
#include<algorithm>
using namespace std;
int n, m;
int v[1001];
int w[1001];
int dp[1001][1001];
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++)
        {
            dp[i][j] = dp[i - 1][j];
            if(j - v[i] >= 0)
                dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);
        }
    }
    cout << dp[n][m];
    return 0;
}

终极背包压缩

#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
int n, m;
int v[1001];
int w[1001];
int f[1001];
int g[1001];
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];
    return 0;
}

这里和01背包的终极压缩优化不同的地方就是我们第二层循环的遍历方向是反的。这是因为的依赖的就是我之前求出来的东西,所以我们需要这么遍历。

多重背包问题

有n种物品要放到一个袋子里,袋子的总容量为m,第i种物品的体积为vi,把它放进袋子里会获得wi的收益,可以用li次。问如何选择物品,使得在物品的总体积不超过m的情况下,获得最大的收益?请求出最大收益。

输入格式
第一行两个整数n,m。

接下来n行,每行三个整数vi,wi,li。

输出格式
一个整数,表示答案。


多重背包问题和完全背包问题的对比

问题对比

多重背包每一个物品可以选择多次,但是和完全背包问题不同的是,完全背包问题可以选择无限多次,但是多重背包问题只能选择有限多次,这会让代码发生翻天复地的变化

解决方案对比

看图:
在这里插入图片描述

我们可以发现,红色打圈的地方多出来了一个东西,我们如果问题是求方法数的话,我们可以利用左边的东西,然后减去红色的部分,组成一个依赖关系,但是我们求的是最大值,我们不方便进行操作,而完全背包问题是无限的,所以他们的结束点必定在同一个地方,也就是最开始的地方,不存在多这么一个出来的情况,所以才可以那么巧妙的转换依赖关系。

而它就麻烦了

多重背包问题朴素做法

如果我们把这个背包展开的话,重量为v的背包可以取5次就相当于有5个重量为v的背包,这样的话就转换成了01背包问题。

#include<iostream>
#include<algorithm>
using namespace std;
int n, m;
int v[1001];
int w[1001];
int l[101];
int dp[1001];
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i] >> l[i];
    }
    for(int i = 1; i <= n; i++)
    {
        for(int k = 1; k <= l[i]; k++)//我们在这个地方让01背包问题进行l[i]次就可以了
        {
            for(int j = m; j >= v[i]; j--)
            {
                dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
            }
        }
    }
    cout << dp[m];
    return 0;
}

或者说我们来写一个更朴素一点的:二维背包

#include<iostream>
#include<algorithm>
using namespace std;
int n, m;
int v[1001];
int w[1001];
int l[101];
int dp[1001][1001];
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i] >> l[i];
    }
    for(int i = 1; i <= n; i++)
    {
        for(int j = 0; j <= m; j++)
        {
            for(int times = 0; times * v[i] <= j && times <= l[i]; times++)
            {
                dp[i][j] = max(dp[i][j], dp[i - 1][j - times * v[i]] + times * w[i]);
            }
        }
    }
    cout << dp[n][m];
    return 0;
}

logn多重背包优化

数学基础预备:

在这里插入图片描述

大概的意思就是:

8 = 1 + 2 + 4 + 1

(这个1就是8 - 1 - 2 - 4)得到的

我们的思路是:我们本来是把重量为2,可以取5次的物品分成5个2。但是我们现在利用这个性质可以把重量2,可以取5次的物品总重为10,分成

10 = 1 + 2 + 4 + 3个物品,也就是说循环次数也logn的次数减少了。然后把剩下的3单独做一次01背包问题。就是说我把背包的重量按照一定的规律自由组装了

#include<iostream>
#include<algorithm>
using namespace std;
int n, m;
int v[2001];
int w[2001];
int l[2001];
int dp[2001];
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i] >> l[i];
    }
    for(int i = 1; i <= n; i++)
    {
        int res = l[i];
        for(int k = 1; k <= res; res -= k, k *= 2)//相当于就是我把原来的取k次,转换成了取logk次,然后再加上一次01背包
        {
            for(int j = m; j >= v[i] * k; j--)
            {
                dp[j] = max(dp[j], dp[j - v[i] * k] + w[i] * k);
            }
        }
        for(int j = m; j >= v[i] * res; j--)
        {
            dp[j] = max(dp[j], dp[j - v[i] * res] + w[i] * res);
        }
    }
    cout << dp[m];
    return 0;
}

多重背包单调队列优化

单调队列基本介绍

生物攻击力问题
有n个生物,第i个生物会在第i到第ai(i≤ai≤n)天出现,它的攻击力为bi。其中对于所有i(1≤i<n),满足ai≤ai+1。请输出每天出现的生物的攻击力的最大值。

输入格式
第一行一个整数n。

接下来n行,每行两个整数ai,bi。

输出格式
一共n行,每行一个数表示答案。

第i个整数表示第i天出现的生物的攻击力的最大值。

先提两句:我们什么时候使用单调队列??
1.我们每次,每一个步骤,每一个阶段都要求出最值,这样可以想到使用单调队列,时间复杂度很低

2.运动轨迹类似于滑动窗口,左区间和右区间都只能进,不能退。

3.队列中的元素必须是单调的

4.后面的元素不可能比前面的元素先消失

来看这一道题,完全符合上面说的,我们可以用数组来模拟一个单调队列,当然也可以直接使用

1.考虑第i天,对于第j个生物,如果b[j] < b[i],那么之后第j个生物都不用考虑了。因为在其存在的期间一定会被第i给生物压制。

代码如下:

#include<iostream>
#include<algorithm>
using namespace std;
int a[100001];//出现时间
int b[100001];//攻击力
int c[100001][2];//0用来存放攻击力,1用来存放存活时间
int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i] >> b[i];
    }
    int head = 1;
    int tail = 0;//定义头指针和尾指针
    for(int i = 1; i <= n; i++)//表示第i个生物,并且同时代表最后的出现时间
    {
        for(; head <= tail && c[tail][0] <= b[i]; tail--);//删掉比目前这个位置攻击力要小的,因为它永远会被压制住,以后完全不需要考虑了
        c[++tail][0] = b[i];//填入攻击力
        c[tail][1] = a[i];//填入存活时间
        printf("%d\n", c[head][0]);
        for(; head <= tail && c[head][1] == i; head++);//把头部过期的东西给删掉
    }
    return 0;
}

这里的printf必须要写在中间

因为我的物品是最后出现在a[i]天,而我们的存活时间是它能活的最后时间,不是死了的时间,如果我存活时间填的是死了的时间的话,那么就是要在for后面写。

多重背包单调队列优化正片

有n种物品要放到一个袋子里,袋子的总容量为m,第i种物品的体积为vi,把它放进袋子里会获得wi的收益,可以用li次。问如何选择物品,使得在物品的总体积不超过m的情况下,获得最大的收益?请求出最大收益。

输入格式
第一行两个整数n,m。

接下来n行,每行三个整数vi,wi,li。

输出格式
一个整数,表示答案。

这里很重要,我们为什么要想到多重背包可以使用单调队列进行优化??
理由1:我们之前是通过穷举每一层来获得方案的,但是我们确实要获得每一个位置的最大值,这跟我们上面说的单调队列的使用条件很想。

理由2:我的背包只要拿了重物,就一定比之前大,不存在越拿越少的情况,所以是单调的,满足条件。

理由3:我们发现如果以j % v[i]来进行分类的话,那么这个东西的运动轨迹很滑动窗口很想。

我们发现假如j = 13, v[i] = 4。那么只跟

0 4 8 12

1 5 9 13

2 6 10

3 7 11

有关,你看这不就是滑动窗口优化吗!!

**因为我们可以这样分组,所以才能使用单调队列优化,如果

那么还有一个问题,我们可以拿裸的值来进行比大小吗??

肯定不行,因为背包必定是越装越贵的.

同重量进行比较,我现在重量是j,那么一定比我重量小于j的东西装的东西要多,因此我们要同重量比较e = dp[j] - x * w[i],x表示我拿了几次了。例如dp[j]依赖于dp[j - x * v[i]],j是1 5 9 13里面我拿了x次可以达到的值。那么这个时候减去x * w[i],和上面的重量是对应的,所以应该这么进行比较。

如果还想不明白可以看这个代码:

    for(int i = 1; i <= n; i++)
    {
        int res = l[i];
        for(int k = 1; k <= res; res -= k, k *= 2)//相当于就是我把原来的取k次,转换成了取logk次,然后再加上一次01背包
        {
            for(int j = m; j >= v[i] * k; j--)
            {
                dp[j] = max(dp[j], dp[j - v[i] * k] + w[i] * k);
            }
        }
        for(int j = m; j >= v[i] * res; j--)
        {
            dp[j] = max(dp[j], dp[j - v[i] * res] + w[i] * res);
        }
    }

这是朴素解法,我们可以看到朴素解法比较最大值的时候就是减去了之后比较的,然后再加上,那这里只不过是把求最大值的方式变成了滑动窗口解决而已。

在这里插入图片描述

(这是王佬的笔记,拿来借用一下)

代码如下:

#include<iostream>
#include<algorithm>
using namespace std;
int n, m;
int v, w, l;
int dp[10001];
int c[10001][2];
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        cin >> v >> w >> l;
        for(int mod = 0; mod < v; mod++)
        {
            int head = 1;
            int tail = 0;
            for(int p = mod, x = 0; p <= m; p += v, x++)//mod表示我的分组的模
                //p表示我目前的重量,x表示我达到这个重量需要拿几次
            {
                int e = dp[p] - w * x;//计算攻击力,类别上一个单调队列的题型
                int t = l + x;//计算持续时间
                //删除比这个东西重量小的
                for(; head <= tail && c[tail][0] <= e; tail--);
                c[++tail][0] = e;
                c[tail][1] = t;
                dp[p] = c[head][0] + x * w;
                //删除时间到期的
                for(; head <= tail && c[head][1] == x; head++);
            }
        }
    }
    cout << dp[m];
    return 0;
}

分组背包

有n种物品要放到一个袋子里,袋子的总容量为m。第i个物品属于第ai组,每组物品我们只能从中选择一个。第i种物品的体积为vi,把它放进袋子里会获得wi的收益。问如何选择物品,使得在物品的总体积不超过m的情况下,获得最大的收益?请求出最大收益。

输入格式
第一行两个整数n,m。

接下来n行,每行三个整数ai,vi,wi。

输出格式
一个整数,表示答案。

朴素写法:

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int a[1001];//物品属于第几组
int v[1001];//体积
int w[1001];//收益
int n, m;
vector<int> c[1001];
int dp[1001][1001];//定义dp表的含义是取到第i组的时候,我的背包里面装的最大价值
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i] >> v[i] >> w[i];
        c[a[i]].push_back(i);//这样可以记录每一组的对应下标是多少
    }
    for(int i = 1; i <= n; i++)//枚举组数,这个是组数,不是物品数,因为最多n组,所以才这么写
    {
        for(int j = 0; j <= m; j++)
        {
            //这一组我一个都不选择
            dp[i][j] = dp[i - 1][j];
            //我要取这个组里面的某一个数据,所以这个时候我要对这个组进行枚举,求出最大值
            for(auto e : c[i])
            {
                if(j - v[e] >= 0)
                    dp[i][j] = max(dp[i][j], dp[i - 1][j - v[e]] + w[e]);
            }
        }
    }
    cout << dp[n][m];
    return 0;
}

状态:取到第i组的时候背包里面装的最大值。

转移:我可以不取这个组里面的物品。也可以取这个组的物品,但是取这个组的物品的话我们需要对这个组的所有物品进行枚举。

压缩成一维dp

因为我们发现依赖关系只有上一层,所以我们可以进行压缩。

代码如下:

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int a[1001];//物品属于第几组
int v[1001];//体积
int w[1001];//收益
int n, m;
vector<int> c[1001];
int dp[1001];
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i] >> v[i] >> w[i];
        c[a[i]].push_back(i);//这样可以记录每一组的对应下标是多少
    }
    for(int i = 1; i <= n; i++)
    {
        for(int j = m; j >= 0; j--)
        {
            for(auto e : c[i])
            {
                if(j - v[e] >= 0)
                    dp[j] = max(dp[j], dp[j - v[e]] + w[e]);
            }
        }
    }
    cout << dp[m];
    return 0;
}

二维背包

朴素写法:

不过是多了一重限制,没有本质区别

代码如下:

#include<algorithm>
#include<iostream>
using namespace std;
int n, m, k;
int v[101];
int w[101];
int t[101];
int dp[101][101][101];
int main()
{
    cin >> n >> m >> k;
    for(int i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i] >> t[i];
    }
    for(int i = 1; i <= n; i++)
    {
        for(int j = 0; j <= m; j++)
        {
            for(int x = 0; x <= k; x++)
            {
                //我可以不取这个物品
                dp[i][j][x] = dp[i - 1][j][x];
                //我可以取这个物品
                if(j - v[i] >= 0 && x - t[i] >= 0)
                    dp[i][j][x] = max(dp[i][j][x], dp[i - 1][j - v[i]][x - t[i]] + w[i]);
            }
        }
    }
    cout << dp[n][m][k];
    return 0;
}

我们可以发先i只依赖于上一行,所以可以进行压缩:

这里提一句,别被砝码问题洗脑了,一般来说有几个格子就有几重循环,表必须全部填完才能拿到结果。

压缩写法

#include<algorithm>
#include<iostream>
using namespace std;
int n, m, k;
int v[101];
int w[101];
int t[101];
int dp[101][101];
int main()
{
    cin >> n >> m >> k;
    for(int i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i] >> t[i];
    }
    for(int i = 1; i <= n; i++)
    {
        for(int j = m; j >= v[i]; j--)
        {
            for(int x = k; x >= t[i]; x--)
            {
                dp[j][x] = max(dp[j][x], dp[j - v[i]][x - t[i]] + w[i]);
            }
        }
    }
    cout << dp[m][k];
    return 0;
}
  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胡桃姓胡,蝴蝶也姓胡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值