继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

动态规划入门教程:轻松掌握基础算法

HUH函数
关注TA
已关注
手记 342
粉丝 66
获赞 315
概述

动态规划是一种通过将复杂问题拆分为更简单子问题来解决的算法技术。其核心思想是通过缓存子问题的结果避免重复计算,从而提高算法效率。动态规划特别适用于具有重叠子问题和最优子结构的问题,并被广泛应用于计算机科学、运筹学、经济学等多个领域。

动态规划简介
什么是动态规划

动态规划(Dynamic Programming,简称DP)是一种通过将复杂问题拆分为更简单子问题来解决的算法技术。其核心思想是将一个问题分解为一系列递归子问题,然后将每个子问题的解存储起来以避免重复计算,从而提高算法效率。这种技术特别适用于具有重叠子问题和最优子结构的问题。

动态规划的特点和应用领域

特点

  1. 重叠子问题:相同的子问题会被多次计算,因此可以通过缓存这些子问题的结果来提高效率。
  2. 最优子结构:问题的最优解可以通过其子问题的最优解来构建。
  3. 自底向上:通常从最小子问题开始,逐步构建到原问题的解。
  4. 自顶向下:也可以从原问题开始,递归地分解成子问题,直到最小子问题。

应用领域

动态规划广泛应用于各个领域,如计算机科学、运筹学、经济学、生物信息学等。典型的应用包括:

  • 最短路径问题:如Dijkstra算法
  • 字符串匹配问题:如Levenshtein距离
  • 资源分配问题:如0-1背包问题
  • 序列分析问题:如最长公共子序列
动态规划的基本思想和步骤

基本思想

动态规划的基本思想是利用子问题的解来构建原问题的解。具体来说,通过将问题分解成若干个子问题,求出每个子问题的解,并将这些子问题的解存储起来,避免重复计算,从而提高效率。

实现步骤

  1. 确定状态:定义问题的状态,即描述问题所需的状态变量。
  2. 确定决策变量:定义为解决问题所做的决策。
  3. 状态转移方程:定义状态之间的转移关系。
  4. 初始化边界条件:确定初始状态。
  5. 求解:通过状态转移方程从初始状态逐步求解最终状态。
动态规划的核心概念
状态和状态转移

状态

状态是指某一时刻的具体情况或条件。在动态规划中,状态通常用一个或多个变量表示。例如,在最长递增子序列问题中,状态可以表示为一个整数序列中的某个位置。

状态转移

状态转移是指从一个状态到另一个状态的变化过程。状态转移通常通过状态转移方程来描述。例如,在0-1背包问题中,状态转移方程可以表示为:
[ dp[i][j] = \max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]) ]

子问题和最优子结构

子问题

子问题是原问题的一部分,通常比原问题更小。解决子问题后,可以通过这些子问题的解来构建原问题的解。例如,在最长公共子序列问题中,子问题可以是两个序列的前缀。

最优子结构

最优子结构是指问题的最优解可以通过其子问题的最优解来构建。例如,在0-1背包问题中,若某个物品加入背包后能得到最优解,则不包含该物品的子问题的解也是最优的。

递归与记忆化搜索

递归

递归是一种解决问题的方法,它将问题分解为更小的子问题,并通过函数调用来解决问题。递归的基本思想是将一个复杂的问题分解为一个或多个更小的问题,然后通过递归调用函数来解决这些更小的问题。

记忆化搜索

记忆化搜索是动态规划的一种实现方式,通过在递归过程中存储已经计算过的结果,避免重复计算。具体实现时,可以使用一个数组或哈希表来存储子问题的解。

递归示例

以下是一个简单的递归函数,用于计算斐波那契数列:

def fibonacci(n):
    if n <= 1:
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)

记忆化搜索示例

将上述递归函数改进为记忆化搜索:

cache = {}

def fibonacci(n):
    if n in cache:
        return cache[n]
    if n <= 1:
        cache[n] = n
    else:
        cache[n] = fibonacci(n-1) + fibonacci(n-2)
    return cache[n]
动态规划的标准模板
动态规划的典型结构

动态规划通常由以下几个部分组成:

  1. 确定状态:定义问题的状态。
  2. 确定决策变量:定义解决问题所做的决策。
  3. 状态转移方程:定义状态之间的转移关系。
  4. 初始化边界条件:确定初始状态。
  5. 求解:通过状态转移方程从初始状态逐步求解最终状态。
动态规划的实现步骤
  1. 定义状态
    • 状态通常表示问题的某个阶段或状态变量。
  2. 定义决策变量
    • 决策变量表示在该状态下可以进行的决策。
  3. 状态转移方程
    • 描述状态之间的转移关系。
  4. 初始化边界条件
    • 确定初始状态或边界条件。
  5. 求解
    • 通过状态转移方程从初始状态逐步求解最终状态。
如何定义状态和状态转移方程

定义状态

定义状态时,要确保状态能够唯一标识问题的某个阶段。例如,在0-1背包问题中,状态可以定义为dp[i][j],表示前i个物品在容量为j的背包中的最优解。

状态转移方程

状态转移方程用于描述状态之间的转移关系。例如,在0-1背包问题中:
[ dp[i][j] = \max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]) ]
其中,dp[i][j]表示前i个物品在容量为j的背包中的最优解,w[i]v[i]分别表示第i个物品的重量和价值。

示例代码

