转载文章,原文章来源于算法设计与分析基础系列--动态规划(一)--币值最大化问题(欢迎关注微信公众号,会定期更新内容)
============================================================
前言
本文内容基于书籍"算法设计与分析基础"(Introduction to The Design and Analysis of Algorithms,作者Anany Levitin),主要学习和讨论其中的动态规划算法。
"如果问题是由交叠的子问题构成的,我们就可以用动态规划技术来解决它。一般来说,这样的子问题出现在对给定问题求解的递推关系中,这个递推关系中包含了相同类型的更小子问题的解。动态规划法建议,与其对交叠的子问题一次又一次地求解,还不如对较小的子问题只求解一次并把结果记录在表中,这样就可以从表中得到原始问题的解。"
上面对动态规划思想进行了一个比较"理论"的解释和说明,可能初学者会觉得稍微有些晦涩难懂。不过没有关系,随着不断地练习相关的题目,自然而然会对动态规划思想逐步形成更加直观或者"直觉"上的认知和理解,到时候再回来看这里的"理论"描述,就会建立更加深刻的认识。
比如动态规划思想就可以实现斐波那契数列的计算和连续子数组最大和问题,见"剑指offer"中的经典算法面试题解析--斐波那契数列和"剑指offer"中的经典算法面试题--连续子数组最大和
问题描述--币值最大化问题
给定一排n个硬币,其面值均为正整数c[1], c[2],..., c[n],这些整数并不一定两两不同。请问如何选择硬币,使得其在原始位置互不相邻的条件下,所选硬币的总金额最大。
解答
这道题算是一个较为经典的动态规划思想的入门题目,也可能会在一些面试题中出现。
我们先来大概说一下笔者对这个题的思考过程,期望是能带来一些一般性的启发。
如何确定选择方案可以使得总金额最大呢?其实关于这个最佳方案的选择,我们并没有任何特别清晰的线索,而且似乎也很难通过数学推导或者其他分析方法得出一种明确的规则。这个时候常见的一种思考方式是,先不要考虑"一步到位",即先不要思考如何直接确定最优的硬币选择组合,而是逐个考虑每一步的硬币选择。具体来说,就是我们依次考虑第一步,第二步,第三步等等,每一步应该选择哪个硬币可以实现最终方案的最优。之所以这么想,是因为,假设最终方案是选择c[1], c[4], c[10],那么这个过程可以看做是我们按照顺序依次选择了第一个,第四个,第十个硬币的结果(当然也可以看做是按照顺序依次选择了第四个,第一个,第十个硬币,这里具体的顺序其实不影响)。也就是说,最优方案一定可以看做是按照某种顺序依次选择某个硬币的过程。反之也是成立的,即我们一定可以通过"逐步选择"的方法来得到最优解。
当我们想到"逐步选择"的时候,接下来很自然的思考是第一步我们应该选择哪个硬币呢?其实,关于这个问题我们依然没有很好的办法,也很难通过数学分析来得出明确的结论。这个时候,我们就要往"暴力枚举"的方向去想了。第一步选择有n种可能,假设,我们第一步选择了c[5],那么接下来我们需要从c[1], c[2], c[3], c[7], c[8],..,c[n]中进行第二步选择(注意不能选择相邻硬币,所以4和6被剔除了),以此类推直到最后一次选择为止。但是,如果这样暴力枚举下去,整个复杂度是非常高的(大约是n的阶乘量级),我们还是要想想其他办法看能否降低这个复杂度。
我们再看一下上面第一步选择了c[5]之后的情况,其实,当第二步我们从c[1], c[2], c[3], c[7], c[8],..,c[n]中进行选择的时候,这个问题和最开始那个从c[1], c[2],...,c[n]中进行选择的问题是"完全类似"的,唯一不同的就是"规模"变小了,即备选硬币的数量(或者说集合)少了一个。这里其实已经符合动态规划思想中所说的"子问题出现在对给定问题求解的递推关系中,这个递推关系中包含了相同类型的更小子问题的解"。对于这个题来说,就是当我们确定了每一步选择哪个硬币之后,下一步选择时所需要解决的问题是同样的只是规模减小了的"硬币选择问题"。
有了上面的思考,我们就可以尝试用动态规划来解决这个问题了。动态规划思想中的一个难点是如何定义"状态",所谓的状态其实就是对当前要处理的硬币选择问题的一种描述,即此刻的硬币选择问题是"什么样"的。对于这类"选择硬币或者若干元素求取极值"问题,一种常见的思想是将当前问题描述成"使用前n个硬币进行选择时能得到的最优解"。一般的,在很多动态规划问题中都会用"使用前n个元素进行选择时能得到的最优解",来作为状态定义。
接着,一般会定义一个数组dp[state]表示当前状态下的最优解。具体到这个题目,就是dp[n],表示"使用前n个硬币进行选择时能得到的最优解",这里n就是"状态"的描述,而dp[n]的取值就是该"状态"下的最优解。
最后一步,就是找到状态转移公式,计算最终答案。我们先看下什么叫状态转移公式?先回顾一下,我们上面曾提到过的"第一步"选择哪个硬币这件事。当我们做出选择之后,就会再次遇到一个规模更小的"类似问题",这个就是状态转移。更一般的,我们需要知道在当前规模问题下,做出某种选择之后,接下来要面对的是什么状态,以及根据接下来要面对状态的答案来更新当前规模问题下的答案。具体到这个题目,最初问题的状态是"使用前n个硬币进行选择时能得到的最优解",然后我们要做出一步选择。这里的选择方式也是有一个常见思路的,就是看第n个硬币是否要选择。如果我们选择了第n个硬币,那么我们可以获得金额c[n],同时接下来问题的状态就变成"使用前n-2个硬币进行选择时能得到的最优解"(因为题目要求选择的硬币不能相邻,所以第n-1个硬币是不能被选择的,需要剔除它),而该状态的最优解就是dp[n-2](根据我们的定义)。反之,如果我们没有选择第n个硬币,那么可以获得金额0,同时接下来问题的状态就变成"使用前n-1个硬币进行选择时能得到的最优解"(因为第n个硬币没有选,所以第n-1个硬币并没有被剔除),而该状态的最优解就是dp[n-1]。显然,我们需要从这两种选择中取金额最大的那个,即max(c[n] + dp[n-2], dp[n - 1])
代码如下
int main() {
dp[0] = 0;// 使用前0个硬币进行选择时能得到的最大金额就是0
dp[1] = c[1];// 使用前1个硬币进行选择时能得到的最大金额是c[1]
for (int i = 2; i <= n; i++) {
dp[i] = max(c[i] + dp[i - 2], dp[i - 1]);// 根据公式迭代计算
}
int answer = dp[n];// 最大金额就是dp[n]
}
总结
1,一个问题是否可以使用动态规划算法求解的关键点在于"如果问题是由交叠的子问题构成的,我们就可以用动态规划技术来解决它",这里需要慢慢体会。
2,动态规划问题在状态定义上是有些技巧的,好的定义可以让转移公式变得简洁明了,这个也只能通过不断地练习来获得更加深刻的理解。
欢迎大家多多转载并关注后续更新,如果有其他算法相关的问题也欢迎留言讨论~