动态规划详解背包问题及实践(附C++代码)

简介: 背包问题是一个经典的组合优化问题,它可以被抽象为一个把物品放入背包中的过程,以求最终背包中物品价值的最大化

一、 简介

1. 背包问题是什么

背包问题是一个经典的组合优化问题,它可以被抽象为一个把物品放入背包中的过程,以求最终背包中物品价值的最大化。

2. 背包问题的分类

常见的背包问题主要分为以下三种:

  1. 01背包问题:每种物品最多只能装一次。
  2. 完全背包问题:每种物品可以无限次装入背包中。
  3. 多重背包问题:每种物品有限制次数可以装入背包中。

二、 0/1背包问题定义

1. 0/1背包问题的定义

0/1背包问题是一个经典的动态规划问题,指的是有n个物品和一个容量为W的背包,每个物品只能选择装入一次或不装入。在装入背包的物品总重量不能超过W的前提下,选择物品装入背包中,使得背包中物品的总价值最大。

2. 动态规划算法解决0/1背包问题

对于0/1背包问题,我们可以采用动态规划算法来解决。具体的解决思路如下:

  • 定义状态:用dp[i][j]表示前i个物品,容量为j时的最大价值。
  • 状态转移方程:对于第i个物品,我们有两种选择,可以选择将其放入背包中,也可以选择不将其放入背包中。因此有如下状态转移方程:

dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]] + v[i])

其中w[i]是第i个物品的重量,v[i]是第i个物品的价值。

通过上述状态转移方程,我们可以求出前i个物品,容量为j时的最大价值。最后返回dp[n][W]即可。

3. 代码示例

下面是一个基于动态规划的0/1背包问题的代码实现

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

int knapsack(vector<int>& w, vector<int>& v, int W) {
   
    int n = w.size();
    vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0)); // 定义状态

    for(int i = 1; i <= n; i++) {
   
        for(int j = 1; j <= W; j++) {
   
            if(j < w[i - 1]) {
    // 当前物品不能装入背包
                dp[i][j] = dp[i - 1][j];
            } else {
    // 当前物品可以装入背包
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i - 1]] + v[i - 1]); // 状态转移方程
            }
        }
    }

    return dp[n][W]; // 返回前n个物品,容量为W时的最大价值
}

int main() {
   
    vector<int> w = {
   2, 3, 4}; // 物品重量
    vector<int> v = {
   4, 5, 6}; // 物品价值
    int W = 5; // 背包容量

    cout << "The maximum value is: " << knapsack(w, v, W) << endl; // 输出最大价值

    return 0;
}

通过动态规划算法,该程序可以计算最大价值并输出,从而解决0/1背包问题。

三、 完全背包问题

1. 完全背包问题的定义

完全背包问题是一个经典的动态规划问题,在0/1背包问题的基础上进行了扩展,它指的是有n个物品和一个容量为W的背包,每个物品可以选择装入多次,装入物品的总重量不能超过背包容量W,目标是装入的物品总价值最大

2. 动态规划算法解决完全背包问题

对于完全背包问题,我们同样可以采用动态规划算法来解决。与0/1背包问题不同的是,在完全背包问题中,每一个物品可以选择装入多次,因此需要重新定义状态和转移方程。

  • 定义状态:用dp[i][j]表示前i个物品,容量为j时的最大价值。
  • 状态转移方程:对于第i个物品,我们可以选择装入它或不装入它。如果选择装入它,则背包容量会减少w[i-1],同时总价值加上v[i-1]。因此有如下状态转移方程:

dp[i][j] = max(dp[i-1][j], dp[i][j-w[i-1]] + v[i-1])

其中w[i-1]是第i个物品的重量,v[i-1]是第i个物品的价值。

需要注意的是,对于完全背包问题,状态转移方程中的dp[i - 1][j - w[i - 1]]已经变为了dp[i][j - w[i - 1]],因为每个物品可以装入多次。

3. 代码示例

下面是一个基于动态规划的完全背包问题的代码实现

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