def knapsack(weights, values, capacity):
    n = len(weights)
    dp = [[0 for _ in range(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] = max(dp[i-1][j], dp[i-1][j-weights[i-1]] + values[i-1])
            else:
                dp[i][j] = dp[i-1][j]

    return dp[n][capacity]
常见动态规划问题解析
最长递增子序列问题

问题描述

给定一个整数序列,找到最长的递增子序列。

示例代码

def longest_increasing_subsequence(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背包问题

问题描述

给定一个物品列表,每个物品有一个重量和一个价值。选择若干物品放入一个容量有限的背包中,使得背包中物品的总价值最大,但总重量不能超过背包的容量。

示例代码

def knapsack(weights, values, capacity):
    n = len(weights)
    dp = [[0 for _ in range(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] = max(dp[i-1][j], dp[i-1][j-weights[i-1]] + values[i-1])
            else:
                dp[i][j] = dp[i-1][j]

    return dp[n][capacity]
背包问题变种

0-1背包问题变种

给定一个物品列表,每个物品有一个重量和一个价值。选择若干物品放入一个容量有限的背包中,使得背包中物品的总价值最大,但总重量不能超过背包的容量。如果选择某个物品,则必须选择该物品的全部或不选择。

示例代码

def knapsack_01(weights, values, capacity):
    n = len(weights)
    dp = [[0 for _ in range(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] = max(dp[i-1][j], dp[i-1][j-weights[i-1]] + values[i-1])
            else:
                dp[i][j] = dp[i-1][j]

    return dp[n][capacity]

完全背包问题

与0-1背包不同,完全背包中的物品可以重复选择。每个物品可以放入背包任意次,但每次放入的重量和价值相同。

示例代码

def knapsack_complete(weights, values, capacity):
    n = len(weights)
    dp = [0 for _ in range(capacity + 1)]

    for i in range(n):
        for j in range(weights[i], capacity + 1):
            dp[j] = max(dp[j], dp[j - weights[i]] + values[i])

    return dp[capacity]

多重背包问题

与完全背包类似,但每个物品最多可以选择k次。

示例代码

def knapsack_multiple(weights, values, capacities, k):
    n = len(weights)
    dp = [0 for _ in range(capacities + 1)]

    for i in range(n):
        for j in range(capacities, weights[i] - 1, -1):
            for t in range(1, min(k[i] + 1, j // weights[i]) + 1):
                dp[j] = max(dp[j], dp[j - t * weights[i]] + t * values[i])

    return dp[capacities]
动态规划的优化技巧
空间优化

一维数组优化

对于某些动态规划问题,可以通过只使用一维数组来优化空间复杂度。例如,在0-1背包问题中,可以只使用一个一维数组来存储状态。

示例代码

def knapsack_01(weights, values, capacity):
    n = len(weights)
    dp = [0 for _ in range(capacity + 1)]

    for i in range(n):
        for j in range(capacity, weights[i] - 1, -1):
            dp[j] = max(dp[j], dp[j - weights[i]] + values[i])

    return dp[capacity]
时间优化

贪心算法

在某些动态规划问题中,可以使用贪心算法来减少计算量。例如,在最长递增子序列问题中,可以使用二分查找来优化求解过程。

示例代码

def longest_increasing_subsequence(nums):
    if not nums:
        return 0

    dp = []
    for num in nums:
        if not dp or num > dp[-1]:
            dp.append(num)
        else:
            dp[bisect.bisect_left(dp, num)] = num

    return len(dp)
动态规划中的剪枝技术

剪枝

剪枝技术通过提前终止不必要的递归分支来优化动态规划算法。例如,在回溯算法中,可以通过剪枝提前终止不必要的递归分支。

示例代码

def knapsack(weights, values, capacity):
    n = len(weights)
    dp = [[0 for _ in range(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]:
                if dp[i-1][j] > dp[i-1][j-weights[i-1]] + values[i-1]:
                    dp[i][j] = dp[i-1][j]
                else:
                    dp[i][j] = dp[i-1][j-weights[i-1]] + values[i-1]
            else:
                dp[i][j] = dp[i-1][j]

    return dp[n][capacity]
动态规划的应用实例

动态规划在实际应用中有很多经典案例。例如,0-1背包问题、最长公共子序列问题等。通过具体案例的分析,可以更好地理解动态规划的实现过程。

示例代码

以下是一个具体案例的代码示例,展示了如何使用动态规划解决最长公共子序列问题。

def longest_common_subsequence(text1, text2):
    m, n = len(text1), len(text2)
    dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]

    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if text1[i-1] == text2[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])

    return dp[m][n]
动态规划实战演练
实战案例分析

动态规划在实际应用中有很多经典案例。例如,0-1背包问题、最长公共子序列问题等。通过具体案例的分析,可以更好地理解动态规划的实现过程。

示例代码

以下是一个具体案例的代码示例,展示了如何使用动态规划解决最长公共子序列问题。

def longest_common_subsequence(text1, text2):
    m, n = len(text1), len(text2)
    dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]

    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if text1[i-1] == text2[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])

    return dp[m][n]

动态规划常见面试题解析

面试中经常会出现动态规划相关的题目。以下是几个常见的面试题示例及其解答。

示例代码

def longest_increasing_subsequence(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)

示例代码

def knapsack(weights, values, capacity):
    n = len(weights)
    dp = [[0 for _ in range(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] = max(dp[i-1][j], dp[i-1][j-weights[i-1]] + values[i-1])
            else:
                dp[i][j] = dp[i-1][j]

    return dp[n][capacity]
打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP