- 什么是动态规划
- 动态规划的定义和特点
- 动态规划的基本思想和步骤
- 动态规划的分类和常见问题
- 线性动态规划
- 最长公共子序列
- 最长递增子序列
- 最大子数组和
- 区间动态规划
- 矩阵链乘法
- 括号化问题
- 背包动态规划
- 0-1背包问题
- 完全背包问题
- 多重背包问题
- 状态压缩动态规划
- 旅行商问题
- 汉密尔顿回路问题
- 线性动态规划
- 动态规划的优化技巧和实战经验
- 状态转移方程的推导和优化
- 空间复杂度的降低和滚动数组的使用
- 记忆化搜索和自底向上的比较和选择
- 动态规划与贪心算法、分治算法、回溯算法的区别和联系
动态规划是一种解决复杂问题的方法,它可以将一个问题分解为若干个子问题,并利用子问题的最优解来构造原问题的最优解。动态规划的关键是找到状态转移方程,即如何从已知的子问题的解推导出更大规模的子问题的解,直到得到原问题的解。
本文将介绍一些常见的动态规划问题的类型和解题思路,并给出一些leetcode上的练习题目。
什么是动态规划
-
动态规划的定义和特点
- 动态规划是一种将原问题分解为子问题,并利用子问题的最优解来构造原问题的最优解的方法。
- 动态规划适用于具有重叠子问题和最优子结构性质的问题,即子问题之间有相互依赖关系,且原问题的最优解可以由子问题的最优解组合而成。
- 动态规划通常采用自底向上或自顶向下的方式来求解问题,其中自底向上是从最小规模的子问题开始逐步扩大,自顶向下是从原问题开始逐步缩小。
- 动态规划通常需要借助一个数组或矩阵来存储子问题的解,以避免重复计算,这样可以提高时间效率,但也会增加空间开销。
-
动态规划的基本思想和步骤
- 动态规划的基本思想是将一个复杂问题分解为若干个简单子问题,并按照一定的顺序逐个求解,同时记录下每个子问题的解,以便后续使用。
- 动态规划的基本步骤如下:
- 定义状态:确定需要求解的变量是什么,以及它们之间有什么关系。
- 定义状态转移方程:确定如何从已知的子问题的解推导出更大规模的子问题的解,即找出递推关系式。
- 确定边界条件:确定初始状态和终止状态,以及它们对应的值。
- 实现算法:根据状态转移方程和边界条件,编写代码实现动态规划算法。
动态规划的分类和常见问题
-
线性动态规划
- 线性动态规划是指状态只与前一个或前几个状态有关,且状态转移方程只涉及加减乘除等基本运算的动态规划问题。
- 线性动态规划通常可以用一维或二维数组来存储状态,且可以通过滚动数组或压缩状态来优化空间复杂度。
- 线性动态规划常见于字符串、数组、序列等数据结构相关的问题,例如:
- 最长公共子序列:给定两个字符串 s1 和 s2 ,找出它们之间最长的公共子序列,即在两个字符串中都存在且保持相对顺序不变的最长子串。例如 s1 = “abcde” , s2 = “ace” ,则它们之间最长公共子序列为 “ace” ,长度为 3 。leetcode 1143
- 最长递增子序列:给定一个无序的整数数组,找到其中最长上升子序列的长度。例如 [10,9,2,5,3,7,101,18] ,则最长上升子序列为 [2,3,7,101] ,长度为 4 。leetcode 300
- 最大子数组和:给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。例如 [-2,1,-3,4,-1,2,1,-5,4] ,则最大和的连续子数组为 [4,-1,2,1] ,其和为 6 。leetcode 53
-
区间动态规划
- 区间动态规划是指状态表示为一个区间,且状态转移方程涉及区间内的元素或子区间的动态规划问题。
- 区间动态规划通常可以用二维数组来存储状态,且需要注意区间的遍历顺序,一般从小到大遍历区间长度,从左到右遍历区间起点。
- 区间动态规划常见于矩阵、字符串、括号等数据结构相关的问题,例如:
- 矩阵链乘法:给定一个矩阵序列,确定最佳的矩阵相乘顺序,使得相乘的计算量最小。例如 A 是 10 × 30 的矩阵, B 是 30 × 5 的矩阵, C 是 5 × 60 的矩阵,则 (AB)C 需要计算 4500 + 3000 = 7500 次乘法,而 A(BC) 需要计算 1500 + 18000 = 19500 次乘法,因此最佳的相乘顺序是 (AB)C 。leetcode 1039
- 括号化问题:给定一个由不同运算符和操作数组成的表达式,确定在哪里添加括号可以改变表达式的值,并求出所有可能的值。例如 “2-1-1” ,则可以添加括号得到 “(2-1)-1” 和 “2-(1-1)” ,它们的值分别为 0 和 2 。leetcode 241
-
背包动态规划
- 背包动态规划是指状态表示为一个背包的容量和物品的数量,且状态转移方程涉及是否选择某个物品或某些物品的动态规划问题。
- 背包动态规划通常可以用一维或二维数组来存储状态,且可以通过滚动数组或压缩状态来优化空间复杂度。
- 背包动态规划常见于组合、选择、分割等类型的问题,例如:
- 0-1背包问题:给定一组物品,每种物品都有自己的重量和价值,在限定的总重量内,选择其中若干个(也即每种物品可以选0个或1个),设计选择方案使得物品的总价值最高。例如物品有 [1,2,3] 的重量和 [6,10,12] 的价值,总重量限制为 5 ,则可以选择第一件和第三件物品,使得总价值为 18 。leetcode 416
- 完全背包问题:给定一组物品,每种物
动态规划的优化技巧和实战经验
-
分解为子问题和状态定义
- 动态规划的第一步是将原问题分解为子问题,即找出问题的最小规模,并定义状态表示子问题的解。
- 状态是动态规划中的一个重要概念,它描述了问题在某个阶段的情况,通常用一个或多个变量来表示。
- 状态的定义需要根据具体问题分析状态之间的联系,一般有以下几种常见的方法:
- 线性状态:状态只与一个变量有关,通常用一个一维数组来存储状态,例如斐波那契数列问题中,状态 f (n) f (n) 只与 n n 有关。
- 区间状态:状态与两个变量有关,通常用一个二维数组来存储状态,例如最长回文子串问题中,状态 dp [i] [j] dp[i][j] 表示字符串从 i i 到 j j 的子串是否为回文。
- 集合状态:状态与多个变量有关,通常用一个多维数组或位运算来存储状态,例如旅行商问题中,状态 dp [i] [j] dp[i][j] 表示从第 i i 个城市出发,经过集合 j j 中的城市后回到起点的最短距离。
-
状态转移方程的推导和优化
- 状态转移方程是动态规划的核心,它描述了如何从已知的子问题的解推导出更大规模的子问题的解,即找出递推关系式。
- 状态转移方程的推导需要根据具体问题分析状态之间的联系,一般有以下几种常见的方法:
- 数学归纳法:假设已知 f (n-1) f (n−1) 的解,尝试推导出 f (n) f (n) 的解,找出递推公式。
- 最优子结构法:假设已知所有子问题的最优解,尝试推导出原问题的最优解,找出最优决策。
- 边界条件法:假设已知边界条件或特殊情况下的解,尝试推导出一般情况下的解,找出递推关系。
- 状态转移方程的优化需要根据具体问题分析状态之间的依赖关系,一般有以下几种常见的方法:
- 状态压缩法:将多维状态压缩为一维或二维状态,减少状态空间,降低空间复杂度。
- 滚动数组法:将二维状态压缩为一维状态,并利用两个或三个变量交替更新状态,降低空间复杂度。
- 区间调整法:根据状态转移方程调整区间遍历的顺序和范围,避免无效计算,降低时间复杂度。
-
动态规划的实现和优化
- 动态规划的实现通常有两种方式:自顶向下和自底向上。
- 自顶向下即记忆化递归,是从原问题开始逐步缩小,直到求解基础问题。这种方式通常使用递归来实现,可以利用一个数组或矩阵来保存已经计算过的子问题的解,避免重复计算。这种方式适用于子问题之间没有明确的依赖关系,且子问题的规模和范围都是不确定的情况。
- 自底向上即迭代法,是从最小规模的子问题开始逐步扩大,直到求解原问题。这种方式通常使用循环来实现,可以保证每个子问题都被计算,并且按照正确的顺序计算。这种方式适用于子问题之间有明确的依赖关系,且子问题的规模和范围都是确定的情况。
- 动态规划的优化通常有以下几种方法:
- 状态压缩法:将多维状态压缩为一维或二维状态,减少状态空间,降低空间复杂度。
- 滚动数组法:将二维状态压缩为一维状态,并利用两个或三个变量交替更新状态,降低空间复杂度。
- 原地修改法:直接在原始数据结构上修改状态,而不额外开辟空间,降低空间复杂度。
-
动态规划与贪心算法、分治算法、回溯算法的区别和联系
- 动态规划与贪心算法、分治算法、回溯算法都是常见的算法思想,它们之间有一些区别和联系。
- 动态规划与贪心算法都是通过组合子问题的最优解来得到原问题的最优解,但贪心算法只能保证局部最优,并不能保证全局最优。贪心算法适用于具有最优子结构和贪心选择性质的问题,即每个子问题的最优解不依赖于其他子问题的选择,而且贪心选择可以导致全局最优。动态规划适用于具有最优子结构和重叠子问题性质的问题,即每个子问题的最优解可能依赖于其他子问题的选择,而且重叠子问题需要避免重复计算。
- 动态规划与分治算法都是通过将原问题分解为子问题,然后组合子问题的解来得到原问题的解。但分治算法的子问题是互不相交的,即每个子问题只被计算一次,而动态规划的子问题是有重叠的,即每个子问题可能被计算多次。分治算法适用于具有分治性质的问题,即原问题可以分解为若干个相互独立的子问题。动态规划适用于具有最优子结构和重叠子问题性质的问题,即原问题可以分解为若干个相互依赖的子问题。
- 动态规划与回溯算法都是通过尝试不同的选择来得到原问题的解,但回溯算法是一种暴力搜索方法,它会遍历所有可能的选择,并且在发现某个选择不可行时回退到上一步继续尝试。动态规划是一种优化搜索方法,它会利用已知的子问题的解来剪枝或查表,避免无效或重复的搜索。回溯算法适用于没有明确求解策略或没有有效剪枝方法的问题,例如八皇后、数独等。动态规划适用于具有最优子结构和重叠子问题性质的问题,例如背包、编辑距离等。