为了账号安全,请及时绑定邮箱和手机立即绑定

动态规划学习:初学者指南

概述

本文介绍了动态规划的基础概念、核心思想和适用场景,并通过实例和代码展示了如何使用动态规划解决经典问题,如最长递增子序列和0/1背包问题。此外,文章还探讨了动态规划的高级技巧,如空间优化和时间复杂度优化,并推荐了相关学习资源和实践项目。

动态规划基础概念
什么是动态规划

动态规划(Dynamic Programming, DP)是一种算法技术,用于解决最优化问题。它通过将问题分解为更小的子问题来解决,这些子问题的解可以被存储和重复使用,以避免重复计算。动态规划通常用于优化问题中,通过将问题分解为子问题的方式,可以在较短的时间内找到最优解。

动态规划的核心在于利用“最优子结构”和“重叠子问题”的特性。最优子结构是指问题的最优解可以通过其子问题的最优解来构建。重叠子问题是指在解决问题的过程中,许多子问题会被重复计算多次,动态规划通过存储已经解决过的子问题的结果,避免了重复计算,从而提高了效率。

动态规划的核心思想

动态规划的核心思想是将问题分解为若干个子问题,通过解决子问题来构建原问题的解。解决问题的过程包括以下几个步骤:

  1. 定义子问题:将原问题拆分为几个子问题,这些子问题的答案可以用来构建原问题的答案。
  2. 计算子问题:通过递归或迭代的方式计算子问题的解。
  3. 存储子问题的解:将已计算过的子问题的解存储起来,以便后续使用。
  4. 合并子问题的解:利用子问题的解构建原问题的解。

通过这种方式,动态规划能够有效地减少重复计算,提高算法的效率。

动态规划的适用场景

动态规划适用于具有以下特点的问题:

  1. 最优子结构:问题的最优解可以通过其子问题的最优解来构建。
  2. 重叠子问题:子问题会被重复计算多次。

这些特点使得动态规划特别适合解决以下类型的问题:

  • 组合优化问题:如背包问题、旅行商问题等。
  • 序列问题:如最长递增子序列问题、最长公共子序列问题等。
  • 图算法问题:如最短路径问题、最大流问题等。

示例

考虑一个简单的例子:计算斐波那契数列的第n项。斐波那契数列的定义为:

  • F(0) = 0
  • F(1) = 1
  • F(n) = F(n-1) + F(n-2) 对于 n > 1

如果不使用动态规划,直接递归计算斐波那契数列会非常低效,因为许多子问题会被重复计算。使用动态规划,可以通过存储已经计算过的斐波那契数来避免重复计算。

def fibonacci(n):
    if n <= 1:
        return n
    dp = [0] * (n + 1)
    dp[0], dp[1] = 0, 1
    for i in range(2, n + 1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]
动态规划的基本方法
递归与记忆化搜索

递归是一种常见的解决问题的方法,它通过将问题分解成更小的子问题来解决。然而,递归计算可能会导致大量的重复计算,特别是对于具有重叠子问题的问题。通过记忆化搜索(Memoization),可以将已经计算过的子问题的结果存储起来,避免重复计算。

示例

考虑计算斐波那契数列的第n项。直接递归计算会导致大量的重复计算,通过记忆化搜索可以避免这种情况。

def fibonacci_memo(n, memo=None):
    if memo is None:
        memo = {}
    if n in memo:
        return memo[n]
    if n <= 1:
        return n
    memo[n] = fibonacci_memo(n-1, memo) + fibonacci_memo(n-2, memo)
    return memo[n]
动态规划数组的使用

动态规划通常使用一个数组或表格来存储子问题的解。数组的大小取决于问题的具体定义和子问题的数量。通过设置初始值和填充数组的方式,可以逐步构建出原问题的解。

示例

继续考虑计算斐波那契数列的第n项。使用动态规划数组存储已经计算过的斐波那契数,避免重复计算。

def fibonacci_dp(n):
    if n <= 1:
        return n
    dp = [0] * (n + 1)
    dp[0], dp[1] = 0, 1
    for i in range(2, n + 1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]
状态转移方程的构建

状态转移方程是动态规划的核心,它描述了如何从子问题的解构建原问题的解。构建状态转移方程时,需要明确每个子问题的状态以及如何通过状态转移方程来更新状态。

示例

继续考虑计算斐波那契数列的第n项。状态转移方程为:

  • F(n) = F(n-1) + F(n-2) 对于 n > 1
  • F(0) = 0
  • F(1) = 1

通过状态转移方程可以构建动态规划数组。

def fibonacci_dp(n):
    if n <= 1:
        return n
    dp = [0] * (n + 1)
    dp[0], dp[1] = 0, 1
    for i in range(2, n + 1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]
动态规划的经典问题
最长递增子序列问题

