算法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]
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容