动态规划之打家劫舍模型

打家劫舍模型

在这里插入图片描述
打家劫舍模型源自于 LeetCode 198. 打家劫舍 这道题目,大意就是现在你将扮演一名小偷,计划偷窃沿街的房屋,每间房内装有一定的现金,不能偷窃相邻的房子,否则会触发警报,试问最终能偷窃盗的最高金额。

根据灵茶山艾府整理的题单,基于“打家劫舍”派生出来了若干种变体,比如“环状打家劫舍”、“值域上的打家劫舍”等,下面我们来对派生题进行整理与学习。

LeetCode 213. 打家劫舍 II

思路

这道题是最基本的派生题,即“环状打家劫舍”,解决的思路也没有那么复杂。

最主要的思路就是是否选取nums[0],如果选取nums[0],那么就不能选取nums[n-1]nums[1],题目退化为nums[2...n-2]上的打家劫舍。否则如果不选取nums[0],那么就可以选取nums[1],退化为nums[1...n-1]上的打家劫舍。

Golang 代码

func solve(nums []int, start, end int) int {
    f0, f1 := 0, 0
    for i := start; i < end; i ++ {
        f0, f1 = f1, max(f1, f0 + nums[i])
    }
    return f1
}

func rob(nums []int) int {
    n := len(nums)
    if n == 1 {
        return nums[0]
    } else if n == 2 {
        return max(nums[0], nums[1])
    }
    return max(nums[0] + solve(nums, 2, n - 1), solve(nums, 1, n))
}

LeetCode 2320. 统计放置房子的方式数

思路

灵神将这道题归类于打家劫舍模型,但我个人认为这道题和打家劫舍的关系并没有那么大。

对于这道题,需要统计的是最终总共有多少种可能的放置方式,由于两侧房子的放置方案互不干扰,所以本质上我们只需要统计一次即可,最终使用乘法原理就可以得到答案。

那么如何得到一侧的房子放置方案呢?我们使用dp数组记录n=i的情况下可能的房屋放置方案数。对于n=0的情况,由于不放置任何房子,所以这种情况下的放置方案为dp[0]=1。对于n=1的情况,此时可以选择放与不放,所以方案数为dp[1]=2。对于n=2的情况,由于题目中已经提到,放置的房子不能相邻,因此n=2的初始状态就是在第二个位置放或者不放房子,如果在第二个位置放房子,那么第一个位置就不能放房子,可能的放置方案数就是dp[0],也就是1;如果在第二个位置不放房子,就意味着可以在上一个位置放房子,方案数就是dp[1],也就是2。是否在第二个位置放房子这两种情况是相互独立的,因此可以通过加法原理,得到dp[2] = dp[0] + dp[1],因此不难归纳出状态转移方程dp[n] = dp[n - 1] + dp[n - 2],由于答案可能很大,设MOD = 1e9 + 7,最终的状态转移方程就是dp[n] = (dp[n - 1] + dp[n - 2]) % MOD

Golang 代码

func countHousePlacements(n int) int {
    const MOD int = 1e9 + 7
    dp := make([]int, n + 1)
    dp[0], dp[1] = 1, 2
    for i := 2; i <= n; i ++ {
        dp[i] = (dp[i - 1] + dp[i - 2]) % MOD
    }
    return dp[n] * dp[n] % MOD  // 乘法原理
}

LeetCode 740. 删除并获得点数

思路

这道题目就是典型的“值域上的打家劫舍”,选取了nums[i]这个值之后,就不能选取相邻的nums[i-1]nums[i+1]了,因此一个可选的方法就是首先将可能的值统计出来,然后再直接应用打家劫舍模型,即可解决这个问题。

Golang 代码

func deleteAndEarn(nums []int) int {
    n := len(nums)
    maxVal := nums[0]
    for i := 1; i < n; i ++ {
        maxVal = max(maxVal, nums[i])
    }
    curr := make([]int, maxVal + 1)
    for i := 0; i < n; i ++ {
        curr[nums[i]] += nums[i]    // 统计值
    }
    n = len(curr)
    f0, f1 := curr[0], max(curr[0], curr[1])
    for i := 2; i < n; i ++ {
        f0, f1 = f1, max(f0 + curr[i], f1)
    }
    return f1
}

LeetCode 3186. 施咒的最大总伤害

思路

同样是“值域上的打家劫舍”,但是需要做一些优化。这道题应该是灵神打家劫舍模型当中最难的一道题。

具体来说,我们需要先使用一个 hashmap 将各个power[i]值的出现次数统计出来。之后,统计每一种power[i],将power[i]记录到一个新的数组当中,并对这个数组排序,暂将这个新的数组命名为curr

curr当中记录的是所以可能的power[i],并且升序排序,现在就可以套用打家劫舍模型了。具体来说,由于这道题的题目要求,如果使用了power[i],就不能使用power[i]-1power[i]-2了,因此需要使用双指针来控制值的关系。这部分的具体实现可以详见下面的 Golang 代码。

Golang 代码

func maximumTotalDamage(power []int) int64 {
    n := len(power)
    mp := map[int]int{}
    for i := 0; i < n; i ++ {
        mp[power[i]] ++
    }
    curr := []int{}
    for k := range mp {
        curr = append(curr, k)
    }
    slices.Sort(curr)

    n = len(mp)
    dp, j := make([]int, n + 1), 0
    for i, x := range curr {
        for curr[j] < x - 2 { // 使用双指针控制 power[i] 和 power[i] - 2 的关系
            j ++    // curr 记录的是值
        }
        dp[i + 1] = max(dp[i], dp[j] + x * mp[x])
        // ⬆️ x 是值, mp[x] 是这个值出现的次数
    }
    return int64(dp[n])
}

LeetCode 2140. 解决智力问题

思路

这道题可以看作是一个“反方向的打家劫舍”,从最后一个问题出发,一直 DP 到第一个问题,获得的最高分数就是最终的答案。

题目本身并不是很难,注意控制 dp 状态转移的边界即可。

Golang 代码

func mostPoints(questions [][]int) int64 {
    n := len(questions)
    dp := make([]int64, n + 1)
    for i := n - 1; i >= 0; i -- {
        dp[i] = max(dp[i + 1], int64(questions[i][0]) + dp[min(n, questions[i][1] + i + 1)])
    }
    return dp[0]
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值