https://www.zhihu.com/question/23995189
动态规划问题的一般形式就是求最值
也需要基本的case
求解动态规划的核心问题是穷举。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值呗。
动态规划的穷举有点特别,因为这类问题存在「重叠子问题」,如果暴力穷举的话效率会极其低下,所以需要「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。
而且,动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。
另外,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,只有列出正确的「状态转移方程」才能正确地穷举。
以上提到的重叠子问题、最优子结构、状态转移方程就是动态规划三要素。
具体什么意思等会会举例详解,但是在实际的算法问题中,写出状态转移方程是最困难的,这也就是为什么很多朋友觉得动态规划问题困难的原因,我来提供我研究出来的一个思维框架,辅助你思考状态转移方程:
明确「状态」 -> 定义 dp 数组/函数的含义 -> 明确「选择」-> 明确 base case。
下面通过斐波那契数列问题和凑零钱问题来详解动态规划的基本原理。前者主要是让你明白什么是重叠子问题(斐波那契数列严格来说不是动态规划问题),后者主要举集中于如何列出状态转移方程。
请读者不要嫌弃这个例子简单,只有简单的例子才能让你把精力充分集中在算法背后的通用思想和技巧上,而不会被那些隐晦的细节问题搞的莫名其妙。想要困难的例子,历史文章里有的是。
一、斐波那契数列
1、暴力递归
斐波那契数列的数学形式就是递归的,写成代码就是这样:
int fib(int N) {
if (N == 1 || N == 2) return 1;
return fib(N - 1) + fib(N - 2);
}
这个不用多说了,学校老师讲递归的时候似乎都是拿这个举例。我们也知道这样写代码虽然简洁易懂,但是十分低效,低效在哪里?假设 n = 20,请画出递归树。
PS:但凡遇到需要递归的问题,最好都画出递归树,这对你分析算法的复杂度,寻找算法低效的原因都有巨大帮助。
这个递归树怎么理解?就是说想要计算原问题 f(20),我就得先计算出子问题 f(19) 和 f(18),然后要计算 f(19),我就要先算出子问题 f(18) 和 f(17),以此类推。最后遇到 f(1) 或者 f(2) 的时候,结果已知,就能直接返回结果,递归树不再向下生长了。
递归算法的时间复杂度怎么计算?子问题个数乘以解决一个子问题需要的时间。
子问题个数,即递归树中节点的总数。显然二叉树节点总数为指数级别,所以子问题个数为 O(2^n)。
解决一个子问题的时间,在本算法中,没有循环,只有 f(n - 1) + f(n - 2) 一个加法操作,时间为 O(1)。
所以,这个算法的时间复杂度为 O(2^n),指数级别,爆炸。
观察递归树,很明显发现了算法低效的原因:存在大量重复计算,比如 f(18) 被计算了两次,而且你可以看到,以 f(18) 为根的这个递归树体量巨大,多算一遍,会耗费巨大的时间。更何况,还不止 f(18) 这一个节点被重复计算,所以这个算法及其低效。
这就是动态规划问题的第一个性质:重叠子问题。下面,我们想办法解决这个问题。
2、带备忘录的递归解法
明确了问题,其实就已经把问题解决了一半。即然耗时的原因是重复计算,那么我们可以造一个「备忘录」,每次算出某个子问题的答案后别急着返回,先记到「备忘录」里再返回;
每次遇到一个子问题先去「备忘录」里查一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用,不要再耗时去计算了。
一般使用一个数组充当这个「备忘录」,当然你也可以使用哈希表(字典),思想都是一样的。
int fib(int N) {
if (N < 1) return 0;
// 备忘录全初始化为 0
vector<int> memo(N + 1, 0);
// 初始化最简情况
return helper(memo, N);
}
int helper(vector<int>& memo, int n) {
// base case
if (n == 1 || n == 2) return 1;
// 已经计算过
if (memo[n] != 0) return memo[n];
memo[n] = helper(memo, n - 1) +
helper(memo, n - 2);
return memo[n];
}
现在,画出递归树,你就知道「备忘录」到底做了什么。
实际上,带「备忘录」的递归算法,把一棵存在巨量冗余的递归树通过「剪枝」,改造成了一幅不存在冗余的递归图,极大减少了子问题(即递归图中节点)的个数。
递归算法的时间复杂度怎么算?子问题个数乘以解决一个子问题需要的时间。
子问题个数,即图中节点的总数,由于本算法不存在冗余计算,子问题就是 f(1), f(2), f(3) ... f(20),数量和输入规模 n = 20 成正比,所以子问题个数为 O(n)。
解决一个子问题的时间,同上,没有什么循环,时间为 O(1)。
所以,本算法的时间复杂度是 O(n)。比起暴力算法,是降维打击。
至此,带备忘录的递归解法的效率已经和迭代的动态规划解法一样了。实际上,这种解法和迭代的动态规划已经差不多了,只不过这种方法叫做「自顶向下」,动态规划叫做「自底向上」。
啥叫「自顶向下」?注意我们刚才画的递归树(或者说图),是从上向下延伸,都是从一个规模较大的原问题比如说 f(20),向下逐渐分解规模,直到 f(1) 和 f(2) 触底,然后逐层返回答案,这就叫「自顶向下」。
啥叫「自底向上」?反过来,我们直接从最底下,最简单,问题规模最小的 f(1) 和 f(2) 开始往上推,直到推到我们想要的答案 f(20),这就是动态规划的思路,这也是为什么动态规划一般都脱离了递归,而是由循环迭代完成计算。
二、凑零钱问题
先看下题目:给你 k 种面值的硬币,面值分别为 c1, c2 ... ck,每种硬币的数量无限,再给一个总金额 amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。算法的函数签名如下:
// coins 中是可选硬币面值,amount 是目标金额
int coinChange(int[] coins, int amount);
比如说 k = 3,面值分别为 1,2,5,总金额 amount = 11。那么最少需要 3 枚硬币凑出,即 11 = 5 + 5 + 1。
你认为计算机应该如何解决这个问题?显然,就是把所有肯能的凑硬币方法都穷举出来,然后找找看最少需要多少枚硬币。
1、暴力递归
首先,这个问题是动态规划问题,因为它具有「最优子结构」的。要符合「最优子结构」,子问题间必须互相独立。啥叫相互独立?你肯定不想看数学证明,我用一个直观的例子来讲解。
比如说,你的原问题是考出最高的总成绩,那么你的子问题就是要把语文考到最高,数学考到最高…… 为了每门课考到最高,你要把每门课相应的选择题分数拿到最高,填空题分数拿到最高…… 当然,最终就是你每门课都是满分,这就是最高的总成绩。
得到了正确的结果:最高的总成绩就是总分。因为这个过程符合最优子结构,“每门科目考到最高”这些子问题是互相独立,互不干扰的。
但是,如果加一个条件:你的语文成绩和数学成绩会互相制约,此消彼长。这样的话,显然你能考到的最高总成绩就达不到总分了,按刚才那个思路就会得到错误的结果。因为子问题并不独立,语文数学成绩无法同时最优,所以最优子结构被破坏。
回到凑零钱问题,为什么说它符合最优子结构呢?
比如你想求 amount = 11 时的最少硬币数(原问题),如果你知道凑出 amount = 10 的最少硬币数(子问题),你只需要把子问题的答案加一(再选一枚面值为 1 的硬币)就是原问题的答案,因为硬币的数量是没有限制的,子问题之间没有相互制,是互相独立的。
那么,既然知道了这是个动态规划问题,就要思考如何列出正确的状态转移方程?
先确定「状态」,也就是原问题和子问题中变化的变量。由于硬币数量无限,所以唯一的状态就是目标金额 amount。
然后确定 dp 函数的定义:当前的目标金额是 n,至少需要 dp(n) 个硬币凑出该金额。
然后确定「选择」并择优,也就是对于每个状态,可以做出什么选择改变当前状态。具体到这个问题,无论当的目标金额是多少,选择就是从面额列表 coins 中选择一个硬币,然后目标金额就会减少:
# 伪码框架
def coinChange(coins: List[int], amount: int):
# 定义:要凑出金额 n,至少要 dp(n) 个硬币
def dp(n):
# 做选择,选择需要硬币最少的那个结果
for coin in coins:
res = min(res, 1 + dp(n - coin))
return res
# 我们要求的问题是 dp(amount)
return dp(amount)
最后明确 base case,显然目标金额为 0 时,所需硬币数量为 0;当目标金额小于 0 时,无解,返回 -1:
def coinChange(coins: List[int], amount: int):
def dp(n):
# base case
if n == 0: return 0
if n < 0: return -1
# 求最小值,所以初始化为正无穷
res = float('INF')
for coin in coins:
subproblem = dp(n - coin)
# 子问题无解,跳过
if subproblem == -1: continue
res = min(res, 1 + subproblem)
return res if res != float('INF') else -1
return dp(amount)
至此,状态转移方程其实已经完成了,以上算法已经是暴力解法了,以上代码的数学形式就是状态转移方程:
至此,这个问题其实就解决了,只不过需要消除一下重叠子问题,比如 amount = 11, coins = {1,2,5} 时画出递归树看看:
时间复杂度分析:子问题总数 x 每个子问题的时间。
子问题总数为递归树节点个数,这个比较难看出来,是 O(n^k),总之是指数级别的。每个子问题中含有一个 for 循环,复杂度为 O(k)。所以总时间复杂度为 O(k * n^k),指数级别。
2、带备忘录的递归 [自上而下]
只需要稍加修改,就可以通过备忘录消除子问题:
def coinChange(coins: List[int], amount: int):
# 备忘录
memo = dict()
def dp(n):
# 查备忘录,避免重复计算
if n in memo: return memo[n]
if n == 0: return 0
if n < 0: return -1
res = float('INF')
for coin in coins:
subproblem = dp(n - coin)
if subproblem == -1: continue
res = min(res, 1 + subproblem)
# 记入备忘录
memo[n] = res if res != float('INF') else -1
return memo[n]
return dp(amount)
不画图了,很显然「备忘录」大大减小了子问题数目,完全消除了子问题的冗余,所以子问题总数不会超过金额数 n,即子问题数目为 O(n)。处理一个子问题的时间不变,仍是 O(k),所以总的时间复杂度是 O(kn)。
3、dp 数组的迭代解法 【自下而上】
当然,我们也可以自底向上使用 dp table 来消除重叠子问题,dp 数组的定义和刚才 dp 函数类似,定义也是一样的:
dp[i] = x 表示,当目标金额为 i 时,至少需要 x 枚硬币。
int coinChange(vector<int>& coins, int amount) {
// 数组大小为 amount + 1,初始值也为 amount + 1
vector<int> dp(amount + 1, amount + 1);
// base case
dp[0] = 0;
for (int i = 0; i < dp.size(); i++) {
// 内层 for 在求所有子问题 + 1 的最小值
for (int coin : coins) {
// 子问题无解,跳过
if (i - coin < 0) continue;
dp[i] = min(dp[i], 1 + dp[i - coin]); //dp[i] 是有个初始值的,在初始值和 1 + dp[i-coin]之间选一个较小的
}
}
return (dp[amount] == amount + 1) ? -1 : dp[amount];
}
PS:为啥 dp 数组初始化为 amount + 1 呢,因为凑成 amount 金额的硬币数最多只可能等于 amount(全用 1 元面值的硬币),所以初始化为 amount + 1 就相当于初始化为正无穷,便于后续取最小值。
三、最后总结
第一个斐波那契数列的问题,解释了如何通过「备忘录」或者「dp table」的方法来优化递归树,并且明确了这两种方法本质上是一样的,只是自顶向下和自底向上的不同而已。
第二个凑零钱的问题,展示了如何流程化确定「状态转移方程」,只要通过状态转移方程写出暴力递归解,剩下的也就是优化递归树,消除重叠子问题而已。
如果你不太了解动态规划,还能看到这里,真得给你鼓掌,相信你已经掌握了这个算法的设计技巧。
计算机解决问题其实没有任何奇技淫巧,它唯一的解决办法就是穷举,穷举所有可能性。算法设计无非就是先思考“如何穷举”,然后再追求“如何聪明地穷举”。
列出动态转移方程,就是在解决“如何穷举”的问题。之所以说它难,一是因为很多穷举需要递归实现,二是因为有的问题本身的解空间复杂,不那么容易穷举完整。
备忘录、DP table 就是在追求“如何聪明地穷举”。用空间换时间的思路,是降低时间复杂度的不二法门,除此之外,试问,还能玩出啥花活?
最后,点击我的头像可以查看更多详细题解,希望读者多多点赞,让我感受到你的认可~
PS:我的所有算法文章都已经上传到了 Github 仓库:fucking-algorithm,共 60 多篇,绝对精品,肯定让你收获满满,求个 star 不过分吧~
PPS:我最近精心制作了一份电子书《labuladong的算法小抄》,分为「动态规划」「数据结构」「算法思维」「高频面试」四个章节,目录如下,限时开放下载,如有需要可扫码到我的公众号 labuladong 后台回复关键词「pdf」下载:
作者:labuladong
链接:https://leetcode-cn.com/problems/coin-change/solution/dong-tai-gui-hua-tao-lu-xiang-jie-by-wei-lai-bu-ke/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
==========================================
求出f(n),只需要知道几个【数量有限】更小的f(c)。我们将求解f(c)称作求解f(n)的“子问题”。 并不关心结果是怎么来的,只关心最优的结果值。
这就是DP(动态规划,dynamic programming).
将一个问题拆成几个子问题,分别求解这些子问题,即可推断出大问题的解。
链接:https://www.zhihu.com/question/23995189/answer/613096905
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
0. intro
很有意思的问题。以往见过许多教材,对动态规划(DP)的引入属于“奉天承运,皇帝诏曰”式:不给出一点引入,见面即拿出一大堆公式吓人;学生则死啃书本,然后突然顿悟。针对入门者的教材不应该是这样的。恰好我给入门者讲过四次DP入门,迭代出了一套比较靠谱的教学方法,所以今天跑过来献丑。
现在,我们试着自己来一步步“重新发明”DP。
1. 从一个生活问题谈起
先来看看生活中经常遇到的事吧——假设您是个土豪,身上带了足够的1、5、10、20、50、100元面值的钞票。现在您的目标是凑出某个金额w,需要用到尽量少的钞票。
依据生活经验,我们显然可以采取这样的策略:能用100的就尽量用100的,否则尽量用50的……依次类推。在这种策略下,666=6×100+1×50+1×10+1×5+1×1,共使用了10张钞票。
这种策略称为“贪心”【每次选择最大的】:假设我们面对的局面是“需要凑出w”,贪心策略会尽快让w变得更小。能让w少100就尽量让它少100,这样我们接下来面对的局面就是凑出w-100。长期的生活经验表明,贪心策略是正确的。
但是,如果我们换一组钞票的面值,贪心策略就也许不成立了。如果一个奇葩国家的钞票面额分别是1、5、11,那么我们在凑出15的时候,贪心策略会出错:
15=1×11+4×1 (贪心策略使用了5张钞票)
15=3×5 (正确的策略,只用3张钞票)
为什么会这样呢?贪心策略错在了哪里?
鼠目寸光。
刚刚已经说过,贪心策略的纲领是:“尽量使接下来面对的w更小”。这样,贪心策略在w=15的局面时,会优先使用11来把w降到4;但是在这个问题中,凑出4的代价是很高的,必须使用4×1。如果使用了5,w会降为10,虽然没有4那么小,但是凑出10只需要两张5元。
在这里我们发现,贪心是一种只考虑眼前情况的策略。
那么,现在我们怎样才能避免鼠目寸光呢?
如果直接暴力枚举凑出w的方案,明显复杂度过高。太多种方法可以凑出w了,枚举它们的时间是不可承受的。我们现在来尝试找一下性质。
重新分析刚刚的例子。w=15时,我们如果取11,接下来就面对w=4的情况;如果取5,则接下来面对w=10的情况。我们发现这些问题都有相同的形式:“给定w,凑出w所用的最少钞票是多少张?”接下来,我们用f(n)来表示“凑出n所需的最少钞票数量”。
那么,如果我们取了11,最后的代价(用掉的钞票总数)是多少呢?
明显 ,它的意义是:利用11来凑出15,付出的代价等于f(4)加上自己这一张钞票。现在我们暂时不管f(4)怎么求出来。
依次类推,马上可以知道:如果我们用5来凑出15,cost就是 。
那么,现在w=15的时候,我们该取那种钞票呢?当然是各种方案中,cost值最低的那一个!
- 取11:
- 取5:
- 取1:
显而易见,cost值最低的是取5的方案。我们通过上面三个式子,做出了正确的决策!
这给了我们一个至关重要的启示—— 只与
相关;更确切地说:
这个式子是非常激动人心的。
我们要求出f(n),只需要求出几个更小的f值;既然如此,我们从小到大把所有的f(i)求出来不就好了?注意一下边界情况即可。代码如下:

