算法基础-背包问题(01背包问题)

1.背包问题

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

  1. 01 背包问题 :每种物品只能选或不选(选 0 次或 1 次)。
  2. 完全背包问题 :每种物品可以选择⽆限次。
  3. 多重背包问题 :每种物品有数量限制。
  4. 分组背包问题 :物品被分为若⼲组,每组只能选⼀个物品。
  5. 混合背包 :以上四种背包问题混在⼀起。
  6. 多维费⽤的背包问题 :限定条件不⽌有体积,还会有其他因素(⽐如重量)。
    除了经典的总价值最⼤问题,还会有:
  7. ⽅案总数。
  8. 最优⽅案。
  9. ⽅案可⾏性。
  10. 输出具体⽅案。
    因此,背包问题种类⾮常繁多,题型⾮常丰富。但是,尽管背包有很多变形,都是从 01 背包问题演化
    过来的。所以,⼀定要把 01 背包问题学好。

01 背包

1.1 01 背包

题⽬来源: ⽜客⽹
题⽬链接: 【模板】01背包
难度系数: ★★

链接:https://ac.nowcoder.com/acm/problem/226514

来源:牛客网

【题⽬描述】
你有⼀个背包,最多能容纳的体积是 V
现在有 n 个物品,第 i 个物品的体积为 v i ,价值为 w i

  1. 求这个背包⾄多能装多⼤价值的物品?
  2. 若背包恰好装满,求⾄多能装多⼤价值的物品?
    【输⼊描述】
    第⼀⾏两个整数 nV ,表⽰物品个数和背包体积。
    接下来 n ⾏,每⾏两个数 v iw i ,表⽰第 i 个物品的体积和价值。
    1 ≤ n , V , v i , w i ≤ 1000
    【输出描述】
    输出有两⾏,第⼀⾏输出第⼀问的答案,第⼆⾏输出第⼆问的答案,如果⽆解请输出 0 。
    【⽰例⼀】
    输⼊:
    3 5
    2 10
    4 5
    1 4
    输出:
    14 9
    【⽰例⼆】
    输⼊:
    3 8
    12 6
    11 8
    6 8
    输出:
    8
    0

【解法】

我们先解决第⼀问:

  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 个物品中,挑选总体积不超过 jv [ i ] 的物品。
    此时 dp [ i ][ j ] = dp [ i − 1][ jv [ i ]] + w [ i ] 。但是这种状态不⼀定存在,因此需要特判⼀下。
    综上,状态转移⽅程为: dp [ i ][ j ] = max ( dp [ i − 1][ j ], dp [ i − 1][ jv [ i ]] + w [ i ]) 。
  3. 初始化:
    直接填表,第⼀⾏的 0 不影响结果。
  4. 填表顺序:
    根据「状态转移⽅程」,我们仅需「从上往下」填表即可。 接下来解决第⼆问:
    第⼆问仅需修改⼀下初始化以及最终结果即可。
  5. 初始化:
    因为有可能凑不⻬j 体积的物品,因此我们把不合法的状态设置为负⽆穷。这样在取最⼤值的时
    候,就不会考虑到这个位置的值。负⽆穷⼀般设置为−0 x 3 f 3 f 3 f 3 f 即可。
    然后把dp [0][0] = 0 修改成 0,因为这是⼀个合法的状态,最⼤价值是0 ,也让后续填表是正确
    的。
  6. 返回值:
    在最后拿结果的时候,也要判断⼀下最后⼀个位置是不是⼩于 0 ,因为有可能凑不⻬。
    不能判断是否等于−0 x 3 f 3 f 3 f 3 f ,因为这个位置的值会被更新,只不过之前的值太⼩,导致更
    新后还是⼩于0 的。

【参考代码】

cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;

// 常量定义:题目中n/V最大为1000,留10个余量
const int N = 1010;

