01,完全,多重,混合,分组背包相关题目


背包问题的状态定义基本都是:
在前i个物品选,体积不超过j的所有选法。属性可以是max, min, count

注意一点:背包问题基本都可以边读边进行dp运算。多这么写, 可以优化代码简洁度。

01背包

一维写法

#include <iostream>

using namespace std;

const int N = 1010;

int f[N][N];

int n, m;

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int v, w;
        cin >> v >> w;
        for(int j = 0; j <= m; j++)
            if(j >= v) f[i][j] = max(f[i - 1][j], f[i - 1][j - v] + w);
            else f[i][j] = f[i - 1][j];
    }
    
    cout << f[n][m];
}

二维写法
修改方法:

  1. 把第1维删去
  2. 如果当前状态由上一层状态得出, 体积循环就要倒着循环。因为f[j]可以由f[j - v]推导而来, 而j - v是小于j的,因此如果倒着循环的话,f[j - v]存放的还是上一次循环的值,因此是可行的。如果是正着循环,f[j] = f[j - v] + w, 而f[j - v]早就被这次循环的值覆盖了,因此不可行。
#include <iostream>

using namespace std;

const int N = 1010;

int f[N];

int n, m;

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int v, w;
        cin >> v >> w;
        for(int j = m; j >= v; j--)
            f[j] = max(f[j], f[j - v] + w);
    }
    
    cout << f[m];
}

完全背包

具体推导不写了, 很简单。把f[i][j - v]和f[i][j]的表达式比较一下即可。

很容易错的地方是:当背包放不下该物品的时候,f[i][j] = f[i - 1][j],优化成一维后这行就变成了恒等式,因此不用写。但是f[i][j] = max(f[i-1][j], f[i][j - v] + w)只有在书包放的下的情况下才成立,因此一定要加上if!!!

二维写法:

#include <iostream>

using namespace std;

const int N = 1010;

int f[N];

int n, m;

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int v, w;
        cin >> v >> w;
        for(int j = 0; j <= m; j++)
            if(j >= v)
                f[j] = max(f[j], f[j - v] + w);
    }
    
    cout << f[m];
}

这里要说一下一维写法,因为从一维写法才可以看出为什么完全背包可以正在循环

#include <iostream>

using namespace std;

const int N = 1010;

int f[N][N];

int n, m;

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int v, w;
        cin >> v >> w;
        for(int j = 0; j <= m; j++)
        {
        	无论背包体积够不够,f[i][j]都必须有可能等于f[i - 1][j],因此
        	可以这么写
            f[i][j] = f[i - 1][j];
            if(j >= v) f[i][j] = max(f[i][j], f[i][j - v] + w);
			
			当然这么写也是可以的,但是这么写就看不出来为什么优化成一维后
			体积可以正着循环
			if(j >= v) f[i][j] = max(f[i - 1][j], f[i][j - v] + w)
			else f[i][j] = f[i - 1][j]
        }
    }
    
    cout << f[n][m];
}

我们可以发现,在这种写法下,f[i][j] = max(f[i][j], f[i][j - v] + w), 状态的转移并不需要上一层的状态,因此可以正着循环。

多重背包

注意要加上if判断即可。

暴力写法:

#include <iostream>

using namespace std;

const int N = 110;

int n, m;

int f[N];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int v, w, s;
        cin >> v >> w >> s;
        for(int j = m; j >= 0; j--)
            for(int k = 0; k <= s; k++)
                if(k * v <= j)
                    f[j] = max(f[j], f[j - k * v] + k * w);
    }
    
    cout << f[m];
}

二进制优化写法:
原理:二进制 + 一个常数可以表示任意实数。比如10可以用1 2 4三个二进制 + 一个3来表示。又比如20可以用1 2 4 8 +一个5来表示。

因此多重背包,每一个物品的个数可以被拆成若干个01背包。比如该物品有20个,可以拆成{v, w}, {2v, 2w}, {4v, 4w}, {8v, 8w}, {5v, 5w}五个01背包。

因此把任意实数R写成1 + 2 + 4 + … + 2^(n - 1) + c
条件是:2^(n) - 1 < R

代码:

#include <iostream>

using namespace std;

const int N = 1010;

int f[N];