最长递增子序列(Longest Increasing Subsequence, LIS)问题是寻找一个序列中的最长递增子序列。递增子序列是指序列中的一组元素,它们的值依次递增,但这些元素在原始序列中的位置可以不连续。

示例

考虑以下序列:[10, 9, 2, 5, 3, 7, 101, 18]。最长递增子序列是 [2, 3, 7, 101]

解法

动态规划解决LIS问题的方法是:

  1. 定义状态dp[i] 表示以第 i 个元素结尾的最长递增子序列的长度。
  2. 状态转移:对于每个元素,找到其前面所有元素中比它小的元素,并更新 dp[i] 的值。
  3. 初始化dp[i] 的初始值为1,因为每个元素至少能单独构成一个递增子序列。
  4. 结果:遍历整个数组,找到 dp 数组中的最大值。
def length_of_LIS(nums):
    if not nums:
        return 0
    dp = [1] * len(nums)
    for i in range(len(nums)):
        for j in range(i):
            if nums[i] > nums[j]:
                dp[i] = max(dp[i], dp[j] + 1)
    return max(dp)
0/1背包问题

0/1背包问题是一种经典的优化问题,给定一组物品,每个物品都有一个重量和一个价值,选择物品放入一个有重量限制的背包中,目标是使得背包中的总价值最大。

示例

考虑以下背包问题实例:

  • 物品:[1, 2, 3],重量分别为 [1, 2, 3],价值分别为 [1, 2, 3]
  • 背包容量:4

最大价值为 5,物品组合可以是 12

解法

动态规划解决0/1背包问题的方法是:

  1. 定义状态dp[i][j] 表示前 i 个物品放入容量为 j 的背包中的最大价值。
  2. 状态转移dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]),其中 w[i]v[i] 分别是第 i 个物品的重量和价值。
  3. 初始化dp[0][j] = 0dp[i][0] = 0,表示没有物品或背包没有容量时价值为0。
  4. 结果dp[n][W] 表示前 n 个物品放入容量为 W 的背包中的最大价值。
def knapsack_01(n, W, weights, values):
    dp = [[0 for _ in range(W + 1)] for _ in range(n + 1)]
    for i in range(1, n + 1):
        for w in range(1, W + 1):
            if weights[i-1] <= w:
                dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1])
            else:
                dp[i][w] = dp[i-1][w]
    return dp[n][W]
背包问题变种

背包问题有很多变种,如多重背包问题、完全背包问题等。这里以完全背包问题为例进行讲解。

完全背包问题

完全背包问题与0/1背包问题类似,但不同之处在于每个物品可以无限次放入背包中。

示例

考虑以下完全背包问题实例:

  • 物品:[1, 2],重量分别为 [1, 2],价值分别为 [1, 2]
  • 背包容量:4

最大价值为 6,物品组合可以是 1111

解法

动态规划解决完全背包问题的方法是:

  1. 定义状态dp[i][j] 表示前 i 种物品放入容量为 j 的背包中的最大价值。
  2. 状态转移dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] + v[i]),其中 w[i]v[i] 分别是第 i 种物品的重量和价值。
  3. 初始化dp[0][j] = 0dp[i][0] = 0,表示没有物品或背包没有容量时价值为0。
  4. 结果dp[m][W] 表示前 m 种物品放入容量为 W 的背包中的最大价值。
def knapsack_complete(n, W, weights, values):
    dp = [0 for _ in range(W + 1)]
    for i in range(n):
        for w in range(weights[i], W + 1):
            dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
    return dp[W]
动态规划的代码实现
Python语言实例

Python是一种流行的编程语言,其简洁的语法和强大的库支持使得它非常适合进行动态规划的实现。下面通过几个例子来展示Python中的动态规划实现。

示例代码

  1. 最长递增子序列

    def length_of_LIS(nums):
    if not nums:
        return 0
    dp = [1] * len(nums)
    for i in range(len(nums)):
        for j in range(i):
            if nums[i] > nums[j]:
                dp[i] = max(dp[i], dp[j] + 1)
    return max(dp)
  2. 0/1背包问题

    def knapsack_01(n, W, weights, values):
    dp = [[0 for _ in range(W + 1)] for _ in range(n + 1)]
    for i in range(1, n + 1):
        for w in range(1, W + 1):
            if weights[i-1] <= w:
                dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1])
            else:
                dp[i][w] = dp[i-1][w]
    return dp[n][W]
  3. 完全背包问题
    def knapsack_complete(n, W, weights, values):
    dp = [0 for _ in range(W + 1)]
    for i in range(n):
        for w in range(weights[i], W + 1):
            dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
    return dp[W]
C++语言实例

C++是一种广泛使用的编程语言,适用于各种算法实现,包括动态规划。下面通过几个例子来展示C++中的动态规划实现。

示例代码

  1. 最长递增子序列
    
    #include <vector>
    #include <algorithm>

int lengthOfLIS(std::vector<int>& nums) {
if (nums.empty()) return 0;
std::vector<int> dp(nums.size(), 1);
for (int i = 0; i < nums.size(); ++i) {
for (int j = 0; j < i; ++j) {
if (nums[i] > nums[j]) {
dp[i] = std::max(dp[i], dp[j] + 1);
}
}
}
return *std::max_element(dp.begin(), dp.end());
}


2. **0/1背包问题**
```cpp
#include <vector>
#include <algorithm>

int knapsack01(int n, int W, std::vector<int>& weights, std::vector<int>& values) {
    std::vector<std::vector<int>> dp(n + 1, std::vector<int>(W + 1, 0));
    for (int i = 1; i <= n; ++i) {
        for (int w = 1; w <= W; ++w) {
            if (weights[i-1] <= w) {
                dp[i][w] = std::max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1]);
            } else {
                dp[i][w] = dp[i-1][w];
            }
        }
    }
    return dp[n][W];
}
  1. 完全背包问题
    
    #include <vector>
    #include <algorithm>

int knapsackComplete(int n, int W, std::vector<int>& weights, std::vector<int>& values) {
std::vector<int> dp(W + 1, 0);
for (int i = 0; i < n; ++i) {
for (int w = weights[i]; w <= W; ++w) {
dp[w] = std::max(dp[w], dp[w - weights[i]] + values[i]);
}
}
return dp[W];
}


## Java语言实例
Java是一种广泛使用的编程语言,适用于各种算法实现,包括动态规划。下面通过几个例子来展示Java中的动态规划实现。

### 示例代码
1. **最长递增子序列**
```java
public int lengthOfLIS(int[] nums) {
    if (nums.length == 0) return 0;
    int[] dp = new int[nums.length];
    Arrays.fill(dp, 1);
    for (int i = 0; i < nums.length; ++i) {
        for (int j = 0; j < i; ++j) {
            if (nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
    }
    return Arrays.stream(dp).max().getAsInt();
}
  1. 0/1背包问题

    public int knapsack01(int n, int W, int[] weights, int[] values) {
    int[][] dp = new int[n + 1][W + 1];
    for (int i = 1; i <= n; ++i) {
        for (int w = 1; w <= W; ++w) {
            if (weights[i-1] <= w) {
                dp[i][w] = Math.max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1]);
            } else {
                dp[i][w] = dp[i-1][w];
            }
        }
    }
    return dp[n][W];
    }
  2. 完全背包问题
    public int knapsackComplete(int n, int W, int[] weights, int[] values) {
    int[] dp = new int[W + 1];
    for (int i = 0; i < n; ++i) {
        for (int w = weights[i]; w <= W; ++w) {
            dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
        }
    }
    return dp[W];
    }
动态规划的进阶技巧
空间优化

动态规划的一个常见优化手段是空间优化。通常情况下,动态规划需要一个二维数组来存储中间状态,但对于某些特定的问题,可以通过滚动数组或一维数组来减少空间复杂度。

示例

考虑计算斐波那契数列的第n项。使用滚动数组可以将空间复杂度从O(n)优化到O(1)。

def fibonacci_space_optimized(n):
    if n <= 1:
        return n
    prev, curr = 0, 1
    for i in range(2, n + 1):
        prev, curr = curr, prev + curr
    return curr
时间复杂度优化

动态规划的时间复杂度通常较高,但可以通过一些优化技巧来减少计算量。常见的优化方法包括剪枝、状态压缩等。

示例

考虑计算斐波那契数列的第n项。通过状态压缩,可以减少中间状态的计算。

def fibonacci_time_optimized(n):
    if n <= 1:
        return n
    prev, curr = 0, 1
    for i in range(2, n + 1):
        prev, curr = curr, prev + curr
    return curr
状态压缩技巧

状态压缩是一种将多个状态压缩为一个状态的技术,可以减少状态的数量,从而减少计算量。常见的状态压缩方法包括位运算等。

示例

考虑计算斐波那契数列的第n项。通过位运算可以减少中间状态的计算。

def fibonacci_state_compression(n):
    if n <= 1:
        return n
    dp = [0] * (n + 1)
    dp[1] = 1
    for i in range(2, n + 1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]
动态规划学习资源推荐
在线课程推荐

推荐以下在线课程,这些课程涵盖了动态规划的基础知识和高级技巧:

书籍推荐
  • 《算法导论》(Introduction to Algorithms),作者:Thomas H. Cormen、Charles E. Leiserson、Ronald L. Rivest、Clifford Stein,出版社:The MIT Press,出版年:2009年。本书虽然不是专门介绍动态规划,但包含了大量关于动态规划的实例和应用。
  • 《编程珠玑》(Programming Pearls),作者:Jon Bentley,出版社:Addison-Wesley Professional,出版年:1999年。通过实例和案例分析,帮助理解动态规划的思想和方法。
实践项目推荐
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消