动态规划
介绍
动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。20 世纪 50 年代初,美国数学家贝尔曼(R.Bellman)等人在研究多阶段决策过程的优化问题时,提出了著名的最优化原理,从而创立了动态规划。动态规划的应用极其广泛,包括工程技术、经济、工业生产、军事以及自动化控制等领域,并在背包问题、生产经营问题、资金管理问题、资源分配问题、最短路径问题和复杂系统可靠性问题等中取得了显著的效果。(来自百度百科)
简单来说,就是把问题分成多个阶段,每个阶段都符合一个式子(状态转移),后面阶段的状态(结果)一般可以由前面阶段的状态(结果)转移而来。
找出状态转移方程,动态规划已经成功了 90%。使用动态规划求解时,最关键的是找出状态转移方程,而想要找出状态转移方程,首先要对问题状态有清晰的定义。一般来说,动态规划求解主要包括以下步骤:
- 划分阶段:把问题划分成子问题,类似于分治法,每个阶段一般是有序的。
- 定义状态:每个阶段,都有属于自己的最优状态,每一个阶段的最优状态,可以从前面阶段的最优状态中转化获得。
- 状态转化:不同阶段之间的转化关系,就是状态转移方程,定义好它们之间的递推关系,就是极其重要的一步。
- 逆向求解:一般来说我们要求解一个状态,需要先把前面的状态先求解。那么逆向就是自底向上,也就是先挨个把前面的状态求解,再使用前面的状态求解后面的状态。(注意最初的状态定义必须准确,边界值需要处理好)
动态规划一般是用来求解最优解的,这类问题一般有很多种解,但是我们期望的是找出其中的一个最优解。动态规划的厉害之处,在于分解大的问题,并且找出了递推的式子,能够利用前面的状态不断求解出后面的状态。
零钱兑换
问题描述
给定不同面额的硬币 coins 和一个总金额 amount,需要你编写一个函数计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,则返回 -1。你可以认为每种硬币的数量是无限的。
输入:coins = [1, 2, 5],amount = 11
输出:3
解释:11 元可以拆分为 5 + 5 + 1,这是最少的硬币数目。
求解思路
利用 DP 求解问题时,不要一开始就去想第一步具体做什么!那么应该从哪里着手呢?答案是:最后一步!
兑换硬币的时候,假设每一步操作总是选择一个硬币,那么我们看一下最后一步如何达到 amount?
以给定的输入为例:
coins = [1, 2, 5], amount = 11
最后一步可以通过以下 3 个选项得到:
已经用硬币兑换好了 10 元,再添加 1 个 1 元的硬币,凑成 11 元;
已经用硬币兑换好了 9 元,再添加 1 个 2 元的硬币,凑成 11 元;
已经用硬币兑换好了 6 元,再添加 1 个 5 元的硬币,凑成 11 元。
接下来,应该立即将以上 3 个选项中的未知项展开成子问题!
子问题
拿到 3 个选项之后,你可能会想:[10元,9元,6元] 是如何得到?到此时,一定不要尝试递归地去求解 10 元、9 元、6 元,正确的做法是将它们表达为 3 个子问题:
如何利用最少的硬币组成 10 元?
如何利用最少的硬币组成 9 元?
如何利用最少的硬币组成 6 元?
我们原来的问题是,如何用最少的硬币组成 11 元。
不难发现,如果用 f(x) 表示如何利用最少的硬币组成 x 元,就可以用 f(x) 将原问题与 3 个子问题统一起来,得到如下内容:
原问题表达为 f(11);
3 个子问题分别表达为 f(10)、f(9)、f(6)。
接下来我们再利用 f(x) 表示最后一步的 3 个选项:
f(10) + 1 个 1 元得到 f(11);
f(9) + 1 个 2 元得到 f(11);
f(6) + 1 个 5 元得到 f(11)。
递推关系
最后一步,可以通过 3 个选项得到。哪一个选项才是最少的步骤呢?这个时候,我们可以采用一个 min 函数来从这 3 个选项中得到最小值。
f(11) = min(f(11-1), f(11-2), f(11-5)) + 1
接下来,第一次替换:只需要将 11 换成一个更普通的值,就可以得到更加通用的递推关系:
f(x) = min(f(x-1), f(x-2), f(x-5)) + 1
当然,这里 [1, 2, 5] 我们依然使用的是输入示例,进行第二次替换:
f(x) = min(f(x-y), y in coins) + 1
代码实现
/** |
分割等和子集
问题描述
一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意: 1)每个数组中的元素不会超过 100;2)数组的大小不会超过 200
输入:A = [2, 2]
输出:true
解释:可以将这个数组分成 [2], [2] 这两个子集和是相等的。
求解思路
由于分出来的两个子集和要求是相等的,如果整个数组和为奇数,那么没有讨论的必要。这里我们只讨论 sum(A) = 2 x V 的情况。也就是分出来的两个子集,其和分别为 V。
这个问题,也可以想象成:
有一个容量为 V 的背包,给你一些石头,石头的大小放在数组 A[] 中,现在需要捡一些石头,刚好装满容量为 V 的背包。(你可以不去考虑填充的时候的缝隙)
代码实现
var canPartition=function(nums){ |