int n, m;

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int v, w, s;
        cin >> v >> w >> s;
        for(int k = 1; k <= s; k *= 2)
        {
        	k是当前01组的个数, 每次分配完一组,都可以开始计算dp了
        	每次分配完一组,都要将这组对应的个数减去,因此要s -= k
        	直到减到剩下物品没有办法凑成2的n次方了,证明到了最后一个数了
            for(int j = m; j >= k * v; j--)
                f[j] = max(f[j], f[j - k * v] + k * w);
            s -= k;
        }
        
        如果最后一个数不为0,证明任意常数c不为0,分为最后一组
        if(s)
        {
            for(int j = m; j >= s * v; j--)
                f[j] = max(f[j], f[j - s * v] + s * w);
        }
    }
    
    cout << f[m];
}

混合背包

混合背包指的是每一个物品个数可能只有1个,可能有无限个,可能有限个。
做法就是三种背包问题结合起来即可。

当然也可以把完全背包,01背包转换成多重背包。完全背包的物品个数等于总体积 / 单个物品的体积, 01背包的物品个数就是1.

代码:

#include <iostream>

using namespace std;

const int N = 1010;

int f[N];

int n, m;

int main()
{
    cin >> n >> m;
    for(int i = 0; i < n; i++)
    {
        int v, w, s;
        cin >> v >> w >> s;
        if(s == 0)
            for(int j = v; j <= m; j++)
                f[j] = max(f[j], f[j - v] + w);
        else
        {
            if(s == -1) s = 1;
            for(int k = 1; k <= s; k *= 2)
            {
                for(int j = m; j >= k * v; j--)
                    f[j] = max(f[j], f[j - k * v] + k * w);
                s -= k;
            }
            
            if(s)
            {
                for(int j = m; j >= s * v; j--)
                    f[j] = max(f[j], f[j - s * v] + s * w);
            }
        }
    }
    
    cout << f[m];
}

分组背包

分组背包就不要一边读一边计算dp了。先把组分好了再计算会比较清晰。

#include <iostream>

using namespace std;

const int N = 110;

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

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int ss;
        cin >> ss;
        for(int j = 1; j <= ss; j++)
        {
            int vv, ww;
            cin >> vv >> ww;
            v[i][j] = vv, w[i][j] = ww;
        }
        s[i] = ss;
    }
    
    
    
    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];
}

采药

采药
这是一道裸01背包问题。
ac代码:

#include <iostream>

using namespace std;

const int N = 1010;

int f[N];

int n, m;

int main()
{
    cin >> m >> n;
    for(int i = 1; i <= n; i++)
    {
        int v, w;
        cin >> v >> w;
        for(int j = m; j >= v; j--)
            f[j] = max(f[j], f[j - v] + w);
    }
    
    cout << f[m];
}

装箱问题

装箱问题
考点:01背包。题中要求剩余体积最小,也就是装入物品体积最大。我们可以把价值等同于体积来求01背包。

ac代码:

#include <iostream>

using namespace std;

int n, m;

const int N = 50;

int v[N], w[N], f[20010];