int knapsack(vector<int>& w, vector<int>& v, int W) {
   
    int n = w.size();
    vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0)); // 定义状态

    for(int i = 1; i <= n; i++) {
   
        for(int j = 1; j <= W; j++) {
   
            if(j < w[i - 1]) {
    // 当前物品不能装入背包
                dp[i][j] = dp[i - 1][j];
            } else {
    // 当前物品可以装入背包
                dp[i][j] = max(dp[i - 1][j], dp[i][j - w[i - 1]] + v[i - 1]); // 状态转移方程
            }
        }
    }

    return dp[n][W]; // 返回前n个物品,容量为W时的最大价值
}

int main() {
   
    vector<int> w = {
   2, 3, 4}; // 物品重量
    vector<int> v = {
   4, 5, 6}; // 物品价值
    int W = 5; // 背包容量

    cout << "The maximum value is: " << knapsack(w, v, W) << endl; // 输出最大价值

    return 0;
}

通过动态规划算法,该程序可以计算最大价值并输出,从而解决完全背包问题。

四、 多重背包问题

1. 多重背包问题的定义

多重背包问题同样是一个经典的动态规划问题,它指的是有n个物品和一个容量为W的背包,每个物品有对应的数量限制和重量以及价值,物品j最多能装k次,装入物品的总重量不能超过背包容量W,目标是装入的物品总价值最大。

2. 动态规划算法解决多重背包问题

与完全背包问题类似,多重背包问题也可以采用动态规划算法来解决。但是需要注意的是,与完全背包问题不同的是,多重背包问题中每个物品有着数量限制,所以需要重新定义状态和转移方程。

  • 定义状态:用dp[i][j]表示前i个物品,容量为j时的最大价值。
  • 状态转移方程:对于第i个物品,我们可以选择装入它或不装入它。如果选择装入它,需要考虑它的数量限制k[i-1],背包容量会减少多个w[i-1],同时总价值加上多个v[i-1]。因此有如下状态转移方程:

dp[i][j] = max(dp[i-1][j], dp[i-1][j-k[i-1]w[i-1]]+k[i-1]v[i-1], dp[i-1][j-2k[i-1]w[i-1]]+2k[i-1]v[i-1], ...)

其中k[i-1]是第i个物品的数量限制,w[i-1]是第i个物品的重量,v[i-1]是第i个物品的价值。

需要注意的是,转移方程中的数量部分需要列举每一种可能的数量情况,才能求出最大价值。

3. 代码示例

下面是一个基于动态规划的多重背包问题的代码实现

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

int knapsack(vector<int>& w, vector<int>& v, vector<int>& k, int W) {
   
    int n = w.size();
    vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0)); // 定义状态

    for(int i = 1; i <= n; i++) {
   
        for(int j = 1; j <= W; j++) {
   
            for(int t = 0; t <= k[i - 1] && t*w[i - 1] <= j; t++) {
    // 列举每一种可能的数量情况
                dp[i][j] = max(dp[i][j], dp[i - 1][j - t*w[i - 1]] + t*v[i - 1]); // 状态转移方程
            }
        }
    }

    return dp[n][W]; // 返回前n个物品,容量为W时的最大价值
}

int main() {
   
    vector<int> w = {
   2, 3, 4}; // 物品重量
    vector<int> v = {
   4, 5, 6}; // 物品价值
    vector<int> k = {
   2, 3, 1}; // 物品数量限制
    int W = 10; // 背包容量

    cout << "The maximum value is: " << knapsack(w, v, k, W) << endl; // 输出最大价值

    return 0;
}

通过动态规划算法,该程序可以计算最大价值并输出,从而解决多重背包问题。

五、 混合背包问题

1. 混合背包问题的定义

混合背包问题是指有n个物品和容量为W的背包,每个物品有对应的价值和重量以及选择方式(0/1、多重、完全),要求在装入物品的总重量不超过背包容量的前提下,将背包的总价值最大化。

2. 动态规划算法解决混合背包问题

