NO.86十六届蓝桥杯备战|动态规划-01背包|采药|小A点菜|Cow Frisbee Team(C++)

背包问题是动态规划中最经典的问题,很多题⽬或多或少都有背包问题的影⼦。它的基本形式是:给定⼀组物品,每个物品有体积和价值,在不超过背包容量的情况下,选择物品使得总价值最⼤。
背包问题有多种变体,主要包括:

  1. 01背包问题:每种物品只能选或不选(选0次或1次)。
  2. 完全背包问题:每种物品可以选择⽆限次。
  3. 多重背包问题:每种物品有数量限制。
  4. 分组背包问题:物品被分为若⼲组,每组只能选⼀个物品。
  5. 混合背包:以上四种背包问题混在⼀起。
  6. 多维费⽤的背包问题:限定条件不⽌有体积,还会有其他因素(⽐如重量)。

除了经典的总价值最⼤问题,还会有:

  1. ⽅案总数。
  2. 最优⽅案。
  3. ⽅案可⾏性。
  4. 输出具体⽅案。
01背包

我们先解决第⼀问:

  1. 状态表⽰:
    dp[i][j]表⽰:从前i个物品中挑选,总体积「不超过」j,所有的选法中,能挑选出来的最⼤
    价值。
    那么dp[n][v]就是我们要的结果。
  2. 状态转移⽅程:
    线性dp 状态转移⽅程分析⽅式,⼀般都是根据「最后⼀步」的状况,来分情况讨论:
    a. 不选第i个物品:相当于就是去前i-1个物品中挑选,并且总体积不超过j。此时dp[i][j] = dp[i - 1][j]
    b. 选择第i个物品:那么我就只能去前i-1个物品中,挑选总体积不超过j-v[i]的物品。
    此时dp[i][j] = dp[i - 1][j - v[i]] + w[i]。但是这种状态不⼀定存在,因此需要特判⼀下。
    综上,状态转移⽅程为:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i])
  3. 初始化:
    直接填表,第⼀⾏的0 不影响结果。
  4. 填表顺序:
    根据「状态转移⽅程」,我们仅需「从上往下」填表即可
#include <bits/stdc++.h>
using namespace std;

const int N = 1010;

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

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    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][j], f[i-1][j-v[i]] + w[i]);
            }
        }
    }
    cout << f[n][m] << endl;
    
    return 0;
}

接下来解决第⼆问:
第⼆问仅需修改⼀下初始化以及最终结果即可。

  1. 初始化:
    因为有可能凑不⻬j体积的物品,因此我们把不合法的状态设置为负⽆穷。这样在取最⼤值的时候,就不会考虑到这个位置的值。负⽆穷⼀般设置为-0x3f3f3ff3即可。
    然后把dp[0][0] = 0修改成0,因为这是⼀个合法的状态,最⼤价值是0 ,也让后续填表是正确
    的。
  2. 返回值:
    在最后拿结果的时候,也要判断⼀下最后⼀个位置是不是⼩于0 ,因为有可能凑不⻬。
    不能判断是否等于-0x3f3f3f3f,因为这个位置的值会被更新,只不过之前的值太⼩,导致更
    新后还是⼩于0的
#include <bits/stdc++.h>
using namespace std;

const int N = 1010;

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

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    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][j], f[i-1][j-v[i]] + w[i]);
            }
        }
    }
    cout << f[n][m] << endl;
    
    memset(f, -0x3f, sizeof f);
    f[0][0] = 0;
    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]);
            }
        }
    }
    if (f[n][m] < 0) cout << 0 << endl;
    else cout << f[n][m] << endl;
    
    return 0;
}

空间优化:

  1. 考虑是否要修改遍历顺序
  2. 直接在原始代码上,删掉第一维即可
#include <bits/stdc++.h>
using namespace std;

const int N = 1010;

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

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    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 = m; j >= v[i]; j--)
        {
            f[j] = max(f[j], f[j-v[i]] + w[i]);
        }
    }
    cout << f[m] << endl;
    
    memset(f, -0x3f, sizeof f);
    f[0] = 0;
    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]);
        }
    }
    if (f[m] < 0) cout << 0 << endl;
    else cout << f[m] << endl;
    
    return 0;
}
P1048 [NOIP 2005 普及组] 采药 - 洛谷

基本01 背包问题,将时间看成体积,就是标准的不放满的01 背包问题

#include <bits/stdc++.h>
using namespace std;