int main()
{
    cin >> m >> n;
    for(int i = 1; i <= n; i++) cin >> v[i], w[i] = v[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 << m - f[m];
}

宠物小精灵之收服

宠物小精灵之收服

考点:二维费用的01背包。这道题如果没有深刻理解01背包的状态定义就很容易错。
1.01背包的状态定义是在前i个物品里面, 背包体积不超过j的所有选法集合。这里要明确,背包体积不超过j有花费背包体积不超过j的意思。

2.花费的概念很重要, 因为这题最后要你求在价值最大的情况下,皮卡丘的剩余体力最大。剩余体力最大也就是花费体力最小,也就是j最小的情况。

3.还有一点:皮卡丘的体力不可以为0, 我之前犯过的错误就是写成了0 < j <= m, 然而这是不正确的。j代表的是花费的体积而不是剩余的体积。因此正确答案应该是0 <= j < m, j不能等于m。一旦等于m代表皮卡丘花费的体力为m,那么剩余体力就是0了。不符合题意。

#include <iostream>

using namespace std;

int n, m, k;

const int N = 1010, M = 510, K = 110;

int f[N][M];

int main()
{
    cin >> m >> k >> n;
    for(int i = 1; i <= n; i++)
    {
        int v1, v2;
        cin >> v1 >> v2;
        for(int u = m; u >= v1; u--)
            for(int j = k - 1; j >= v2; j--)
                f[u][j] = max(f[u][j], f[u - v1][j - v2] + 1);
    }
    
    cout << f[m][k - 1] << ' ';
    
    int res = 0;
   for(int i = 0; i <= k - 1; i++)
        if(f[m][i] == f[m][k - 1])
        {
            res = i;
            break;
        }
    cout << k - res;
}

数字组合

数字组合
考点:01背包求方案个数。也就是属性是count的情况
转移方程是:如果放得下的话:f[j] = f[j] + f[j - v]
初始化f[0] = 1, 因为要组合的数字是0的话,不需要任何数字都是一个方案。

ps:这种是恰好情况的01背包,之前做的大多数都是背包体积不超过j的01背包。之后还有背包体积不小于j的01背包。它们三个的状态转移方程是完全一样的,但是循环范围和初始化方式不同。且恰好情况一般求count,不超过情况一般求max,不小于情况一般求min

#include <iostream>

using namespace std;

const int M = 10010;

int f[M];

int n, m;

int main()
{
    cin >> n >> m;
    f[0] = 1;
    for(int i = 1; i <= n; i++)
    {
        int v;
        cin >> v;
        for(int j = m; j >= v; j--)
            f[j] = f[j] + f[j - v];
    }
    cout << f[m];
}

买书

买书
考点:完全背包求方案数量。本质还是count属性
在放的下的情况下:f[j] = f[j] + f[j - v]

#include <iostream>

using namespace std;

int v[5] = {0, 10, 20, 50, 100};

int n;

const int N = 1010;
int f[N];

int main()
{
    cin >> n;
    
    f[0] = 1;
    for(int i = 1; i <= 4; i++)
        for(int j = v[i]; j <= n; j++)
            f[j] = f[j] + f[j - v[i]];
            
    cout << f[n];
}       

货币系统

和买书是一模一样的题目。

#include <iostream>

using namespace std;

const int N = 3010;

long long f[N];

int n, m;

int main()
{
    cin >> n >> m;
    f[0] = 1;
    for(int i = 1; i <= n; i++)
    {
        int v;
        cin >> v;
        for(int j = v; j <= m; j++)
            f[j] += f[j - v];
    }
    
    cout << f[m];
}

庆功会

考点:二进制优化多重背包裸题。

#include <iostream>

using namespace std;
const int N = 6010;

int n, m;
int f[N];
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int v, w, s;
        cin >> v >> w >> s;
        for(int k = 1; k <= s; k *= 2)
        {
            for(int j = m; j >= k * v; j--)
                f[j] = max(f[j], f[j - k * v] + k * w);
            s -= k;
        }
        
        if(s)
        {
            for(int j = m; j >= s * v; j--)
                f[j] = max(f[j], f[j - s * v] + s * w);
        }
    }
    
    cout << f[m];
}

二维费用背包问题

和宠物小精灵那道题目差不多,考点都是二位费用的01背包问题。直接一维是体积,一维是重量即可。

#include <iostream>

using namespace std;

const int N = 110;

int f[N][N];

int n, m, k;

int main()
{
    cin >> n >> m >> k;
    for(int i = 1; i <= n; i++)
    {
        int v, w, ww;
        cin >> v >> w >> ww;
        for(int j = m; j >= v; j--)
            for(int u = k; u >= w; u--)
                f[j][u] = max(f[j][u], f[j - v][u - w] + ww);
    }
    
    cout << f[m][k];
}

潜水员

考点:求最小值的二维背包问题。
之前说过,不管是最小,最大,还是恰好,它们的转移方程都是一样的。但是循环范围和初始化不同。

有题意得状态定义是氧气体积不少于j, 氮气体积不少于k的所有选法。属性是min。
对于求min的背包问题,所有格子都要初始化成INF(不然求不了min)。f[0][0] = 0,因为氧气氮气体积不少于0,那么最小重量一定是0.

关于循环范围的问题。由于状态定义是体积不少于某个数,那么这个数若是负数的话,状态定义也是成立的。(不少于一个负数题目中所有情况都符合,但是负数索引会越界,因此不少于负数可以全部当成不少于0的状态,也就是最小重量为0)

在这里插入图片描述

循环范围: 求体积不超过j的时候,一定要注意放不下的情况,因此一般都是 j = m; j >= v; j–等等
求体积至少为j的时候,一定要注意负数是可行的,因此一般都是j = m; j >= 0; j–

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

const int N = 100;

int n, m, k;
int f[N][N];

int main()
{
    cin >> n >> m >> k;
    
    memset(f, 0x3f, sizeof f);
    f[0][0] = 0;
    for(int u = 1; u <= k; u++)
    {
        int v1, v2, w;
        cin >> v1 >> v2 >> w;
        for(int i = n; i >= 0; i--)
            for(int j = m; j >= 0; j--)
                f[i][j] = min(f[max(0, i - v1)][max(0, j - v2)] + w, f[i][j]);
    }
    
    cout << f[n][m];
}

机器分配