混合背包问题同时拥有0/1、多重和完全三种选择方式,而前两者的解法已经在前面的问题中讨论过。因此,我们只需考虑如何通过动态规划算法解决混合背包问题中的完全背包问题。

  • 定义状态:用dp[i][j]表示前i个物品,容量为j时的最大价值。
  • 状态转移方程:对于第i个物品,若采用完全背包的方式装入背包,则有如下状态转移方程:

dp[i][j] = max(dp[i][j], dp[i-1][j-kw[i-1]]+kv[i-1])

其中k为第i个物品的选择数量,w[i-1]为第i个物品的重量,v[i-1]为第i个物品的价值。

需要注意的是,完全背包问题中采用的是无限制的选取,因此我们只需在状态转移方程中将k从1到j/w[i-1]的所有情况都遍历一遍即可。

  • 同样需要记得将前面两种选择方式的结果进行合并。

3. 代码示例

下面是一个基于动态规划的混合背包问题的代码实现

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

int knapsack(vector<int>& w, vector<int>& v, vector<int>& c, int W) {
   
    int n = w.size();
    vector<int> dp(W + 1, 0); // 定义状态,此处只需要开一个一维数组,因为混合背包只需要合并前两种选择方式的结果即可

    for(int i = 1; i <= n; i++) {
   
        if(c[i - 1] == 0) {
    // 0/1选择方式
            for(int j = W; j >= w[i - 1]; j--) {
   
                dp[j] = max(dp[j], dp[j - w[i - 1]] + v[i - 1]);
            }
        } else if(c[i - 1] == 1) {
    // 多重选择方式
            for(int j = w[i - 1]; j <= W; j++) {
   
                dp[j] = max(dp[j], dp[j - w[i - 1]] + v[i - 1]);
            }
        } else {
    // 完全选择方式
            for(int j = 1; j <= W; j++) {
   
                for(int k = 1; k <= j / w[i - 1]; k++) {
    // 遍历每一个可能的数量情况
                    dp[j] = max(dp[j], dp[j - k * w[i - 1]] + k * v[i - 1]);
                }
            }
        }
    }

    return dp[W]; // 返回前n个物品,容量为W时的最大价值
}

int main() {
   
    vector<int> w = {
   2, 3, 4}; // 物品重量
    vector<int> v = {
   4, 5, 6}; // 物品价值
    vector<int> c = {
   0, 1, 2}; // 物品选择方式
    int W = 10; // 背包容量

    cout << "The maximum value is: " << knapsack(w, v, c, W) << endl; // 输出最大价值

    return 0;
}

通过动态规划算法,该程序可以计算最大价值并输出,从而解决混合背包问题。

六、c++ 背包问题的优化

1. 背包问题的常见优化策略

在实际应用中,我们通常需要通过优化背包问题的算法以降低时间和空间复杂度,提高程序运行效率。以下是背包问题的常见优化策略:

  • 优化状态转移方程:

在计算某个状态时,我们可以通过前面已经计算出来的状态来简化目标状态的计算。例如,我们可以将0/1背包问题的状态转移方程优化为:

dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i-1]] + v[i-1])

即将原来的dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i-1]] + v[i-1], dp[i-2][j], dp[i-2][j-w[i-1]] + v[i-1]...)转换为只考虑前一行dp[i-1][ ]的值。

  • 优化空间复杂度:

使用滚动数组可以将背包问题中的二维数组降为一维数组,从而只需要开O(W)的空间。

  • 优化搜索顺序:

将物品按照单位重量价值从大到小排序,可以让后面的物品更有可能被选择,从而避免搜索过多无用的状态。

2. 代码示例

下面是一个基于优化策略的背包问题的代码实现

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

// 滚动数组版本的0/1背包问题
int knapsack(vector<int>& w, vector<int>& v, int W) {
   
    int n = w.size();
    vector<int> dp(W + 1, 0);

    for(int i = 1; i <= n; i++) {
   
        for(int j = W; j >= w[i - 1]; j--) {
   
            dp[j] = max(dp[j], dp[j - w[i - 1]] + v[i - 1]);
        }
    }

    return dp[W];
}

// 根据单位重量价值排序后的0/1背包问题
struct Item {
   
    int w, v;
    double unitValue; // 单位重量价值
};

