跳过正文

Python系列 - 动态规划

·2966 字·
Python 动态规划 Python算法
EZ
作者
EZ
Take it EZ!
目录
Python - 这篇文章属于一个选集。
§ 3: 本文

1. 动态规划介绍
#

动态规划(Dynamic Programming,简称 DP)是一种算法设计方法,适用于解决具有重叠子问题和最优子结构性质的问题。 其基本思想是将复杂问题分解成更小的子问题,通过存储子问题的解来避免重复计算,从而提高效率。

  • 动态规划的核心概念
    • 重叠子问题:问题可以分解为相互重叠的子问题,即子问题会被多次计算。
    • 最优子结构:问题的最优解可以由其子问题的最优解构造而来。
  • 动态规划的步骤
    1. 定义状态:确定数组或矩阵的维度,定义状态 dp[i][j] 表示问题在子问题 i, j 的最优解。
    2. 状态转移方程:找到状态之间的关系,写出状态转移方程,这一步是动态规划的核心。
    3. 初始状态和边界条件:确定初始状态和边界条件的值。
    4. 计算顺序:根据状态转移方程和初始状态,按照一定顺序计算各个状态的值。
    5. 返回结果:最后根据问题要求,返回所需状态的值。

2. 斐波那契数列
#

  • 斐波那契数列是递归的经典例子。其定义为:
    • F(0) = 0
    • F(1) = 1
    • F(n) = F(n-1) + F(n-2) (n ≥ 2)
  • 以下是通过动态规划的方式实现:
    def fibonacci(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]
    
    # 示例调用
    print(fibonacci(10))  # 输出 55
    
  • 也可以使用递归的方法实现,参考: 斐波那契数列-递归方法

2. 背包问题
#

2.1 0-1背包
#

给定一组物品,每个物品有重量和价值,在限定的总重量内,选择某些物品,使得总价值最大。这个问题称之为0-1背包问题。

2.1.1. 二维数组
#

  • 解法一:二维 dp 数组
    • 定义 dp[i][w] 表示前 i 个物品中,总重量不超过 w 的最大价值。
def knapsack(weights, values, capacity):
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        for w in range(1, capacity + 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][capacity]

# 示例调用
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 5
print(knapsack(weights, values, capacity))  # 输出 7
  • i 代表物品从1开始的索引,0 代表不放入物品。有 4 个物品的话,i 的取值范围是1,2,3,4。
  • w 代表背包的剩余限重,0 代表背包没有剩余限重。
    • for w in range(1, capacity + 1) 这个遍历的作用是为了计算出所有剩余容量的情况下的最大价值,用 w 来表示当前剩余容量。
  • 关于索引的说明:
    • dp[i - 1][w] 代表的是不放入当前物品在包剩余容量为 w 时的总价值。
    • 由于 Python 的 index 是从 0 开始的,weights[i - 1]values[i - 1] 分别代表第 i 个物品的重量和价值。
    • 特别注意 dpi-1 指的是上一个物品的情况;而 weightsvaluesi-1 指的是当前物品。
    • 因为dp 考虑到了不放物品和没有容量的情况;weightsvalues 只是两个 Python 列表,记录物品的重量和价值。
  • if weights[i - 1] <= w:当前物品重量小于等于背包剩余限重。
  • 如果当前容量满足放下第 i 个物品,我们需要考虑是否放入该物品
    • dp[i][w] = max(dp[i - 1][w], dp[i - 1][w - weights[i - 1]] + values[i - 1])
      • 不放入第 i 个物品
        • dp[i][w] =dp[i - 1][w]
        • 当前价值等于上个物品在剩余容量为 w 的价值
      • 放入第 i 个物品
        • dp[i][w] = dp[i - 1][w - weights[i - 1]] + values[i - 1]
        • 当前价值等于上个物品在 w 减去当前物品重量时的价值加上当前物品的价值
      • 对于这两种情况取最大值。
        • 说明:当 w 不一样时,面对固定的 i 有可能是会有不一样的价值。容量会限定我们选择一些物品不放入,所以需要比较出最大值,来决定当前物品放还是不放。
  • 如果当前容量不满足放下第 i 个物品,使其等于上一个相同 w 的情况:dp[i][w] = dp[i - 1][w]

2.1.2. 一维数组
#

  • 解法二:一维 dp 数组
    def knapsack_1d(weights, values, capacity):
        n = len(weights)
        dp = [0] * (capacity + 1)
    
        for i in range(n):
            # 反向遍历背包容量,防止在同一轮迭代中重复使用物品
            for w in range(capacity, weights[i] - 1, -1):
                dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
    
        return dp[capacity]
    
    # 示例调用
    weights = [2, 3, 4, 5]
    values = [3, 4, 5, 6]
    capacity = 5
    print(knapsack_1d(weights, values, capacity))
    
    • 由解法一可以看出,当前物品放与不放,只和求上一个物品时得到的最优解有关。
    • 更进一步来说,需要比较(当前容量下,不放当前物品)和(放了当前物品后,上一个物品在剩余容量下的最优解),然后取价值最高的,得到当前最优解。
    • 使用一维数组的思想是,逐步用当前求出来的结果替换掉上一步求出来的结果。
    • 有一个问题是,如果按照正序逐步替换,在计算放了当前物品后的最大价值,有可能左侧的值已经被替换掉了。
    • 解决方法是,将 dp 倒序记录,也就是将剩余容量从大到小来考虑。
    • 原因是 剩余容量 = 当前容量 - 当前物品的重量,所以剩余容量不可能会超过当前容量,也就不会跑到右边去。倒序后可以避免造成记录值的污染。