for()
{ //cost是全局变量
if(i - 1 >= 0) cost = min(cost, f[i - 1] + 1); //[1, 5) 能按1拆分
if(i - 5 >= 0) cost = min(cost, f[i - 5] + 1); //[5, 11) 能按1或者5两种来拆分,取两者中的小的; 比如当i ==5时, f[5] = f[5-1] + 1 = 5或者 f[5] = f[5-5] + 1 = 1; 取较小的
if(i - 11 >= 0) cost = min(cost, f[i - 11] + 1);
f[i] = cost; //算出各个f[i]的值
}
我们以 的复杂度解决了这个问题。现在回过头来,我们看看它的原理:
- 只与
的值相关。
- 我们只关心 的值,不关心是怎么凑出w的。
这两个事实,保证了我们做法的正确性。它比起贪心策略,会分别算出取1、5、11的代价,从而做出一个正确决策,这样就避免掉了“鼠目寸光”!
它与暴力的区别在哪里?
我们的暴力枚举了“使用的硬币”,然而这属于冗余信息。
我们要的是答案,根本不关心这个答案是怎么凑出来的。
譬如,要求出f(15),只需要知道f(14),f(10),f(4)的值。其他信息并不需要。我们舍弃了冗余信息。我们只记录了对解决问题有帮助的信息——f(n).
我们能这样干,取决于问题的性质:求出f(n),只需要知道几个更小的f(c)。我们将求解f(c)称作求解f(n)的“子问题”。
2. 几个简单的概念
【无后效性】
一旦f(n)确定,“我们如何凑出f(n)”就再也用不着了。
要求出f(15),只需要知道f(14),f(10),f(4)的值,而f(14),f(10),f(4)是如何算出来的,对之后的问题没有影响。
“未来与过去无关”,这就是无后效性。
(严格定义:如果给定某一阶段的状态,则在这一阶段以后过程的发展不受这阶段以前各段状态的影响。)
【最优子结构】
回顾我们对f(n)的定义:我们记“凑出n所需的最少钞票数量”为f(n).
f(n)的定义就已经蕴含了“最优”。利用w=14,10,4的最优解,我们即可算出w=15的最优解。
大问题的最优解可以由小问题的最优解推出,这个性质叫做“最优子结构性质”。
引入这两个概念之后,我们如何判断一个问题能否使用DP解决呢?
能将大问题拆成几个小问题,且满足无后效性、最优子结构性质。
链接:https://www.zhihu.com/question/23995189/answer/613096905
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
3. DP的典型应用:DAG最短路
问题很简单:给定一个城市的地图,所有的道路都是单行道,而且不会构成环。每条道路都有过路费,问您从S点到T点花费的最少费用。

