动态规划与0/1背包问题:深入解析

目录

一、动态规划简介

二、0/1背包问题概述

三、动态规划解决0/1背包问题

[1. 定义子问题](#1. 定义子问题)

[2. 确定状态](#2. 确定状态)

[3. 初始条件和边界情况](#3. 初始条件和边界情况)

[4. 计算最终结果](#4. 计算最终结果)

[5. 代码实现](#5. 代码实现)

[6. 空间优化](#6. 空间优化)

四、例题讲解

例题1:基础例题

例题2:路径恢复

例题3:扩展问题

五、总结


动态规划(Dynamic Programming, DP)是计算机科学中一种解决复杂问题的高效方法。通过将问题分解成更小的子问题并存储其结果,动态规划避免了重复计算,从而显著提高了效率。本文将详细介绍动态规划的基本概念,并以经典的0/1背包问题为例,展示如何应用动态规划进行求解。

一、动态规划简介

动态规划是一种优化技术,适用于解决具有重叠子问题和最优子结构性质的问题。其核心思想是通过记录已解决的子问题的结果,避免重复计算,从而提高算法效率。

动态规划的步骤通常包括:

  1. 定义子问题:将原问题分解为更小的子问题。
  2. 确定状态:定义表示子问题的状态变量。
  3. 状态转移方程:找到状态之间的递推关系。
  4. 初始条件和边界情况:设定初始状态的值。
  5. 计算最终结果:根据递推关系和初始条件计算出原问题的解。

二、0/1背包问题概述

0/1背包问题是组合优化中的经典问题之一,其定义如下:

给定一个容量为 W的背包,以及 n个物品,每个物品有一个重量 wi和价值 vi​。在保证总重量不超过背包容量的前提下,如何选择物品使得总价值最大?

与之不同的是,每个物品只能选择一次(即0/1选择),而不是无限制地选择(完全背包问题)。

三、动态规划解决0/1背包问题

1. 定义子问题

设 dpij表示前 iii 个物品在背包容量为 jjj 时的最大总价值。目标是求 dpnW

2. 确定状态

状态 dpijdpijdpij 的值可以通过以下方式递推得到:

  • 如果不选择第 i个物品,则 dpij=dpi−1jdpij = dpi-1jdpij=dpi−1j
  • 如果选择第 i个物品,则 dpij=dpi−1j−wi+vi,前提是j ≥ wi。

因此,状态转移方程为: dpij=max⁡(dpi−1j,dpi−1j−wi+vi)

3. 初始条件和边界情况

对于初始条件,当没有物品或背包容量为0时,总价值均为0:

dp0j = 0 对于0≤j≤W

dpi0 = 0 对于0≤i≤n

4. 计算最终结果

通过自底向上填充DP表格,最终结果即为 dpnW

5. 代码实现

以下是C++实现,代码中包含了详细的解释:

#include <iostream>

#include <vector>

#include <algorithm>

using namespace std;

int knapsack(const vector<int>& weights, const vector<int>& values, int W) {

int n = weights.size();

// 创建一个二维数组 dp,大小为 (n+1) x (W+1),并初始化为 0

vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));

// 填充 dp 表格

for (int i = 1; i <= n; ++i) {

for (int w = 0; w <= W; ++w) {

if (weightsi-1 <= w) {

// 如果可以放入当前物品,选择放或不放,取最大值

dpiw = max(dpi-1w, dpi-1w - weights\[i-1] + valuesi-1);

} else {

// 如果不能放入当前物品,直接取前 i-1 个物品的最大值

dpiw = dpi-1w;

}

}

}

// 最终结果是 dpnW,即考虑所有物品在最大重量 W 时的最大价值

return dpnW;

}

int main() {

vector<int> weights = {2, 3, 4, 5};

vector<int> values = {3, 4, 5, 6};

int W = 5;

cout << "Maximum value in Knapsack = " << knapsack(weights, values, W) << endl;

return 0;

}

6. 空间优化

上述实现的时间复杂度为 O(nW),空间复杂度同样为 O(nW)。可以通过空间优化将空间复杂度降至 O(W)。这是因为在计算 dpij时,只需要参考 dpi−1j和 dpi−1j−wi 两个状态,因此可以使用一维数组进行优化。

优化后的实现如下:

#include <iostream>

#include <vector>

#include <algorithm>

using namespace std;

int knapsack_optimized(const vector<int>& weights, const vector<int>& values, int W) {

int n = weights.size();

vector<int> dp(W + 1, 0);

// 通过一维数组优化空间复杂度

for (int i = 0; i < n; ++i) {

for (int w = W; w >= weightsi; --w) {

dpw = max(dpw, dpw - weights\[i] + valuesi);

}

}

return dpW;

}

int main() {

vector<int> weights = {2, 3, 4, 5};

vector<int> values = {3, 4, 5, 6};

int W = 5;

cout << "Maximum value in Knapsack = " << knapsack_optimized(weights, values, W) << endl;

return 0;

}

四、例题讲解

例题1:基础例题

题目 :给定一个背包的容量为 5,以及 4 个物品,每个物品的重量和价值分别为 {2, 3, 4, 5}{3, 4, 5, 6}。求如何选择物品使得总价值最大。

#include <iostream>

#include <vector>

#include <algorithm>

using namespace std;

int knapsack(const vector<int>& weights, const vector<int>& values, int W) {

int n = weights.size();

vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));

// 填充 dp 表格

for (int i = 1; i <= n; ++i) {

for (int w = 0; w <= W; ++w) {

if (weightsi-1 <= w) {

// 如果可以放入当前物品,选择放或不放,取最大值

dpiw = max(dpi-1w, dpi-1w - weights\[i-1] + valuesi-1);

} else {

// 如果不能放入当前物品,直接取前 i-1 个物品的最大值

dpiw = dpi-1w;

}

}

}

return dpnW;

}

int main() {

vector<int> weights = {2, 3, 4, 5};

vector<int> values = {3, 4, 5, 6};

int W = 5;

cout << "Maximum value in Knapsack = " << knapsack(weights, values, W) << endl;

return 0;

}

代码解释

  1. 初始化 dp 数组,用于存储子问题的解。
  2. 双重循环填充 dp 表格,其中 dp[i][w] 表示前 i 个物品在容量为 w 时的最大价值。
  3. 根据物品是否放入背包来更新 dp[i][w] 的值。
  4. 最后返回 dp[n][W],即为最大总价值。

例题2:路径恢复

题目:求解背包问题的同时,找出选择的物品。

#include <iostream>

#include <vector>

#include <algorithm>

using namespace std;

pair<int, vector<int>> knapsack_with_items(const vector<int>& weights, const vector<int>& values, int W) {

int n = weights.size();

vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));

vector<vector<bool>> keep(n + 1, vector<bool>(W + 1, false));

// 填充 dp 表格并记录是否选择物品

for (int i = 1; i <= n; ++i) {

for (int w = 0; w <= W; ++w) {

if (weightsi-1 <= w) {

if (dpi-1w < dpi-1w - weights\[i-1] + valuesi-1) {

dpiw = dpi-1w - weights\[i-1] + valuesi-1;

keepiw = true;

} else {

dpiw = dpi-1w;

}

} else {

dpiw = dpi-1w;

}

}

}

// 回溯找出选择的物品

int w = W;

vector<int> items;

for (int i = n; i >= 1; --i) {

if (keepiw) {

items.push_back(i-1);

w -= weightsi-1;

}

}

return {dpnW, items};

}