bool cmp(Item a, Item b) {
   
    return a.unitValue > b.unitValue; // 按照单位重量价值从大到小排序
}

int knapsack2(vector<Item>& items, int W) {
   
    int n = items.size();
    sort(items.begin(), items.end(), cmp);
    vector<int> dp(W + 1, 0);

    for(int i = 0; i < n; i++) {
   
        for(int j = W; j >= items[i].w; j--) {
   
            dp[j] = max(dp[j], dp[j - items[i].w] + items[i].v);
        }
    }

    return dp[W];
}

int main() {
   
    vector<int> w = {
   2, 3, 4}; // 物品重量
    vector<int> v = {
   4, 5, 6}; // 物品价值
    int W = 5; // 背包容量

    cout << "The maximum value is: " << knapsack(w, v, W) << endl; // 输出最大价值

    vector<Item> items = {
   {
   2, 4}, {
   3, 5}, {
   4, 6}};
    cout << "The maximum value is: " << knapsack2(items, W) << endl; // 输出最大价值

    return 0;
}

使用滚动数组优化后的0/1背包问题和通过排序优化后的0/1背包问题分别被实现,并输出了最大价值。

七、背包问题的应用

1. 背包问题在实际生活中的应用

背包问题是一个经典的组合优化问题,可以用于很多实际生活场景中,例如:

  • 快递员在派送物品时,需要用最少的次数将所有物品全部送出去,即可把问题转化为背包问题。
  • 旅行者要将一定数量的行李装在行李箱或背包里,而行李箱或背包有一个特定的容量,旅行者需要在有限的容量内带走价值最高的行李。
  • 设计带宽优化问题,选择一定数量的数据包来传输,使得总价值最大,但不超过宽带承载能力。

2. 代码示例

下面是一个背包问题的代码示例

def knapsack(weights, values, capacity):
    """
    使用动态规划思想解决背包问题
    :param weights: 物品重量列表
    :param values: 物品价值列表
    :param capacity: 背包容量
    :return: 背包装物品的最大价值
    """
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        for j in range(1, capacity + 1):
            if j < weights[i - 1]:
                dp[i][j] = dp[i - 1][j]
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1])
    return dp[n][capacity]


if __name__ == '__main__':
    weights = [2, 3, 4]  # 物品重量
    values = [4, 5, 6]  # 物品价值
    capacity = 5  # 背包容量
    print("背包最大价值为:", knapsack(weights, values, capacity))

以上代码使用了动态规划的思想解决了背包问题,通过构建dp数组记录状态转移结果。在实现过程中,根据物品重量和背包容量的大小进行判断,得出最大价值。

八、 背包问题结语

1. 背包问题的意义和价值

背包问题是一类经典的组合优化问题,其在工业、经济、科学和技术等各个方面有着广泛的应用。一个更贴切生活的例子是,假设你有一个最大容量为 $V$ 的背包,有 $n$ 个物品,第 $i$ 个物品的重量为 $w_i$,价值为 $v_i$。现在你要从这 $n$ 个物品中选择一部分放入背包中,使得这些物品的总重量不超过 $V$,并且价值最大。这就是背包问题的典型描述。

背包问题的求解过程可以使用各种方法,如贪心、动态规划等。其中,动态规划是最常用的求解方法,常见的动态规划解法包括 0/1 背包问题、完全背包问题和多重背包问题等。

2. 学习建议

如果想要深入学习背包问题,可以从以下两个方面入手:

  • 了解各种求解算法的思想与具体实现。背包问题的各个求解算法具有不同的适用范围和选材方式,对不同类型的问题适用的算法也不同。因此,了解并熟练掌握各种求解算法的思想和具体实现方法,可以更好地解决实际问题。
  • 刷题练习。虽然背包问题的求解方法多种多样,但是题目的套路和解题思路却有一定的共性。不管是初学者还是进阶者,通过不断刷题和实践,学习更多的算法思想和解题技巧,对于学习和掌握背包问题都是非常有帮助的。

