今天记录的题目是一道动态规划算法,刷题也有一段时间了,但是每次遇到动态规划算法还是唯恐避之不及,看到使用动态规划就害怕,但是学习不能老是逃避,终究还是要学的,今天的题目正好很适合作为动态规划初学者的练手题目,题目并不难,因此,学习动态规划,就从今天这道打家劫舍题目开始吧。
在看今天这题的解法之前,我们先来看一下什么是动态规划。先来看一下《算法导论》中是如何描述动态规划的:
从以上的描述中我们提取出几个重点,一是动态规划与分治的思想相似,都是把问题分解为子问题解决。二是动态规划是避免不必要的计算,也就是,每次计算都保留上一次计算的结果以便接下来调用。最后就是动态规划一般都用来求解最优解问题。
可以用一个简单的小例子来理解动态规划。
比如对于要求[1,1,1,1,1,1,1,1]这几个数的和,我们经过计算是8,那么我们如果在最左边再增加一个1呢,我们可能立马就能说出来,结果是9.这是因为我们的大脑计算完前面的结果之后并没有立马删除,还保留在我们的脑海中,因此再增加一个数我们直接加上这个数就可以了。这个就是动态规划思想最简单最朴素的表达了。具体可以参考知乎一个回答:如何理解动态规划?
接下来我们就来看一下如何使用动态规划的思想来解决今天的这道题目。
首先我们考虑最简单的情况,也就是如果数组中只有一个数,那么毫无疑问,我们直接返回数组中这个元素就可以了。如果再增加一个元素,数组中有两个数的话,那么我们直接返回两个数中最大的那个就可以了。那么如果数组中的数更多的时候我们怎么办呢,首先我们创建一个数组dp[],在这个数组中,记录着前面每一次的计算结果,也就是dp[i]表示数组中前i个数的最好结果。那么现在的问题就变成了,我们在已经有了前i-1个的结果的情况下,如何来计算dp[i]呢。这个问题也就是动态规划的核心了,也就是我们必须要找到动态规划的转移方程。思考这道题的转移方程,我们要计算dp[i],也就是前i个数中打家劫舍的最优解,我们不能连续两次选择相邻的选项,那么dp[i]就有两种情况,一种情况是选择了第i个数的前一个数,也就是i-1,那么这时候我们就不能选择当前的位置i这个数了,因为这两个数相邻,这时dp[i]的结果就是dp[i-1]。另一种情况就是我们选择了前两位的数,也就是i-2的数,那么这时候i-2与当前的i不相邻,可以再加上i处的数,这时dp[i]=dp[i-2]+nums[i]。那么,我们只要选出这两种情况中更大的数就可以了。综合以上分析,我们可以写出状态转移方程:
d p [ i ] = m a x ( d p [ i − 2 ] + n u m s [ i ] , d p [ i − 1 ] ) dp[i]=max(dp[i-2]+nums[i], dp[i-1]) dp[i]=max(dp[i−2]+nums[i],dp[i−1])
另外,边界条件为:
d p [ 0 ] = n u m s [ 0 ] dp[0]=nums[0] dp[0]=nums[0]
d p [ 1 ] = m a x ( n u m s [ 0 ] , n u m s [ 1 ] ) dp[1]=max(nums[0], nums[1]) dp[1]=max(nums[0],nums[1])
下面我们就可以来进行编码了:
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
if n == 1:
return nums[0]
dp = [0] * n
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2,n):
dp[i] = max(dp[i-2]+nums[i], dp[i-1])
return dp[n-1]
有了上面的分析之后,代码就很简单了。另外,因为我们的转移方程只用到了前面两个的结果,因此我们并不需要保留全部的dp数组,直接维护前两个结果就可以了,我们的代码可以进行如下修改:
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
if n == 1:
return nums[0]
first = nums[0]
second = max(nums[0], nums[1])
for i in range(2,n):
temp = max(first+nums[i], second)
first = second
second = temp
return second
这道题的动态规划解法就是这样了,下面来看一下这题的一个改进问题。
打家劫舍二在一的基础上增加了一个条件,即,将数组看做循环数组,因此,首尾也算作是相邻的,不能同时选择首尾的元素。
题目地址:打家劫舍二
有了上题的经验,这题很自然的一个想法就是维度两个dp数组,一个从首位开始,另一个从第二位开始。最后返回两个dp[n-1]中较大的那个。先看一下我自己的代码:
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
if n==1:
return nums[0]
if n==2:
return max(nums[0], nums[1])
dp1 = [0] * n
dp2 = [0] * n
dp1[0], dp2[1] = nums[0], nums[1]
dp1[1], dp2[2] = max(nums[0],nums[1]), max(nums[1], nums[2])
for i in range(2,n-1):
dp1[i] = max(dp1[i-2]+nums[i], dp1[i-1])
for i in range(3,n):
dp2[i] = max(dp2[i-2]+nums[i], dp2[i-1])
return max(dp1[n-2], dp2[n-1])
题解区优雅版代码:
class Solution:
def rob(self, nums: [int]) -> int:
def my_rob(nums):
cur, pre = 0, 0
for num in nums:
cur, pre = max(pre + num, cur), cur
return cur
return max(my_rob(nums[:-1]),my_rob(nums[1:])) if len(nums) != 1 else nums[0]
作者:jyd
链接:https://leetcode-cn.com/problems/house-robber-ii/solution/213-da-jia-jie-she-iidong-tai-gui-hua-jie-gou-hua-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
(为什么我的python代码总是一股c的味道,为什么我写不出别人这样优雅的代码
以上就是本次学习的全部内容,花了一上午的时间梳理这一道题目,希望这道题可以成为我从此以后解决动态规划问题的开始,那这一上午时间也没白花了。