背包问题(01背包和无限背包)

假设有一个背包,体积是 V,另外有 n 个物品,物品的体积分别是 v1, v2, ... vn,每个物品的价值是 w1, w1, ... wn。求怎么将物品放到背包里,才能使背包中物品的价值最大 ?

背包问题是一个典型的动态规划问题。动态规划问题中经常包含一个最字,比如最大价值 ? 最短路径 ? 动态规划问题的求解思路包括以下几点:

(1)只看眼前利益

动态规划,关键字是动态,也就是说结果是在变化的。在计算过程中,只看眼前利益,只要当前这种情况满足要求,那么这就是中间的一个结果。

所有情况都遍历完之后的眼前利益就是最终想要的结果。

下边的代码是找数组的最大值。FindMax() 函数中,找最大值的时候,max 一直是已经遍历的数据的最大值,一直在更新,体现了只看眼前利益;max 也一直在更新,直到把数据都遍历完,max 就是最终的结果。这就是动态规划。

cpp 复制代码
#include <iostream>

int FindMax(int *data, int size) {
    int max = -1;
    for (int i = 0; i < 4; i++) {
        if (data[i] > max) {
            max = data[i];
        }
    }
    return max;
}

int main()
{
    int data[4] = {100, 200, 50, 10};
    std::cout << "max: " << FindMax(data, 4) << std::endl;
    return 0;
}

(2)选与不选

选与不选,就是分类讨论的思想。比如背包问题,当考虑一个物品时,要考虑两种情况,即这个物品放入背包的话,最终能放入的最大价值是多少;这个物品不放入背包的话,最终能放入的价值最大是多少。如果有 n 个物品,每个物品都要做这样的分类讨论,共有 2 的 n 次方种组合。把这些所有的情况的价值都计算出来,哪个组合的价值最大,那么这个组合就是最终的结果。

(3)记录历史信息

在动态规划的计算过程中,对一些情况的讨论往往会重复,在计算过程中可以记录历史信息,那么可以减小后边的重复计算。

背包问题分为两类:0-1 背包和无限背包。0-1 背包,说的是每个物品的数量只有一个, 也就是这个物品要么放进去,要么不放进去,只有两种情况。无限背包说的是每个物品的数量有无数个,每个物品都可以放 0 个,1 个或者多个。

数据遍历是很多算法的基础。

无论是排序算法,还是搜索算法或者动态规划。

算法的基础就是对一定数量的数据进行遍历,在遍历的过程中嵌入自己的算法逻辑,逻辑的不同就产生了了不同的算法。

数据保存在数据结构中,比如数组,链表,二叉树,图。每种数据结构都有自己的遍历方法。

1 0-1 背包

牛客网 01背包链接。

01背包

使用一维数组,时间复杂度是 O(n);使用选与不选的原始算法,时间复杂度是 O(2 的 n 次方)。所以优先选用以为数组。

1.1 一维数组

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

// 能够放下的最大价值
int MaxValue(std::vector<int> &v, std::vector<int> &w, int n, int V) {
    // 数组的长度是 V + 1
    // 之所以比背包的体积大 1,这样就最大可以使用 V 做数组的下标了,便于使用
    std::vector<int> value(V + 1);
    // 数组元素的值是这个体积下能放下的价值,初始化为 0
    value.assign(V + 1, 0);
    // 两层循环,第一层循环遍历物品
    for (int i = 0; i < n; i++) {
        // 第二层循环遍历背包剩余的空间,能放下当前这个物品的空间
        // 体积从大到小进行遍历
        for (int j = V; j >= v[i]; j--) {
            // value[j] 是没放这个物品的时候,背包在 j 这个体积下的价值
            // value[j - v[i]] + w[i] 是放下这个物品的时候,j 体积下的价值
            if (value[j - v[i]] + w[i] > value[j]) {
                value[j] = value[j - v[i]] + w[i];
            }
        }
    }
    return value[V];
}

// 背包正好装满时的最大价值
int FullMaxValue(std::vector<int> &v, std::vector<int> &w, int n, int V) {
    std::vector<int> value(V + 1);
    // 与背包能放下的最大值比较的话
    // 初始值是不一样的
    // 将 value[0] 初始化为 0, 其它的元素初始化为一个非常小的数
    // 这个非常小的数要保证物品价值都加起来和这个数相加,也不会大于 0
    // 这样能保证正好装满的时候,value[V] 是大于 0 的
    // 最后可以通过 value[V] 是不是大于 0 来判断背包是不是可以正好装满
    // 如果能正好装满,那么 value[V] 价值是在 value[0] 也就是 0 的基础上加上物品的价值
    // 所以,value[V] 是大于 0 的。
    // 如果不能正好装满,那么 value[V] 的价值,在计算过程中,肯定与一个非常小的数进行了相加
    // 所以 value[V] 是小于 0 的
    value.assign(V + 1, -99999999);
    value[0] = 0;
    
    for (int i = 0; i < n; i++) {
        int tmp_v = v[i];
        int tmp_w = w[i];
        for (int j = V; j >= tmp_v; j--) {
            int value_old = value[j];
            int value_new = value[j - tmp_v] + tmp_w;
            if (value_new > value_old) {
                value[j] = value_new;
            }
        }
    }
    if (value[V] < 0) {
        return 0;
    }
    return value[V];
}

int main()
{
    int n = 0;
    int V = 0;
    std::vector<int> v;
    std::vector<int> w;
    cin >> n >> V;
    for (int i = 0; i < n; i++) {
        int a = 0;
        int b = 0;
        cin >> a >> b;
        v.push_back(a);
        w.push_back(b);
    }

    std::cout << MaxValue(v, w, n, V) << std::endl;
    std::cout << FullMaxValue(v, w, n, V);
    return 0;
}