int main() {

vector<int> weights = {2, 3, 4, 5};

vector<int> values = {3, 4, 5, 6};

int W = 5;

auto result = knapsack_with_items(weights, values, W);

cout << "Maximum value in Knapsack = " << result.first << endl;

cout << "Items included: ";

for (int i : result.second) {

cout << i << " ";

}

cout << endl;

return 0;

}

代码解释

  1. dp 数组外增加一个 keep 数组,用于记录是否选择某个物品。
  2. 在填充 dp 表格时,更新 keep 数组以记录选择情况。
  3. 通过回溯 keep 数组,找出选择的物品并存储在 items 数组中。
  4. 最后返回最大总价值和选择的物品列表。

例题3:扩展问题

题目:考虑更多约束条件,如物品的体积和背包的体积限制。

#include <iostream>

#include <vector>

#include <algorithm>

using namespace std;

struct Item {

int weight;

int volume;

int value;

};

int knapsack_with_volume(const vector<Item>& items, int W, int V) {

int n = items.size();

vector<vector<vector<int>>> dp(n + 1, vector<vector<int>>(W + 1, vector<int>(V + 1, 0)));

for (int i = 1; i <= n; ++i) {

for (int w = 0; w <= W; ++w) {

for (int v = 0; v <= V; ++v) {

if (itemsi-1.weight <= w && itemsi-1.volume <= v) {

dpiwv = max(dpi-1wv, dpi-1w - items\[i-1.weight]v - items\[i-1.volume] + itemsi-1.value);

} else {

dpiwv = dpi-1wv;

}

}

}

}

return dpnWV;

}