这个问题能用DP解决吗?我们先试着记从S到P的最少费用为f(P).
想要到T,要么经过C,要么经过D。从而.
好像看起来可以DP。现在我们检验刚刚那两个性质:
- 无后效性:对于点P,一旦f(P)确定,以后就只关心f(P)的值,不关心怎么去的。
- 最优子结构:对于P,我们当然只关心到P的最小费用,即f(P)。如果我们从S走到T是 ,那肯定S走到Q的最优路径是
。
对一条最优的路径而言,从S走到沿途上所有的点(子问题)的最优路径,都是这条大路的一部分。这个问题的最优子结构性质是显然的。
既然这两个性质都满足,那么本题可以DP。式子明显为:
其中R为有路通到P的所有的点, 为R到P的过路费。
代码实现也很简单,拓扑排序即可。
4. 对DP原理的一点讨论
【DP的核心思想】
DP为什么会快?
无论是DP还是暴力,我们的算法都是在可能解空间内,寻找最优解。
来看钞票问题。暴力做法是枚举所有的可能解,这是最大的可能解空间。
DP是枚举有希望成为答案的解。这个空间比暴力的小得多。
也就是说:DP自带剪枝。
DP舍弃了一大堆不可能成为最优解的答案。譬如:
15 = 5+5+5 被考虑了。
15 = 5+5+1+1+1+1+1 从来没有考虑过,因为这不可能成为最优解。
从而我们可以得到DP的核心思想:尽量缩小可能解空间。
在暴力算法中,可能解空间往往是指数级的大小;如果我们采用DP,那么有可能把解空间的大小降到多项式级。
一般来说,解空间越小,寻找解就越快。这样就完成了优化。
【DP的操作过程】
一言以蔽之:大事化小,小事化了。
将一个大问题转化成几个小问题;
求解小问题;
推出大问题的解。
【如何设计DP算法】
下面介绍比较通用的设计DP算法的步骤。
首先,把我们面对的局面表示为x。这一步称为设计状态。
对于状态x,记我们要求出的答案(e.g. 最小费用)为f(x). 我们的目标是求出f(T).
找出f(x)与哪些局面有关(记为p),写出一个式子(称为状态转移方程),通过f(p)来推出f(x).
【DP三连】
设计DP算法,往往可以遵循DP三连:
我是谁? ——设计状态,表示局面
我从哪里来?
我要到哪里去? ——设计转移
设计状态是DP的基础。接下来的设计转移,有两种方式:
一种是考虑我从哪里来(本文之前提到的两个例子,都是在考虑“我从哪里来”);
另一种是考虑我到哪里去,这常见于求出f(x)之后,更新能从x走到的一些解。这种DP也是不少的,我们以后会遇到。
总而言之,“我从哪里来”和“我要到哪里去”只需要考虑清楚其中一个,就能设计出状态转移方程,从而写代码求解问题。前者又称pull型的转移,后者又称push型的转移。
(这两个词是 @阮止雨 妹妹告诉我的,不知道源出处在哪)
思考题:如何把钞票问题的代码改写成“我到哪里去”的形式?
提示:求出f(x)之后,更新f(x+1),f(x+5),f(x+11).
5. 例题:最长上升子序列
扯了这么多形而上的内容,还是做一道例题吧。
最长上升子序列(LIS)问题:给定长度为n的序列a,从a中抽取出一个子序列,这个子序列需要单调递增。问最长的上升子序列(LIS)的长度。 [只关心最终的值,不关心过程]
e.g. 1,5,3,4,6,9,7,8的LIS为1,3,4,6,7,8,长度为6。
如何设计状态(我是谁)?
我们记 为以
结尾的LIS长度,那么答案就是
.
状态x从哪里推过来(我从哪里来)?
考虑比x小的每一个p:如果 ,那么f(x)可以取f(p)+1.
解释:我们把 接在
的后面,肯定能构造一个以
结尾的上升子序列,长度比以
结尾的LIS大1.那么,我们可以写出状态转移方程了:
至此解决问题。两层for循环,复杂度 .