1.2 选与不选

选与不选使用递归算法。递归算法的时间复杂度是O(2的 n 次方),时间复杂度太大,在牛客网上运行经常超时。使用一维数组的方式,时间复杂度是 O(n),所以有限选择数组的方式。

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

// 保存背包能放得下的最大价值
int max_value = 0;
// 保存背包正好放满时的最大价值
int max_full_value = 0;
// 已放入的物品的价值
int value = 0;
// 已放入的物品的体积
int volume = 0;
void MaxValue(std::vector<int>& v, std::vector<int>& w, int n, int V,
              int index) {
    if (volume > V) {
      return;
    }
    if (index == n) {
        if (volume <= V && value > max_value) {
            max_value = value;
        }

        if (volume == V && value > max_full_value) {
            max_full_value = value;
        }
        return;
    }
    for (int i = index; i < n; i++) {
        // 选择这个物品
        volume += v[i];
        value += w[i];
        MaxValue(v, w, n, V, i + 1);
        // 不选择这个物品
        volume -= v[i];
        value -= w[i];
        MaxValue(v, w, n, V, i + 1);
    }
}

int main() {
    int n = 0;
    int V = 0;
    std::vector<int> v;
    std::vector<int> w;
    cin >> n >> V;
    for (int i = 0; i < n; i++) {
        int a = 0;
        int b = 0;
        cin >> a >> b;
        v.push_back(a);
        w.push_back(b);
    }

    MaxValue(v, w, n, V, 0);
    std::cout << max_value << std::endl;
    std::cout << max_full_value;
    return 0;
}

2 无限背包

无限背包也叫完全背包,牛客网链接如下。

完全背包

无限背包,说的是每个物品的个数都有无限个。可以使用一维数组的方式来求解,与 01 背包不同的是,在遍历体积的时候,需要从小到大进行遍历,01 背包是从大到小进行遍历的

为什么从小到大进行遍历呢,这样对于一个物品可以遍历到放置个的情况。比如一个背包的体积是 10,一个物品的体积是 2。如果从小到大进行遍历,那么只放这个物品的话,可以放置 5 个这样的物品,体积遍历到 2 的时候,可以放一个,4 的时候可以再放一个,以此类推。如果从大到小进行遍历,从 10 遍历到 2,那么只能放置一个,不能在前边放置的基础之上,再次进行放置。

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

int MaxValue(std::vector<int> &v, std::vector<int> &w, int n, int V) {
    std::vector<int> value(V + 1);
    value.assign(V + 1, 0);
    for (int i = 0; i < n; i++) {
        int tmp_v = v[i];
        int tmp_w = w[i];
        for (int j = tmp_v; j <= V; j++) {
            int value_old = value[j];
            int value_new = value[j - tmp_v] + tmp_w;
            if (value_new > value_old) {
                value[j] = value_new;
            }
        }
    }
    return value[V];
}

int BagFullMaxValue(std::vector<int> &v, std::vector<int> &w, int n, int V) {
    std::vector<int> value(V + 1);
    value.assign(V + 1, -99999999);
    value[0] = 0;
    for (int i = 0; i < n; i++) {
        int tmp_v = v[i];
        int tmp_w = w[i];
        for (int j = tmp_v; j <= V; j++) {
            int value_old = value[j];
            int value_new = value[j - tmp_v] + tmp_w;
            if (value_new > value_old) {
                value[j] = value_new;
            }
        }
    }
    if (value[V] < 0) {
        return 0;
    }
    return value[V];
}

int main() {
    int n = 0;
    int V = 0;
    std::vector<int> v;
    std::vector<int> w;
    cin >> n >> V;
    for (int i = 0; i < n; i++) {
        int a = 0;
        int b = 0;
        cin >> a >> b;
        v.push_back(a);
        w.push_back(b);
    }
    
    int max_value = MaxValue(v, w, n, V);
    int bag_full_max_value = BagFullMaxValue(v, w, n, V);
    std::cout << max_value << std::endl;
    std::cout << bag_full_max_value << std::endl; 
}
相关推荐
scx201310042 分钟前
20251214 字典树总结
算法·字典树
leiming65 分钟前
MobileNetV4 (MNv4)
开发语言·算法
YGGP19 分钟前
【Golang】LeetCode 136. 只出现一次的数字
算法·leetcode
YGGP27 分钟前
【Golang】LeetCode 169. 多数元素
算法·leetcode
顾安r30 分钟前
11.20 脚本网页 数学分支
算法·数学建模·html
少许极端34 分钟前
算法奇妙屋(二十)-回文子串/子序列问题(动态规划)
java·算法·动态规划·图解·回文串·回文序列
天赐学c语言42 分钟前
12.20 - 反转链表II && 传值和传地址的区别
数据结构·c++·算法·链表·leecode
如意鼠43 分钟前
大模型教我成为大模型算法工程师之day20: 预训练语言模型 (Pre-trained Language Models)
人工智能·算法·语言模型
_OP_CHEN43 分钟前
【算法基础篇】(三十六)图论基础之拓扑排序:从原理到实战,搞定 DAG 图的 “先后次序” 难题
c++·算法·蓝桥杯·图论·拓扑排序·算法竞赛·acm/icpc
良木生香1 小时前
【诗句结构-初阶】详解栈和队列(2)---队列
c语言·数据结构·算法·蓝桥杯