机器分配
考点:分组背包+输出具体选择的方案

分组背包不再赘述,讲一讲怎么输出背包问题的具体方案。

关键操作在于倒推。加入f[i][j] = f[i-1][j-v] + w,则证明我选了当前的物品。若f[i][j] = f[i -
1][j]则证明没有选当前物品。
ps:求背包具体方案不能优化空间,因为要保存所有状态信息。优化了只有一层的状态了。无法倒推。

还要注意一点,m>=v[i][k]才可以继续倒推,背包体积一定要放得下这个物品才行。

在这里插入图片描述

#include <iostream>
#include <iostream>

using namespace std;

const int N = 20;

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

int n, m;

int main()
{
    cin >> n >> m;
    
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= m; j++)
        {
            int ww;
            cin >> ww;
            v[i][j] = j, w[i][j] = ww;
        }
    }
    
    for(int i = 1; i <= n; i++)
        for(int j = 0; j <= m; j++)
            for(int k = 0; k <= m; k++)
                if(v[i][k] <= j)
                    f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
    
    cout << f[n][m] << endl;
    
    for(int i = n; i >= 1; i--)
        for(int k = 0; k <= m; k++)
        {
            if(m >= v[i][k] && f[i][m] == f[i - 1][m - v[i][k]] + w[i][k])
            {
                cout << i << ' ' << k << endl;
                m -= v[i][k];
                break;
            }
        }
    
    
}

求背包问题的具体方案(输出字符串字典序最小)

求背包问题的具体方案
考点:求背包问题的具体方案,方法还是倒推。

由于这次要求字典序最小,因此要从第一件物品开始输出,然而我们是要倒推的。因此我们要让f[1][1]是最终的结果状态。f[n][m]是最初的状态。

因此倒着循环计算dp即可。

易错点:该物品要放得下才可以继续倒推!!!背包问题里面不要老是忘记加这个条件!!!

#include <iostream>
using namespace std;

const int N = 1010;

int f[N][N], V[N], W[N];

int n, m;

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int v, w;
        cin >> v >> w;
        V[i] = v, W[i] = w;
    }
    
    
    for(int i = n; i >= 1; i--)
    {
        for(int j = m; j >= 0; 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]);
        }
    }
    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];
        }
    }
}

物品是不同策略的背包问题

金明的预算方案
考点:用二进制枚举所有情况转换成分组背包的问题。

这题说的是:要买附件就必须先买主件。因此可以将买主件和附件排列组合一下。
比如:如果有两个附件一个主件,可以组合成四种情况
在这里插入图片描述
这四种情况可以是一组背包,每一个决策只能选一次,因为只有一个主件。这样就转换成分组背包问题了。

我们发现,策略的个数是2^(附件数量).因为每一个附件都有选和不选两种情况。

所以可以用二进制枚举所有情况:下面给出代码模板:

for(int k = 0; k < 1 << xx.size(); k++)所有情况.比如2^2 = 4
{
    for(int u = 0; u < xx.size(); u++)每种情况的选择对象,这里是2
    {
        if(1 << u & k)如果这一位是0代表不选,是1代表选
        {
            xxxxxxxx
        }
    }
   	xxxxxxx
}

ac代码:

#include <iostream>
#include <vector>
using namespace std;

const int N = 32010, M = 100;

typedef pair<int, int> pii;

pii master[M];
vector<pii>servent[M];
vector<pii> group[M];
int f[N];

int n, m;

int main()
{
    cin >> m >> n;
    for(int i = 1; i <= n; i++)
    {
        int v, w, s;
        cin >> v >> w >> s;
        if(!s)
            master[i] = {v, v * w};
        else
            servent[s].push_back({v, v * w});
    }
    
    int cnt = 0;
    for(int i = 1; i <= n; i++)
    {
        if(master[i].first)
        {
            cnt++;
            for(int k = 0; k < 1 << servent[i].size(); k++)
            {
                int tmp_v = master[i].first, tmp_w = master[i].second;
                for(int u = 0; u < servent[i].size(); u++)
                {
                    if(1 << u & k)
                    {
                        tmp_v += servent[i][u].first;
                        tmp_w += servent[i][u].second;
                    }
                }
                group[cnt].push_back({tmp_v, tmp_w});
            }
        }
    }
    
    for(int i = 1; i <= cnt; i++)
        for(int j = m; j >= 0; j--)
            for(int k = 0; k < group[i].size(); k++)
                if(group[i][k].first <= j)
                    f[j] = max(f[j], f[j - group[i][k].first] + group[i][k].second);
    cout << f[m];
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值