int main() {
    // 1. 变量定义
    int n, m;          // n=物品数,m=背包最大容量(对应题目中的V)
    int v[N], w[N];    // v[i]=第i个物品体积,w[i]=第i个物品价值
    int f[N][N];       // 二维dp数组:f[i][j]表示前i个物品、体积≤j的最大价值

    // 2. 读取输入
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> v[i] >> w[i];
    }

    // ===================== 第一问:不要求装满 =====================
    // 初始化:二维数组默认值为0(未选物品时价值为0)
    // 遍历前i个物品
    for (int i = 1; i <= n; i++) {
        // 遍历所有可能的背包体积(0到m)
        for (int j = 0; j <= m; j++) {
            // 情况1:不选第i个物品 → 价值 = 前i-1个物品、体积j的价值
            f[i][j] = f[i-1][j];
            // 情况2:选第i个物品(前提:背包体积≥物品体积)
            if (j >= v[i]) {
                f[i][j] = max(f[i][j], f[i-1][j - v[i]] + w[i]);
            }
        }
    }
    // 输出第一问结果:前n个物品、体积≤m的最大价值
    cout << f[n][m] << endl;

    // ===================== 第二问:要求恰好装满 =====================
    // 初始化:将数组全部设为极小值(-0x3f3f3f3f),代表"无法装满该体积"
    memset(f, -0x3f, sizeof f);
    f[0][0] = 0;  // 唯一合法初始状态:前0个物品、体积0,价值0(恰好装满)

    // 遍历前i个物品(逻辑和第一问一致)
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= m; j++) {
            // 情况1:不选第i个物品
            f[i][j] = f[i-1][j];
            // 情况2:选第i个物品(前提:背包体积≥物品体积)
            if (j >= v[i]) {
                f[i][j] = max(f[i][j], f[i-1][j - v[i]] + w[i]);
            }
        }
    }

    // 结果判断:若f[n][m]为极小值,说明无法装满,输出0;否则输出f[n][m]
    if (f[n][m] < 0) {
        cout << 0 << endl;
    } else {
        cout << f[n][m] << endl;
    }

    return 0;
}

1.2 采药

题⽬来源: 洛⾕
题⽬链接: P1048 [NOIP2005 普及组] 采药
难度系数: ★

题目描述

辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:"孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。"

如果你是辰辰,你能完成这个任务吗?

输入格式

第一行有 2 个整数 T(1≤T≤1000)和 M(1≤M≤100),用一个空格隔开,T 代表总共能够用来采药的时间,M 代表山洞里的草药的数目。

接下来的 M 行每行包括两个在 1 到 100 之间(包括 1 和 100)的整数,分别表示采摘某株草药的时间和这株草药的价值。

输出格式

输出在规定的时间内可以采到的草药的最大总价值。

输入输出样例

输入 #1复制

复制代码
70 3
71 100
69 1
1 2

输出 #1复制

复制代码
3

说明/提示

【数据范围】

  • 对于 30% 的数据,M≤10;
  • 对于全部的数据,M≤100。

【题目来源】

NOIP 2005 普及组第三题


【解法】

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


【参考代码】

cpp 复制代码
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int t[N], w[N];
int f[N];
int main()
{
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;
}

1.3 ⼩ A 点菜

题⽬来源: 洛⾕
题⽬链接: P1164 ⼩A点菜
难度系数: ★★


题目背景

uim 神犇拿到了 uoi 的 ra(镭牌)后,立刻拉着基友小 A 到了一家......餐馆,很低端的那种。

uim 指着墙上的价目表(太低级了没有菜单),说:"随便点"。

题目描述

不过 uim 由于买了一些书,口袋里只剩 M 元 (0<M≤10000)。

餐馆虽低端,但是菜品种类不少,有 N 种 (1≤N≤100),第 i 种卖 ai​ 元 (0<ai​≤1000)。由于是很低端的餐馆,所以每种菜只有一份。

小 A 奉行"不把钱吃光不罢休"的原则,所以他点单一定刚好把 uim 身上所有钱花完。他想知道有多少种点菜方法。

由于小 A 肚子太饿,所以最多只能等待 1 秒。

输入格式

第一行两个整数 N 和 M,分别表示菜品种类和 uim 身上的钱数。

第二行 N 个正整数 ai​(可能有重复),用空格隔开,分别表示每种菜的价格。

输出格式

一个正整数,表示点菜方案数,保证答案的范围在 [0,231−1] 之内(不超过 C/C++的 int 范围)。

输入输出样例

输入 #1复制

复制代码
4 4
1 1 2 2

输出 #1复制

复制代码
3

说明/提示

2020.8.29,增添一组 hack 数据 by @yummy


【解法】

背包问题求⽅案数,稍微修改⼀个状态表⽰,然后根据具体问题分析状态转移⽅程和初始化即可。
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][ja [i]] 。
注意第⼆个状态可能不存在,要注意判断⼀下 ja [i]
3. 初始化:
dp [0][0] = 1,如果没有物品,想凑成总体积为 0是可⾏的,啥也不选就是⼀种⽅案。当然,这
个状态也是为了让后⾯的值是正确的。 其余位置的值是 0 就不影响填表的正确性。
4. 填表顺序:
从上往下每⼀⾏,每⼀⾏从左往右。
空间优化版本:每⼀⾏从右往左。


【参考代码】

cpp 复制代码
#include<iostream>
#include<cstring>
using namespace std;
const int V=1010;
int n, m;
int a[110];
int f[V];  // 一维数组:f[j]表示凑出体积j的方案数

int main() {
    cin >> n >> m;
    for(int i=1; i<=n; i++) cin >> a[i];
    
    f[0] = 1;  // 初始化:凑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;
}

3.1.4 Cow Frisbee Team

题⽬来源: 洛⾕
题⽬链接: P2946 [USACO09MAR] Cow Frisbee Team S
难度系数: ★★★