int main() {

vector<Item> items = {

{2, 3, 4},

{3, 4, 5},

{4, 5, 6},

{5, 6, 7}

};

int W = 5;

int V = 7;

cout << "Maximum value in Knapsack = " << knapsack_with_volume(items, W, V) << endl;

return 0;

}

代码解释

  1. 定义 Item 结构体,包含物品的重量、体积和价值。
  2. 初始化 dp 三维数组,用于存储子问题的解。
  3. 三重循环填充 dp 表格,其中 dp[i][w][v] 表示前 i 个物品在重量为 w 和体积为 v 时的最大价值。
  4. 根据物品是否放入背包来更新 dp[i][w][v] 的值。
  5. 最后返回 dp[n][W][V],即为最大总价值。

五、总结

通过本文的详细解析和多个例题的讲解,我们可以深入理解动态规划及其在0/1背包问题中的应用。从定义子问题、确定状态、推导状态转移方程、设定初始条件,到计算最终结果,动态规划提供了一种系统而高效的解决问题的思路。

掌握动态规划的基本原理和应用技巧,不仅能解决背包问题,还能扩展到其他领域,如字符串匹配、序列比对、路径规划等。希望本文能够帮助读者更好地理解和应用动态规划,提升解决复杂问题的能力。

相关推荐
啥都想学点的研究生44 分钟前
一篇文章讲清楚:线性回归问题的求解——正规方程法
算法·机器学习·线性回归
青少儿编程课堂1 小时前
树状数组精讲:逆序对计数与离散化(附双语言解法)
算法·树状数组·离散化·信息学奥赛·逆序对
血小板要健康1 小时前
网格 dfs 与 FloodFill:从岛屿、区域到搜索路径
笔记·算法·leetcode·深度优先
VALENIAN瓦伦尼安教学设备2 小时前
设备状态检测振动分析实训台案例分析
大数据·数据库·人工智能·嵌入式硬件·算法
小小晓.2 小时前
C++ 值类别与完美转发深度剖析:从左值右值到 std::forward 源码解密
开发语言·c++·算法
怕浪猫2 小时前
用了两年 AI 编程工具后,我重新理解了什么是「资深工程师」
算法·面试·架构
hetao17338373 小时前
2026-08-27~29 hetao1733837 的刷题记录
c++·算法
云淡风轻~窗明几净3 小时前
宇宙管理学猜想
算法·图论
春风解人意3 小时前
从零开始学习嵌入式P28----线程
c语言·学习·算法
luj_17683 小时前
合法避税与投资评估实战指南
c语言·开发语言·网络·经验分享·算法