算法7:动态规划

  • 动态规划的关键思想在于将问题转换成较小的子问题,然后根据子问题的结果总结出一个状态转移方程,最后得到整个问题的解

7.1 打家劫舍

LeetCode No.198

问题描述:你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

思路:当设选第i个房屋,则不能选i-1,假设dp[i]表示前i个房屋能偷到的最高金额,则有 dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])

示例代码:

func rob(nums []int) int {
    n := len(nums)
    if n == 0 {
        return 0
    }
    if n == 1 {
        return nums[0]
    }
    dp := make([]int, n)
    dp[0], dp[1] = nums[0], max(nums[0], nums[1])
    ans := dp[1]
    for i := 2; i < n; i++ {
        dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
        if ans < dp[i] {
            ans = dp[i]
        }
    }
    return ans
}

7.1-1 打家劫舍2

LeetCode No.213

问题描述:房屋变成了环形排列,其他和7.1相同

思路:环形排列后,第一个和最后一个不能同时偷,可以转化为[0, n-1]和[1, n]两个单排街道较大值。同时,可以看到第i个问题的最大值只和第i-1和i-2有关,可以只用两个值来保存前两个结果,降低空间复杂度。

示例代码:

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

func dorob(nums []int) int {
    n := len(nums)
    if n == 1 {
        return nums[0]
    }
    fisrt, second := nums[0], max(nums[0], nums[1])
    ans := second
    for i := 2; i < n; i++ {
        fisrt, second = second, max(fisrt + nums[i], second)
        if second > ans {
            ans = second
        }
    }
    return ans
}

7.2 分割等和子集

LeetCode No.416

问题描述:给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11]

思路:如果数组大小为奇数,结果必为false。然后该问题可以转化为背包大小为sum/2的0-1背包问题,如果恰好能装满则结果为true。

示例代码:

func canPartition(nums []int) bool {
    sum := 0
    for _, n := range nums {
        sum += n
    }
    if sum % 2 != 0 {
        return false
    }
    W := sum >> 1
    dp := make(map[int]bool)
    dp[0] = true
    for _, n := range nums {
        // 这里进行了空间优化,需要从后向前算,否则计算后面的时候前面的值已经改过,不是上一层的值了。
        // dp[i][w] = dp[i - 1][w] || dp[i-1][w-v]
        for i := W; i >= n; i-- {
            dp[i] = dp[i] || dp[i - n]
        }
    }
    return dp[W]
}

7.3 青蛙过河

LeetCode No.403

题目描述:一只青蛙想要过河。 假定河流被等分为 x 个单元格,并且在每一个单元格内都有可能放有一石子(也有可能没有)。 青蛙可以跳上石头,但是不可以跳入水中。
给定石子的位置列表(用单元格序号升序表示), 请判定青蛙能否成功过河(即能否在最后一步跳至最后一个石子上)。 开始时, 青蛙默认已站在第一个石子上,并可以假定它第一步只能跳跃一个单位(即只能从单元格1跳至单元格2)。
如果青蛙上一步跳跃了 k 个单位,那么它接下来的跳跃距离只能选择为 k - 1、k 或 k + 1个单位。 另请注意,青蛙只能向前方(终点的方向)跳跃。
输入:[0,1,3,5,6,8,12,17]
总共有8个石子。
第一个石子处于序号为0的单元格的位置, 第二个石子处于序号为1的单元格的位置,
第三个石子在序号为3的单元格的位置, 以此定义整个数组...
最后一个石子处于序号为17的单元格的位置。
输出: true。即青蛙可以成功过河,按照如下方案跳跃:
跳1个单位到第2块石子, 然后跳2个单位到第3块石子, 接着
跳2个单位到第4块石子, 然后跳3个单位到第6块石子,
跳4个单位到第7块石子, 最后,跳5个单位到第8个石子(即最后一块石子)。

思路:参考官方题解动态规划的方法,使用dmap[curpos] = {jumps}表示到达当前curpos可以由jumps集合的任意一个步长一次到达当前位置,对于dmap[0] = {0},依次遍历每个石头的位置,对每个位置pos遍历jumps集合,对每个jump遍历k = [jump-1,jump+1],如果当前位置跳k补可以到达某个石头的位置(dmap中的存在key=curpos+k),则将k添加到dmap[curpos+k]的集合。

示例代码:

func canCross(stones []int) bool {
    // 用集合模拟set,只需要键值当做集合的元素,value设为空结构
    dmap := make(map[int]map[int]struct{}, 0)
    dmap[0] = map[int]struct{}{0: {}}
    for i := 1; i < len(stones); i++ {
        dmap[stones[i]] = map[int]struct{}{}
    }
    for _, cur_pos := range stones {
        steps := dmap[cur_pos]
        for step, _ := range steps {
            for k := step - 1; k <= step + 1; k++ {
                if _, ok := dmap[cur_pos + k]; ok == true && k > 0 {
                    dmap[cur_pos + k][k] = struct{}{}
                }
            }
        }
    }
    return len(dmap[stones[len(stones) - 1]]) != 0
}

7.4 编辑距离

LeetCode No.72

题目描述:给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:插入一个字符、删除一个字符、替换一个字符。

思路:使用dp[i][j]表示从word1[:i]转换为word2[:j]需要的最少次数,对于任意一个单词为空,所需次数为另一个单词的长度,只能通过增或删来达到相同。对于dp[i][j],dp[i-1][j]表示从word2中删除一个字符得到,dp[i][j - 1]表示从word1中增加一个字符得到,dp[i-1][j-1]表示从word1中修改一个字符得到,对于word1[i] == word2[j]则不需要修改。那么dp[i][j]只能为上述三种情况的最小值。所以有状态转移方程:

  • 当word1[i] == word2[j]时 dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1]) + 1)
  • 当word1[i] != word2[j]时 dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1

示例代码:

func min(x, y int) int {
    if x < y {
        return x
    }
    return y
}

func minDistance(word1 string, word2 string) int {
    LFROM, LTO := len(word1), len(word2)
    dp := make([][]int, LFROM + 1)
    for i := 0; i <= LFROM; i++ {
        dp[i] = make([]int, LTO + 1)
        dp[i][0] = i
    }
    for j := 0; j <= LTO; j++ {
        dp[0][j] = j
    }
    for i := 1; i <= LFROM; i++ {
        for j := 1; j <= LTO; j++ {
            if word1[i - 1] == word2[j - 1] {
                dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1]) + 1)
            } else {
                dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1
            }
        }
    }
    return dp[LFROM][LTO]
}

你可能感兴趣的:(算法7:动态规划)