2.2. 完全背包
#

完全背包问题是背包问题的一种变体,其中每种物品可以选择多个(不限次数)放入背包,前提是总重量不超过背包的容量。 与0-1背包问题不同,这里每种物品可以放入背包多次。可以使用动态规划来解决完全背包的问题。

2.2.1. 二维数组
#

  • 解法一:二维 dp 数组
def complete_knapsack_2d(weights, values, capacity):
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]

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

    return dp[n][capacity]

# 示例调用
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 5
print(complete_knapsack_2d(weights, values, capacity))
  • 在代码中,和0-1背包唯一的区别就在转换方程里的索引,其余都是一样的。
  • 对于每个物品 i,我们有两种选择:不选择这个物品或选择这个物品多次。
    • 同样,dp[i-1][w] 还是表示不选择第 i 个物品时,背包容量为 w 时的最大价值。
    • dp[i][w-weights[i-1]] + values[i-1] 表示选择第 i 个物品且可以继续选择这个物品时,背包容量为 w 时的最大价值。
      • dp[i][w-weights[i-1]] 首先重量需要减去当前物品的重量,然后去寻找对应的 i 而不是 i-1,因为物品可以重复放置。
      • i 和 限重等于 w-weights[i-1] 的时候已经求得了当前最优解,也就是考虑过 i 物品在此限重条件下是否放入背包,所以需要将索引从原先问题的 i-1 改成 i

2.2.2. 一维数组
#

  • 解法二:一维 dp 数组
def complete_knapsack(weights, values, capacity):
    n = len(weights)
    # 创建一维数组 dp,其中 dp[j] 表示容量为 j 的背包所能容纳的最大价值
    dp = [0] * (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]

# 示例调用
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 5
print(complete_knapsack(weights, values, capacity))
  • 和 0-1 背包问题的区别在于,我们这里需要考虑剩余重量从小到大正序计算。
  • 在 0-1 背包问题中,我们要防止数据被污染;而这里我们需要用 i 算出来的值来替代掉 i-1 计算出来的值。
  • 原理与上面完全背包的二维数组解法类似。

3. 使用最小花费爬楼梯
#

LeetCode:746. 使用最小花费爬楼梯

  • 题目描述:
    • 给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
    • 你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
    • 请你计算并返回达到楼梯顶部的最低花费。
  • 示例 1:
    • 输入:cost = [10, 15, 20]
    • 输出:15
    • 解释:你将从下标为 1 的台阶开始。
      • 支付 15 ,向上爬两个台阶,到达楼梯顶部。
      • 总花费为 15 。
  • 示例 2:
    • 输入:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
    • 输出:6
    • 解释:你将从下标为 0 的台阶开始。
      • 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
      • 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
      • 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
      • 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
      • 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
      • 支付 1 ,向上爬一个台阶,到达楼梯顶部。
      • 总花费为 6 。
  • 代码:
    class Solution(object):
        def minCostClimbingStairs(self, cost):
            """
            :type cost: List[int]
            :rtype: int
            """
            n = len(cost)
            dp = [0] * (n+1)
            dp[0], dp[1] = cost[0], cost[1]
            for i in range(2, n+1):
                previous_cost = min(dp[i-1], dp[i-2])
                if i == n:
                    dp[i] = previous_cost
                else:
                    dp[i] = previous_cost + cost[i]
            return dp[-1]
    
    • 由 dp 记录每一步最优解,长度为n+1,最后一个值代表抵达顶部的最优花费。
    • i 定义为当前的台阶索引,0 和 1 分别代表第一步踏上第一个台阶和第一步踏上第二个台阶的费用。
    • 遍历从2 开始,因为0和1的花费已知。
    • 之前的最优花费就是比较 dp[i-1]dp[i-2] 的最小值,因为一次只能走一个台阶或者两个台阶。
    • 当前最优总花费 = 之前最优花费 + 当前台阶的费用。
    • 最后一步抵达顶部时没有花费,所以等于之前的最优总花费。
    • 这个问题有不同的写法(参考 LeetCode 题解),但是这样的写法方便于本人自己的理解。
Python - 这篇文章属于一个选集。
§ 3: 本文

相关文章

Python系列 - 滑动窗口
·1059 字
Python 滑动窗口 Python算法
滑动窗口是一种常用于数组或字符串的算法技巧,它通过在数组或字符串上维护一个窗口。
Python系列 - 递归函数
·2425 字
Python 递归 Python算法
如果一个函数在内部调用自身本身,那么这个函数就是递归函数。
Spark系列 - 数据存储
·1316 字
大数据 Spark DataFrame SparkSQL 分布式数据库
本章主要讨论 pySpark 的数据存储。
Spark系列 - 数据读取
·2470 字
大数据 Spark DataFrame SparkSQL 分布式数据库
本章主要讨论 pySpark 的数据读取。
Spark系列 - 配置Spark
·1559 字
大数据 Spark 日志 Log4J 硬编码 软编码
本文将详细介绍如何在 Spark 项目中配置 Log4J 日志模块,以及配置 Spark Session。
Spark系列 - 本地环境的搭建
·544 字
大数据 Spark 环境安装
本篇文章将介绍如何在本地 Mac 环境下搭建 Spark,包括安装 JDK、配置环境变量、安装和配置 Spark 以及安装 PySpark。