作为一名c++程序员,可以在实现背包问题的过程中,多注意以下几点:

  • 注意使用STL库函数和语法糖,这可以提高程序开发效率。
  • 注意代码的易读性和可维护性,可以采用面向对象的方式对代码进行重构。
  • 注意C++语言的强类型和数组越界等问题。
目录
相关文章
|
10天前
|
存储 算法 C++
【C++数据结构——查找】二分查找(头歌实践教学平台习题)【合集】
二分查找的基本思想是:每次比较中间元素与目标元素的大小,如果中间元素等于目标元素,则查找成功;顺序表是线性表的一种存储方式,它用一组地址连续的存储单元依次存储线性表中的数据元素,使得逻辑上相邻的元素在物理存储位置上也相邻。第1次比较:查找范围R[0...10],比较元素R[5]:25。第1次比较:查找范围R[0...10],比较元素R[5]:25。第2次比较:查找范围R[0..4],比较元素R[2]:10。第3次比较:查找范围R[3...4],比较元素R[3]:15。,其中是顺序表中元素的个数。
116 66
【C++数据结构——查找】二分查找(头歌实践教学平台习题)【合集】
|
10天前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
127 75
|
10天前
|
存储 C++
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
【数据结构——树】哈夫曼树(头歌实践教学平台习题)【合集】目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果:任务描述 本关任务:编写一个程序构建哈夫曼树和生成哈夫曼编码。 相关知识 为了完成本关任务,你需要掌握: 1.如何构建哈夫曼树, 2.如何生成哈夫曼编码。 测试说明 平台会对你编写的代码进行测试: 测试输入: 1192677541518462450242195190181174157138124123 (用户分别输入所列单词的频度) 预
49 14
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
|
10天前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
34 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
10天前
|
算法 C++
【C++数据结构——查找】二叉排序树(头歌实践教学平台习题)【合集】
【数据结构——查找】二叉排序树(头歌实践教学平台习题)【合集】 目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果: 任务描述 本关任务:实现二叉排序树的基本算法。 相关知识 为了完成本关任务,你需要掌握:二叉树的创建、查找和删除算法。具体如下: (1)由关键字序列(4,9,0,1,8,6,3,5,2,7)创建一棵二叉排序树bt并以括号表示法输出。 (2)判断bt是否为一棵二叉排序树。 (3)采用递归方法查找关键字为6的结点,并输出其查找路径。 (4)分别删除bt中关键
41 11
【C++数据结构——查找】二叉排序树(头歌实践教学平台习题)【合集】
|
10天前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
51 18
|
10天前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
37 13
|
10天前
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
36 12
|
10天前
|
C++
【C++数据结构——树】二叉树的性质(头歌实践教学平台习题)【合集】
本文档介绍了如何根据二叉树的括号表示串创建二叉树,并计算其结点个数、叶子结点个数、某结点的层次和二叉树的宽度。主要内容包括: 1. **定义二叉树节点结构体**:定义了包含节点值、左子节点指针和右子节点指针的结构体。 2. **实现构建二叉树的函数**:通过解析括号表示串,递归地构建二叉树的各个节点及其子树。 3. **使用示例**:展示了如何调用 `buildTree` 函数构建二叉树并进行简单验证。 4. **计算二叉树属性**: - 计算二叉树节点个数。 - 计算二叉树叶子节点个数。 - 计算某节点的层次。 - 计算二叉树的宽度。 最后,提供了测试说明及通关代
36 10
|
10天前
|
算法 C++
【C++数据结构——图】最小生成树(头歌实践教学平台习题) 【合集】
【数据结构——图】最小生成树(头歌实践教学平台习题)目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果:【合集】任务描述 本关任务:编写一个程序求图的最小生成树。相关知识 为了完成本关任务,你需要掌握:1.建立邻接矩阵,2.Prim算法。建立邻接矩阵 上述带权无向图对应的二维数组,根据它建立邻接矩阵,如图1建立下列邻接矩阵。注意:INF表示无穷大,表示整数:32767 intA[MAXV][MAXV];Prim算法 普里姆(Prim)算法是一种构造性算法,从候选边中挑
34 10