什么叫做动态规划
动态规划,即dp,dynamic programming。
- 这里的
programming
不是编程的意思,而是决策。但是这种决策并不是一下就出来的,而是一步步积累出来的。 - 换句话说,我们需要一个决策,但是这个决策太大了,我们做不来,所以需要把它递归到我们可以简单的做出决策的状态,然后从这些状态开始,慢慢的动态的演进到最终的决策
举个例子:
- 用最少的硬币换零钱,突然和你说要换78分钱,你怎么就能迅速给出答案呢?你不能,但是如果是1分的话,你就可以;如果是2分的话,就是在1分的基础上再加1分,你也可以。于是你就慢慢地从1分开始一直算到78就有答案了。从另一个角度说,如果你用DP算出了怎么换78分,那如果我问你76分怎么换,你也应该有答案了。
- 所以在DP的实践中很重要的就是递推关系和边界条件。
- 所谓边界条件就是最简单的情况
- 所谓递推关系就是如果你已经知道这么多,再多给你一个,你怎么得到。也就是常说的状态转移方程
- 说一个最最最简单的例子,找出一个数组中的最大值。这个问题边界条件是什么呢,就是如果只有一个元素,那最大值就是他;递推关系是什么,就是你已经知道当下最大的数,再多给你一个数你怎么办。你会拿这个数和当下最大的数去比,其中较大的那个就是新的最大的数。这就是典型dp的思想。所以不要把DP看的过于高深就好了。
动态规划问题的一般形式就是求最值。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用的比较多。比如求最小编辑距离、最长递增子序列等
既然是要求最值,核心问题又是什么呢?求解动态规划的核心问题是穷举。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值。
- 首先,动态规划的穷举有些特别,因为这类问题存在[重叠子问题],如果暴力穷举的话效率会非常低下,所以需要使用[备忘录]或者[DP table来优化穷举过程],避免不必要的计算
- 而且,动态规划问题一定会具备[最优子结构],才能通过子问题的最值得到原问题的最值。
- 另外,虽然动态规划的思想核心就是穷举求最值,但是问题千变万化,穷举所有可行解并不是一件容易的事情,只有列出正确的[状态转移方程]才能正确的穷举。这是最难的一步。
贪心、穷举、动态规划
我们先明确一个点,计算机的所有解决问题都是穷举,算法就是聪明的进行穷举
理论
假设我们面对一个求最优解或统计之类的问题,这个问题基于“我们要模拟完成一个大任务”,这个大任务可以分成若干步骤,每个步骤有若干种决策,每个步骤完成后,就到达了一个阶段性状态
比如,你要从A地到Z地,没有直达,所以第一步需要到一个中间地点,比如H或I,第二步再前进,比如到P或Q,最后到达Z,每一步有若干决策,比如第一步你可以决定到H或I的中的某个,大致就是这样一个模型,可以自己画个地图看看
等等,你大概发现问题了,如果第一步到H和I都可以,第二步到P和Q都可以,那我每一步只选最优,不就用贪心得到结果了吗,没错,如果你需要经历的每个阶段状态跟决策无关,那就贪心得到结果好了,理解贪心了吗:)
然而现实情况可能是,你第一步的选择会影响后面的分支,比如你第一步可以选择到H或I,但是到了H后,你只能选择经过P或Q到Z了,而如果到了I,你只能选择R或S到Z,这样一来,即便第一步到H或I你选择了较好的一条路,也不保证最终结果最优,因为比如你选了H,那万一I-R-Z的路要比H开始到Z的路径短了更多,最优路径可能是A-I-R-Z,所以你要把这些路都尝试一遍,才知道哪个最优,理解穷举了么?:)
OK,我们稍微改下题设,假如从I出发不是到R和S,而是到Q或R(中间有重合的地方),会如何
诚然,我们可以用穷举每条路来解决这个问题,需要穷举的路径数和上面的图一样,但是,我们可以有更快的办法,
- 你不用将A-H-Q-Z和A-I-Q-Z两条路单独计算,因为他们有状态交点,结合第一张图的思想,可以敏锐地感觉到,我们只需要计算到每个有共同状态的位置求各阶段的最优,最后每阶段选最优组合贪心组合起来就行,因为各阶段完成的状态点是大家都有的嘛
- 因此,咱们先计算A-H-Q和A-I-Q,选个最好的,然后跟Q到Z中最好的拼起来,就是最优了,没必要把所有路径都搞一遍(虽然图里面Q到Z直达,但你可以发挥想象力,将其想象成各种分支的一条复杂道路)
因此,我们可以从A开始,向Z进行BFS,并对BFS中每个点保存最优状态,如果有不同的路径BFS到了同一个点,留最好的一条就行,比如上面这个,你的算法可能先从A-H-Q搜到了Q这个位置,之后从A-I-Q又到了这里,留最好的一条,最后一轮从PQR三个点到Z,就结束了,相对第二章图要少一次运算
如果你理解了,恭喜你已经能有效解决很多需要DP的问题了,同时还学会了解图论的单源最短路径问题呢
小结
一个问题是该用递推、贪心、搜索(穷举)还是动态规划,完全是由这个问题本身阶段间状态的转移方式决定的!
- 每个阶段只有一个状态-----递推
- 每个阶段的最优状态都是由上一个阶段的最优状态得到—贪心
- 每个阶段的最优状态都是由之前所有阶段的状态的组合得到–穷举
- 每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到而不管之前这个状态是如何得到——动态规划。
每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到,这个性质叫做最优子结构,不管之前这个状态是如何得到。
关于状态
- 当你企图使用计算机解决一个算法问题时,你其实就是在思考如何将这个问题表示为状态(用哪些遍历存储哪些数据)以及如何在状态中转义(怎样根据一些变量计算出另一些变量)。
- 所以所谓的空间复杂度就是为了支持你的计算所必须存储的状态最多都多少,所谓的时间复杂度就是从初始状态到达最终状态中间需要多少步。
贪心和dp经常混淆
简单区别就是
- 动态规划中每一个状态一定是由上一个状态推导出来的
- 贪心没有状态推导,而是从局部直接选最优的
记住这一点就好啦。
举个例子。对于01背包,即有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
- 动态规划中
dp[j]
是由dp[j - weight[i]]
推导出来的,然后取max(dp[j], dp[j - weight[i]] + value[i])
- 但是如果是贪心呢?每次拿物品的一个最大的会在最小的就完事了,和上一个状态没有关系
所以贪心解决不了动态规划的问题。
刷题
这道题是否能够用动态规划来解决
什么样的问题适合用动态规划来解决呢?换句话说,动态规划能解决的问题有什么规律可循呢?可以用“一个模型三个特性”来回答这个问题。
(1)什么是一个模型?
- 它指的是动态规划适合解决的问题的模型,即多阶段决策最优解模型
- 我们一般是用动态规划来解决最优问题。而仅仅问题的过程,需要经历多个决策阶段。每个决策阶段都对应着一组状态,然后我们寻址一组决策序列,经过这组决策序列,能够产生最终期望求解的最优值
(2)什么是“三个类型”
- 最优子结构指的是
- 我们可以通过子问题的最优解,推导出问题的最优解。
- 无后效性有两层含义。
- 第一层含义是,在推导后面阶段的状态的时候,我们只关心前面阶段的状态值,不关心这个状态是怎么一步一步推导出来的。
- 第二层含义是,某阶段状态一旦确定,就不受之后阶段的决策影响。
- 重复子问题。
- 用一句话概况一下就是不同的决策序列,到达某个相同阶段时,可能会产生重复的状态
动态规划的解题步骤
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
为什么要先确定递推公式,然后在考虑初始化呢?因为一些情况是递推公式决定了dp数组要如何初始化!
小结
动态规划总结:
- 计算机解决问题其实没有任何奇技淫巧,它唯一的解决办法就是穷举,穷举所有可能性。算法设计无非是先思考“如何穷举”,然后再追求“如何聪明的穷举”。(穷举一般有两种方向:自下而上、自上往下)
- 列出状态转移方程,就是在解决“如何穷举”的问题。之所以说它难,⼀是因为很多穷举需要递归实现,⼆是因为有的问题本⾝的解空间复杂,不那么容易穷举完整。
- 备忘录、dp table就是在追求“如何聪明的穷举”。⽤空间换时间的思路,是降低时间复杂度的不⼆法门
背包问题
背包问题 (Knapsack problem) 是一种组合优化的 NP (NP-Complete) 完全问题。
什么是NP完全问题
我们可以将NP完全问题简单理解为[无法直接求解]的问题。
比如[分解质因数]问题,我们无法像四则运算(加减乘除)那样,按照特定的逻辑进行求解。
只能通过[穷举] + [验证]的方式求解。
问题描述
问题可以描述为:
- 书面语言描述:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。
- 说句人话就是: 给定一个背包容量target以及一组数组nums,能否按照一定方式选取nums中的元素得到target
- 背包容量target和物品nums的类型可能是数,也可能是字符串
- target可能题目已经给出(显式),也可能是需要我们从题目的信息中挖掘出来(
- 选取方式有常见的一下几种:每个元素选一次/每个元素选多次/选元素进行排列组合
- 数学语言描述: 我们有 n 件物品和一个容量 (capacity)为 C的背包,记第 i件物品的重量 (weight) 为
w
i
w_i
wi ,价值 (value)为
v
i
v_i
vi ,求将哪些物品装入背包可使价值总和最大。
- 0-1背包: 如果限定每件物品最多只能选取 1 次(即 0或\1),则问题称为 0-1背包问题。
- 完全背包: 如果每件物品最多可以选取无限次,则问题称为 完全背包问题。
背包问题可以分为如下几类:
- 0/1背包问题:每个元素最多选取一次
- 完全背包问题:每个元素可以被重复选择
- 组合背包问题:背包中的物品可以考虑顺序
- 分组背包问题:不止一个背包,需要遍历每个背包
而每个背包问题要求的也是不同的,按照问题分类:
- 最值问题:要求最大值/最小值
- 存在问题:是否存在…………,满足…………
- 组合问题:求所有满足……的排列组合
因此把背包类型和问题类型结合起来就会出现以下细分的题目类型:
- 1、0/1背包最值问题
- 2、0/1背包存在问题
- 3、0/1背包组合问题
- 4、完全背包最值问题
- 5、完全背包存在问题
- 6、完全背包组合问题
- 7、分组背包最值问题
- 8、分组背包存在问题
- 9、分组背包组合问题
背包问题本质上可以是一个组合问题:被选物品之间不需要满足特定关系,只需要选择物品,以达到「全局最优」或者「特定状态」即可。
- 0-1背包 和 完全背包 是两种最为常见的背包问题.。
- 「0-1 背包」是「完全背包」的基础。01 背包问题与完全背包问题主要区别就是物品是否可以重复选取。
- 动态规划 是解决「0−1 背包问题」和「完全背包问题」的标准做法。
动态规划刷题总结
坐标型动态规划(重点)
理论
- 最简单的动态规划类型。
- 题目一般是:
- 给定输入为序列或者网格/矩阵,需要找到序列中某个/某些子序列或网格中的某条路径
- 某种性质最大/最小
- 计数
- 存在性
- 给定输入为序列或者网格/矩阵,需要找到序列中某个/某些子序列或网格中的某条路径
- 动态规划方程中:
- 动态规划状态下标为序列下标i或者网格坐标(i,j)
- d p [ i ] dp[i] dp[i]:从起点走到坐标 i i i( 以 a i a_i ai个元素结尾)的某种性质
- d p i ] [ j ] dpi][j] dpi][j]:从起点走到坐标 ( i , j ) (i,j) (i,j)的路径的性质
- 性质:
- 最大值
- 最小值
- 个数
- 是否存在
- 方程:研究走到x,y这个点之前的一步
- 动态规划状态下标为序列下标i或者网格坐标(i,j)
- 初始化:设置起点,设置f[0]的值/f[0][0…n-1]的值
- 答案:终点
- 二维空间优化:如果f[i][j]的值只依赖于当前行和前一行,则可以用滚动数组节省空间
刷题
- leetcode:263. 丑数 — 判断是不是丑数。这不是动态规划,是下面题的前置
- leetcode:264. 丑数 II — 返回第i个丑数
- leetcode:70. Climbing Stairs 爬楼梯 — 到达楼顶有几种方法
- lintcode:272. Climbing Stairs II 爬楼梯 II — 到达楼顶有几种方法
- lintcode:514 · 栅栏染色 — 一共有多少种染色方法
- leetcode:343. integer-break 整数拆分 分拆数字i,可以得到的最大乘积
- leetcode:96. 不同的二叉搜索树
- leetcode:62. Unique Path 不同路径 – 到达右下角有多少种方法
- leetcode:63. Unique Path II 不同路径 II – 到达右下角有多少种方法
- leetcode:746. Min Cost Climbing Stairs 使用最小花费爬楼梯 — 到达楼顶的权值最小,坐标型动态规划
- leetcode:64. Minimum Path Sum–最小路径和 – 到达右下角权值最小,坐标型动态规划
- lintcode:109.Triangle 数字三角形 – 到达底部权值最小,坐标型动态规划
- leetcode:剑指 Offer 60. n个骰子的点数 — 每一个target出现的概率
- leetcode:361.轰炸敌人 — 一个炸弹最多能炸死多少人
序列型动态规划,重点
(2)序列型动态规划,重点
- 给定一个序列
- 状态:方程
d
p
[
i
]
dp[i]
dp[i]中的下标
i
i
i表示前
i
i
i个元素
a
[
0
]
、
a
[
1
]
、
…
a
[
i
−
1
]
a[0]、a[1]、…a[i-1]
a[0]、a[1]、…a[i−1]的某种性质
- 坐标型: d p [ i ] dp[i] dp[i]:从起点走到坐标i( 以 a i a_i ai为元素结尾)的某种性质
- 初始化中,
d
p
[
0
]
dp[0]
dp[0]表示空序列的性质
- 坐标型动态规划的初始条件 d p [ 0 ] dp[0] dp[0]就是指以 a 0 i a_0i a0i为结尾的子序列的性质
(2.1) 单序列型动态规划
-
方程: d p [ i ] = d p ( d p [ j ] ) dp[i] = dp(dp[j]) dp[i]=dp(dp[j]), j是i之前的一个位置;
-
初始化: d p [ 0 ] dp[0] dp[0]; 答案 d p [ n − 1 ] dp[n-1] dp[n−1]
-
小技巧: 一般有N个数字/字符, 就开N+1个位置的数组, 第0个位置单独留出来作初始化.(跟坐标相关的动态规划除外)
-
leetcode:673. number-of-longest-increasing-subsequence最长递增子序列的个数
- leetcode:53. Maximum Subarray 最大子数组和
- 44.Minimum Subarray
- leetcode:256. Paint House 粉刷房子
- leetcode:265. Paint House II 粉刷房子 II
- leetcode:198. House Robber · 打家劫舍
- leetcode:213. House Robber II · 打家劫舍 II
- leetcode:132. Palindrome Partitioning II 分割回文串 II
- leetcode:55. jump-game · 跳跃游戏
- 45. jump-game II · 跳跃游戏 II
- 139. Word Break · 单词拆分
(2.2)双序列动态规划
- 状态: f[i][j]表示第一个sequence的前i个数字/字符, 配上第二个sequence的前j个;
- 方程: f[i][j] = 研究第i个和第j个的匹配关系;
- 初始化: f[i][0]和f[0][i]; 答案: f[n][m], 其中n = s1.length(); m = s2.length();
- leetcode:1143. Longest Common Subsequence 最长公共子序列
- leetcode:72. Edit Distance 编辑距离
- leetcode:161.相隔为1的编辑距离
- leetcode:115. Distinct Subsequence 不同的子序列
- leetcode:97. Interleaving String 交错字符串
(3)划分型动态规划,重点
- 状态: f[i]表示前i个元素的最大值; 方程: f[i] = 前i个元素里面选一个区间的最大值; 初始化: f[0]; 答案: f[n - 1]
- leetcode:279 perfect-squares 完全平方数
- leetcode:132. Palindrome Partitioning II 分割回文串 II
- leetcode:买卖股票
- lintcode:Maximum Subarray III 最大子数组 III
(4)区间型动态规划,重点
- 区间DP是一类在区间上进行动态规划的最优问题。
- 什么是区间呢?以一组数列为例,那么[i][j]表示的就是i到j的这个区间。
- 区间dp,一般是枚举区间,把区间分成左右两部分,然后求出左右区间再合并
- 区间一般是根据问题设计出一个表示状态的dp,可以是二维的,也可以是三维的,一般情况下为二维,
- 然后将子问题划分为两个子问题,也就是一段区间分为左右两个区间,然后将左右两个区间合并到整个区间,或者说局部最优解合并为全局最优解,然后得解
- 特点:
- 1). 求一段区间的解max/min/count;
- 2). 转移方程通过区间更新;
- 3). 从大到小的更新;
- 这种题目共性就是区间最后求[0, n-1]这样一个区间逆向思维分析, 从大到小就能迎刃而解
- 区间型动态规划的递归关系,通常是呈现出去头去尾的特点,以下面的伪代码为例。
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) //区间[i][j]由去掉头(或者去掉尾)的[i + 1][j](dp[i][j - 1])推出
- 基于此种特性,区间型DP的代码实现通常要对区间长度进行循环,即从区间长度短的循环到区间长度长的,当求当前状态的值的时由于使用到的子状态的区间长度都小于当前状态,因此都可以推出,非常的合理且符合直觉,见下面伪代码。
for (int i = 1; i <= len; i++) //循环区间长度
for (int j = 0; j <= n - len; j++) //循环做端点
for (int z = j + 1; z < n; z++) //循环右端点
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); //转移方程
(5)背包型动态规划
- 特点: 1). 用值作为DP维度, 2). DP过程就是填写矩阵, 3). 可以滚动数组优化
- 状态: f[i][S]前i个物品, 取出一些能否组成和为S; 方程: f[i][S] = f[i-1][S-a[i]] or f[i-1][S]; 初始化: f[i][0]=true; f[0][1…target]=false; 答案: 检查所有f[n][j]
(6)最长序列型动态规划
(8)综合型动态规划
- 树形dp是建立在树这种数据结构上的dp,一般状态比较好想,通过dfs维护从根到叶子或从叶子到根的状态转移。
- 数位dp,主要用来解决统计满足某类特殊关系或有某些特点的区间内的数的个数,它是按位来进行计数统计的,可以保存子状态,速度较快。数位dp做多了后,套路基本上都差不多,关键把要保存的状态给抽象出来,保存下来。
按照背包类型
(1)01背包
- lintcode:89 · K数之和
- 有一个不重复的nums[i],每个数只能使用一次,要求找出k个数,刚好等于目标amount,问有多少种方案
- leetcode:474.一和零
- leetcode:418. 目标和
(2)完全背包
- leetcode:322. 零钱兑换
- 有不同面额的硬币数组coins,每个硬币可以使用无限次,要求找出最少的硬币个数刚好等于目标amount
(3)多重背包
按照背包类型
(1)刚好将背包装满
- leetcode:322. 零钱兑换
- 有不同面额的硬币数组coins,每个硬币可以使用无限次,要求找出最少的硬币个数刚好等于目标amount
- 完全背包、最值型动态规划
- lintcode:89 · K数之和
- 有一个不重复的nums[i],每个数只能使用一次,要求找出k个数,刚好等于目标target,问有多少种方案
- 01背包、记数型动态规划
(2)不要求将背包装满
- lintcode:92 背包问题
- 给定一些物品装入背包,每一个物品只能使用一次,能够获得的最大重量
- 01背包、最值型动态规划
- lintcode:125 背包问题 II
- 给定一些物品装入背包,每一个物品只能使用一次,能装入背包的最大价值是多少?
- 01背包、最值型动态规划
- lintcode:563 背包问题 V
- 给出 n 个物品, 以及一个数组,nums[i]代表第i个物品的大小, 保证大小均为正数并且没有重复, 正整数 target 表示背包的大小,每一个物品只能使用一次,找到能填满背包的方案数
- 01背包、记数型背包
- lintcode:800 · 背包问题 IX
- 你总共有 n 万元,希望申请国外的大学,要申请的话需要交一定的申请费用,给出每个大学的申请费用以及你得到这个大学offer的成功概率,大学的数量是 m。如果经济条件允许,你可以申请多所大学。找到获得至少一份offer最高可能性
- 01背包变形问题
- lintcode:440 背包问题 III
- 给定一些物品装入背包,每一个物品可以使用无数次,能装入背包的最大价值是多少
- 完全背包、最值型背包
- lintcode:562 · 背包问题 IV
- 给出 n 个物品, 以及一个数组,nums[i]代表第i个物品的大小, 保证大小均为正数并且没有重复, 正整数 target 表示背包的大小,每一个物品可以使用无数次,找到能填满背包的方案数
- 完全背包、记数型背包
- lintcode:564 · 组合总和 IV
- 对于一个无重复数字的nums数组,找出所有和为target的组合个数。 每一个数可以使用无限次,数的顺序不同表示不同组合
- 无穷背包、记数型背包
- lintcode:798 背包问题VII
- 给定一些物品装入背包,每个物品有数量限制,能装入背包的最大价值是多少?
- 多重背包、最值型背包
-
- 给一些不同价值和数量的硬币。找出这些硬币可以组合在1 ~ n范围内的值的数量 。每个硬币价值为value[i],每个硬币数量为amount[i]。
- 多重背包、记数型背包
-
- 你总共有 n 元,商人总共有三种商品,它们的价格分别是150元,250元,350元,三种商品的数量可以认为是无限多的,购买完商品以后需要将剩下的钱给商人作为小费,求最少需要给商人多少小费。
- 完全背包、最值型背包
- 有两个变量:坐标、走法。
- 题目要求走到右下角一共有几种走法,也就是说走法是dp(xxx)的返回值,变化维度只有一个。
- 但是坐标有x和y,因此需要双重for循环,以及f(x, y)
- 和上面一样,和上面不同的是,路径中可能有障碍
- 有两个变量:要拆解成k个数(k>=2),这些数的最大乘积。
- 题目要求最大乘积,也就是乘积是dp(xxxx)的返回值。
- 即:变化维度只有一个,也就是用一个一维数组dp[i]就足以描述了。
- 也就是说对于一个n来说,只需要一维数组,一层遍历所有的子结构即可。
- 但是为了求n,需要求n-1的子问题,…3、2、1。所以还是双重遍历
- 有三个变量:可选金矿限制、可选工人数量限制、得到的最大价值
- 题目要求价值最大,也就是价值是dp(xxx)的返回值。
- 即变化维度有两个,所以需要用一个二维数组dp(n,w)来描述
- 为遍历所有可能,需要双重for循环
- 有三个变量:可选物品限制、背包的容量限制、得到的最大价值
- 题目要求价值最大,也就是价值是dp(xxx)的返回值
- 即变化维度有两个,所以需要用一个二维数组dp(n,w)来描述
- 为遍历所有可能,需要双重for循环