vlambda博客
学习文章列表

写写代码系列033:打家劫舍(动态规划)


题目描述


你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统, 如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。


示例:

写写代码系列033:打家劫舍(动态规划)


写写代码系列033:打家劫舍(动态规划)


题目解析


本题是leetcode上198号问题。

解法1:暴力解法

大家可以想一想,在这条街上每个房子我都可以偷,而最终的结果是我选择几个房子进行偷取,那么问题的结果其实是我在所有的n个房子中选择的组合。那么n个房子共有多少种组合呢? 如果允许偷取0个房子,那么所有的组 合数就是2^n。

写写代码系列033:打家劫舍(动态规划)

对于每一个组合,我们考察组合中有没有相邻的房子,如果有相邻的房子,则会触发报警系统,不是我们要的解;如果没有相邻的房子,则可能是我们 要的一个解,记录组合的价值,最终找到最大值。所以,在找到所有组合之后,还要对每个组合再循环一遍,这个循环组合的过程时间复杂度为O(n)。
所以暴力解法的时间复杂度为O((2^n)*n)。
显然,这个问题并不是让你求所有的组合,而是让你求一个最大值,换句话说他是一个最优化的问题。对于这个问题,他的解空间又是一个组合问题,所以很自然的可以想到使用递归或者动态规划解决这个问题。


解法2:动态规划

在这里,我画这样的一个递归树。大家可以思考,我们的整个问题是考虑偷取[0,…,n-1]这n所房子所有的宝物。那么很显然,在这n所房子里,我们可以选择偷取0号房子,也可以选择不偷取0偷取1,……,也可以选择偷取n-1号房子。进一步,我们可以选择偷取哪些房子呢?比如我们第一步选择偷取0号房子,那么我们现在的子问题就变成了考虑偷取[2,…,n-1]这个范围的房子,大家注意我把1号房子给除去了,因为1和0是相邻的,我们既然已经决定了偷取0,那就肯定不能偷取1了。同理,我们如果第一步偷取1号房子,那么我们现在的子问题就变成了考虑偷取[3,…,n-1]这个范围的房子,以此类推,我们最后偷取的是n-1号房子,在这种情况下,后续也没有房子了,这个时候的子问题就是空集。

写写代码系列033:打家劫舍(动态规划)

下面我们继续来看,如果我们考虑偷取[2,…,n-1]范围内的房子的话,如果我们偷取2号房子, 那么我们现在的子问题就变成了考虑偷取[4,…,n-1]这个范围的房子如果我们偷取3 号房子, 那么我们现在的子问题就变成了考虑偷取[5,…,n-1]这个范围的房子。以此类推。

写写代码系列033:打家劫舍(动态规划)

这里我们可以看到,每一个问题都是求解一个最优化的值,那么子问题的最优化的值再配合上我当前的决策,在每一步中选择一个最大值,就能得到原问题的最优解,相当于这也拥有一个最优子结构的形式。因此也可以用动态规划的方式求解。
对于这个问题,我们再进行一步形式化的分析。在之前的递归树中,每一个节点表达的是我要解决一个考虑偷取[x,…,n-1]范围里的房子的问题,这样的问题在动态规划里我们通常称之为定义了一个状态。

写写代码系列033:打家劫舍(动态规划)

注意这个状态的定义,说的是 考虑偷取[x,…,n-1],意思是我考虑从x开始偷,但不一定保证偷x,这个x只是我考虑的起点。在有些动态规划问题中,有的状态是一定偷取x,那这就是另一个状态了,这二者是完全不一样的两个函数。
当我们完成了状态的定义后,下面一件事就是定义状态的转移。对于这个问题,我们定义每一个房子的价值为v(i)(0≤i≤n-1),定义状态f(x) 考虑偷取 [x,… ,n-1 ]范围里的房子。所以本问题的最终解就是f(0),也就是考虑偷取[0,…,n-1]范围里的房子,就是考虑偷取所有房子。状态转移方程为:

写写代码系列033:打家劫舍(动态规划)

这个状态转移方程不难理解,就是我们上面的递归树的函数形式。事实上,不管是递归,还是动态规划,都是在实现我们的状态转移方程,状态转移方程就是此类问题的核心,很多面试基本上只要写出状态的定义和状态转移方程,就可以算过了。


写写代码系列033:打家劫舍(动态规划)


程序实现



class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        if n == 0:
            return 0
        
        memo = [-1] * n
        memo[n - 1] = nums[n - 1]
        for i in range(n - 2,-1,-1):
            for j in range(i,n):
                if (j + 2) >= n:
                    memo[i] = max(memo[i],nums[j] + 0)
                else:
                    memo[i] = max(memo[i],nums[j] + memo[j + 2])
        return memo[0]


写写代码系列033:打家劫舍(动态规划)


写写代码系列033:打家劫舍(动态规划)

扫码关注