const int N = 1010;

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

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> m >> n;
    for (int i = 1; i <= n; i++) cin >> t[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 >= t[i]) f[i][j] = max(f[i][j], f[i-1][j-t[i]] + w[i]);
        }
    }
    cout << f[n][m] << endl;
    
    return 0;
}

空间优化:

#include <bits/stdc++.h>
using namespace std;

const int N = 1010;

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

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> m >> n;
    for (int i = 1; i <= n; i++) cin >> t[i] >> w[i];

    for (int i = 1; i <= n; i++)
    {
        for (int j = m; j >= t[i]; j--)        
        {
            f[j] = max(f[j], f[j-t[i]] + w[i]);
        }
    }
    cout << f[m] << endl;
    
    return 0;
}
P1164 小A点菜 - 洛谷

背包问题求⽅案数,稍微修改⼀个状态表⽰,然后根据具体问题分析状态转移⽅程和初始化即可。

  1. 状态表⽰:
    dp[i][j]表⽰:从前i 个菜中挑选,总价钱恰好等于j ,此时的总⽅案数。
  2. 状态转移⽅程:
    针对a[i]选或者不选,分两种情况讨论:
    a. 如果不选a[i]:相当于去前i-1个菜中挑选,总价钱恰好为j的⽅案数,此时的⽅案数就
    dp[i - 1][j]
    b. 如果选a[i]:那么应该去前i-1个菜中挑选,总价值恰好为j-a[i],此时的⽅案数就是dp[i - 1][j - a[i]]
    因为我们要的是总⽅案数,于是dp[i][j] = dp[i - 1][j] + dp[i - 1][j - a[i]]
    注意第⼆个状态可能不存在,要注意判断⼀下j ≥ a[i]
  3. 初始化:
    dp[0][0],如果没有物品,想凑成总体积为0是可⾏的,啥也不选就是⼀种⽅案。当然,这个状态也是为了让后⾯的值是正确的。
    其余位置的值是0 就不影响填表的正确性。
  4. 填表顺序:
    从上往下每⼀⾏,每⼀⾏从左往右。
    空间优化版本:每⼀⾏从右往左
#include <bits/stdc++.h>
using namespace std;

const int N = 110, M = 10010;

int n, m;
int a[N];
int f[N][M];

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i];

    f[0][0] = 1;
    for (int i = 1; i <= n; i++)
    {
        for (int j = m; j >= 0; j--)
        {
            f[i][j] = f[i-1][j];
            if (j >= a[i]) f[i][j] += f[i-1][j-a[i]];
        }
    }
    cout << f[n][m] << endl;
    
    return 0;
}

空间优化:

#include <bits/stdc++.h>
using namespace std;

const int N = 110, M = 10010;

int n, m;
int a[N];
int f[M];

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);

    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] << endl;
    
    return 0;
}
P2946 [USACO09MAR] Cow Frisbee Team S - 洛谷

01 背包问题变形。

  1. 状态表⽰:
    dp[i][j]表⽰:从前i 头奶⽜中挑选,总和模f 之后为j 时,⼀共有多少种组合。
    那么dp[n][0] - 1就是最终结果。(因为动态规划会把全都不选这种情况也考虑进去,所以要剪掉)
  2. 状态转移⽅程:
    对于第i 头奶⽜选或者不选,可以分为两种情况讨论:
    a. 如果不选a[i]:此时的总⽅案数就是去[1, i - 1]⾥⾯凑余数正好是j ,也就是dp[i - 1][j]
    b. 如果选a[i] :此时已经有⼀个余数为a[i] % f,只⽤再去前⾯凑⼀个j - a[i] % f即可。但是直接减可能会减出来⼀个负数,我们要把它补正,最终凑的数为(j - a[i] % f + f) % f
    那么总⽅案数就是dp[i - 1][(j - a[i] % f + f) % f]
    因为要的总⽅案数,所以状态转移⽅程就是上⾯两种情况的总和。
  3. 初始化:
    dp[0][0] = 1:什么也不选的时候,总和是0 ,余数也是0 ,属于⼀种⽅案,也是为了后续填表是正确的。
  4. 填表顺序:
    从上往下每⼀⾏,每⼀⾏从左往右
#include <bits/stdc++.h>
using namespace std;

const int N = 2010, M = 1010, MOD = 1e8;

int n, m;
int a[N];
int f[N][M];

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i];

    f[0][0] = 1;
    for (int i = 1; i <= n; i++)
    {
        for (int j = 0; j < m; j++)
        {
            f[i][j] = (f[i-1][j] + f[i-1][((j - a[i] % m)%m +m) %m]) %MOD;        
        }
    }
    cout << f[n][0] - 1 << endl;
    
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值