题目描述

老唐最近迷上了飞盘,约翰想和他一起玩,于是打算从他家的 N 头奶牛中选出一支队伍。

每只奶牛的能力为整数,第 i 头奶牛的能力为 Ri​。飞盘队的队员数量不能少于 1、大于 N。一支队伍的总能力就是所有队员能力的总和。

约翰比较迷信,他的幸运数字是 F,所以他要求队伍的总能力必须是 F 的倍数。请帮他算一下,符合这个要求的队伍组合有多少?由于这个数字很大,只要输出答案对 108 取模的值。

输入格式

第一行:两个用空格分开的整数:N 和 F。

第二行到 N+1 行:第 i+1 行有一个整数 Ri​,表示第 i 头奶牛的能力。

输出格式

第一行:单个整数,表示方案数对 108 取模的值。

输入输出样例

输入 #1复制

复制代码
4 5 
1 
2 
8 
2 

输出 #1复制

复制代码
3 

说明/提示

对于 100% 的数据,1≤N≤2000,1≤F≤1000,1≤Ri​≤105。


【解法】

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 ,只⽤再去前⾯凑⼀个 ja [ i ] % f 即可。但
是直接减可能会减出来⼀个负数,我们要把它补正,最终凑的数为 (ja [i ] % f + f ) % f
那么总⽅案数就是 dp [i − 1][(ja [i ] % f + f ) % f]
因为要的总⽅案数,所以状态转移⽅程就是上⾯两种情况的总和。
3. 初始化:
dp[0][0] = 1 :什么也不选的时候,总和是 0 ,余数也是 0 ,属于⼀种⽅案 ,也是为了后续填表
是正确的。
4. 填表顺序:
从上往下每⼀⾏,每⼀⾏从左往右。


【参考代码】

cpp 复制代码
#include <iostream>
using namespace std;

// 常量定义:
// N:奶牛数量上限(题目N≤2000,留10余量)
// M:F的上限(题目F≤1000,留10余量)
// MOD:取模值(10^8)
const int N = 2010, M = 1010, MOD = 1e8;

int main() {
    int n, F;  // n=奶牛数,F=幸运数字(避免用m,和模数混淆)
    int R[N];  // R[i]存储第i头奶牛的能力值
    // dp[i][j]:前i头奶牛中选,总和%F=j的组合数
    long long dp[N][M];  // 用long long避免中间结果溢出

    // 初始化dp数组为0(避免随机值干扰)
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < M; j++) {
            dp[i][j] = 0;
        }
    }

    // 1. 读取输入
    cin >> n >> F;
    for (int i = 1; i <= n; i++) {
        cin >> R[i];
        // 提前计算能力值的模F结果,减少后续计算量
        R[i] = R[i] % F;
    }

    // 2. 动态规划初始化:选0头奶牛,总和为0,模F=0,仅1种方案
    dp[0][0] = 1;

    // 3. 状态转移:遍历每头奶牛,计算所有余数的组合数
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j < F; j++) {
            // 情况1:不选第i头奶牛 → 组合数 = 前i-1头凑余数j的数量
            dp[i][j] = dp[i-1][j];

            // 情况2:选第i头奶牛 → 需前i-1头凑余数 (j - R[i]) mod F
            // 修正负数:(j - R[i] + F) % F 确保结果在0~F-1之间
            int pre_j = (j - R[i] + F) % F;
            dp[i][j] = (dp[i][j] + dp[i-1][pre_j]) % MOD;
        }
    }

    // 4. 计算结果:总组合数 - 选0头的无效方案(确保非负)
    long long ans = (dp[n][0] - 1 + MOD) % MOD;
    cout << ans << endl;

    return 0;
}
相关推荐
特立独行的猫a2 小时前
C++ Core Guidelines(C++核心准则):2025现代C++开发关键要点总结
c++·core guidelines·核心准测
蒙奇D索大2 小时前
【数据结构】考研408 | 伪随机探测与双重散列精讲:散列的艺术与均衡之道
数据结构·笔记·学习·考研
Joy-鬼魅2 小时前
VC中共享内存的命名空间
c++·vc·共享内存命名空间
我不是小upper2 小时前
从理论到代码:随机森林 + GBDT+LightGBM 融合建模解决回归问题
人工智能·深度学习·算法·随机森林·机器学习·回归
budingxiaomoli2 小时前
分治算法-快排
数据结构·算法
dragoooon342 小时前
[C++——lesson30.数据结构进阶——「红黑树」]
开发语言·数据结构·c++
云泽8082 小时前
C++ STL 栈与队列完全指南:从容器使用到算法实现
开发语言·c++·算法
前端小白在前进3 小时前
力扣刷题:复原IP地址
tcp/ip·算法·leetcode
历程里程碑3 小时前
C++ 17异常处理:高效捕获与精准修复
java·c语言·开发语言·jvm·c++