从这三个例题中可以看出,DP是一种思想,一种“大事化小,小事化了”的思想。带着这种思想,DP将会成为我们解决问题的利器。
最后,我们一起念一遍DP三连吧——我是谁?我从哪里来?我要到哪里去?
6. 习题
如果读者有兴趣,可以试着完成下面几个习题:
一、请采取一些优化手段,以 的复杂度解决LIS问题。
提示:可以参考这篇博客 Junior Dynamic Programming--动态规划初步·各种子序列问题
二、“按顺序递推”和“记忆化搜索”是实现DP的两种方式。请查阅资料,简单描述“记忆化搜索”是什么。并采用记忆化搜索写出钞票问题的代码,然后完成P1541 乌龟棋 - 洛谷 。
三、01背包问题是一种常见的DP模型。请完成P1048 采药 - 洛谷。
=======================
https://blog.csdn.net/qq_14821023/article/details/50880893
House Robber(强盗入屋)
【难度:Easy】
You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security system connected and it will automatically contact the police if two adjacent houses were broken into on the same night.
Given a list of non-negative integers representing the amount of money of each house, determine the maximum amount of money you can rob tonight without alerting the police.
给定一个整数数组表示一系列连续的屋子里面放有的金钱,在不能连续进入相邻屋子抢钱的情况下求能抢到的最大金额。
解题思路
动态规划问题,要考虑要是在当前点i抢钱的话,加上前面在点i-2的所抢到的金额之和是否比在i-1抢到的钱要多,是则当前点的最大金额变为dp[i] = dp[[i-2]+n[i],否则变为dp[i] = dp[i-1]。
c++代码如下:
class Solution { public: int rob(vector<int>& nums) { if (nums.empty()) return 0; int n = nums.size(); int rob[n]; rob[0] = nums[0]; rob[1] = nums[1]>nums[0]?nums[1]:nums[0]; for (int i = 2; i < n; i++) { rob[i] = (rob[i-2]+nums[i])>rob[i-1] ?(rob[i-2]+nums[i]):rob[i-1]; } return rob[n-1]; } };
#include <vector> #include <iostream> using namespace std; int rob(vector<int>& nums) { if (nums.empty()) return 0; int n = nums.size(); int rob[n]; //分别求出最后如果抢i时获得的总金额: rob[i]; 不关心过程,只关心值 rob[0] = nums[0]; //如果最后抢的是0,那么总金额就是nums[0] rob[1] = nums[1]>nums[0]?nums[1]:nums[0]; //如果最后抢的是1,那么得比较dp[[i-2]+n[i]和dp[i-1],取较大的
//依次求出每个i点可rob的最大值 for (int i = 2; i < n; i++) { rob[i] = (rob[i-2]+nums[i])>rob[i-1] ?(rob[i-2]+nums[i]):rob[i-1]; } for(auto i : rob) { cout << i << endl; } return rob[n-1]; } int main() { vector<int> v = {10, 2, 33, 9, 5, 41, 66, 77, 12, 1}; cout << rob(v) << endl; return 0; } /* 10 10 43 43 48 84 114 161 161 162 162 */
//上面都解答无法处理, [], [1] , [1,2]数组
上面都方法把所有可能都解答都存起来来,实际上每一步你只需要前两个最大值,两个变量就足够用了
选择 f(–1) = f(0) = 0 为初始情况,这将极大地简化代码
class Solution { public: int rob(vector<int>& nums) { int p1 = 0, p2 = 0, max_val = 0; for(auto num : nums) { int cur = (num + p1) > p2 ? (num + p1) : p2; p1 = p2; p2 = cur; max_val = std::max(p1, p2); } return max_val; } };