动态规划套路

动态规划

动态规划(dynamic programming,简称 dp),是一类求解最优值的算法。有一定的套路可以遵循。

特点

牛逼了,原来大神都是这样学动态规划的...》文章总结的:

动态规划是一种多阶段决策最优解模型,一般用来求最值问题,多数情况下它可以采用自下而上的递推方式来得出每个子问题的最优解(即最优子结构),进而自然而然地得出依赖子问题的原问题的最优解。

包含三个特点:

  1. 多阶段决策:是指原问题可以拆分成子问题以及子子问题
  2. 最优子结构:是指子问题的最优解可以推导为原问题的最优解
  3. 自下而上:由子问题到上层问题之间,寻找迭代递推公式,也叫「状态转移方程」

特别说明一下,「自下而上」区别于递归算法的「自上而下」,后者的子问题可能存在大量的重叠,导致大量的重复计算,这样时间复杂度很可能呈指数级上升。这个从后面的例子可以看出。

套路

怎么判断题目可以使用动态规划

根据上述特点,当问题的定义是求最值问题,且问题可以采用递归的方式,并且递归的过程中有大量重复子问题的时候,基本可以断定问题可以用动态规划求解。故此基本套路是:

  1. 判断是否可用递归来解
  2. 分析在递归的过程中是否存在大量的重复子问题
  3. 采用备忘录的方式来存子问题的解以避免大量的重复计算(剪枝)
  4. 改用自底向上的方式来递推,即动态规划

状态转移方程

实际中写出「状态转移方程」是最困难的,《动态规划详解(修订版)》文章给出了一个套路:

明确「状态」 -> 定义 dp 数组/函数的含义 -> 明确「选择」-> 明确 base case

下面我们用零钱兑换问题来实践上面的套路。

零钱兑换问题

这是 LeetCode 的322题:

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

示例 1:

输入: coins = [1, 2, 5], amount = 11

输出: 3

解释: 11 = 5 + 5 + 1

示例 2:

输入: coins = [2], amount = 3

输出: -1

说明:

你可以认为每种硬币的数量是无限的。

1,递归解法

递归的关键是看问题能否拆分成子问题,并且是否有临界条件。

一,定义递归函数,函数的功能是给定 coins 列表和 amount,返回凑硬币的最少个数:

func exchangeRecursive(coins []int, amount int) int {

}

二,寻找问题与子问题的关系,即递推公式。推导过程如下:

函数记为 f(coins, amount)。假设选择了第一枚硬币,则需要对剩余的 amount - coins[0] 金额求最少硬币数:

f(coins, amount) = f(coins, amount-coins[0]) + 1 (这里的 1 代表选择了第一枚硬币)

如果选择了第二,第三枚呢,递推公式如下:

f(coins, amount) = f(coins, amount-coins[1]) + 1 (这里的 1 代表选择了第二枚硬币)
f(coins, amount) = f(coins, amount-coins[2]) + 1 (这里的 1 代表选择了第三枚硬币) 

选择最优解,递归公式如下:

f(coins, amount) = min{f(coins, amount - coins[i]) + 1)}, 其中 i 的取值为所有硬币的序号

三,逻辑补充到递归函数

//递归函数,返回凑硬币的最小个数,不能凑的情况返回-1
func exchangeRecursive(coins []int, amount int) int {
    //两个临界条件
    if amount == 0 {
        return 0
    }
    if amount < 0 {
        return -1
    }

    //因为是求最小值,所以先初始化为极大值
    var result int = math.MaxInt32
    for _, coin := range coins {
        ret := exchangeRecursive(coins, amount-coin)
        if ret == -1 {
            continue
        }
        result = int(math.Min(float64(result), float64(ret)+1))
    }

    if result == math.MaxInt32 {
        return -1
    }

    return result
}

func coinChangeRecursive(coins []int, amount int) int {
    return exchangeRecursive(coins, amount)
}

分析时间复杂度,由下图递归树可知为多叉树,是 O(n^k),k 是硬币个数,总之是指数级别的。

图自《牛逼了,原来大神都是这样学动态规划的...》

自测通过,但是提交代码超时。分析可知递归解法存在大量重叠子问题,导致的大量重复计算,如上图中,节点9和节点8都被计算了两次,而且每个节点下面都可能是一颗巨大的子树。

2,递归剪枝解法

既然存在重叠子问题,则引入备忘录的方式来存储中间结果。实际中选择数组而不是字典来存储,能提升一点效率:

func coinChangeCut(coins []int, amount int) int {
    m := make([]int, amount+1)
    for i := 0; i <= amount; i++ {
        m[i] = math.MaxInt32
    }
    return exchangeCut(coins, amount, m)
}

//递归函数,加剪枝
func exchangeCut(coins []int, amount int, m []int) int {
    if amount == 0 {
        return 0
    }
    if amount < 0 {
        return -1
    }
    //存储中间结果
    if m[amount] != math.MaxInt32 {
        return m[amount]
    }

    var result int = math.MaxInt32
    for _, coin := range coins {
        ret := exchangeCut(coins, amount-coin, m)
        if ret == -1 {
            continue
        }
        result = int(math.Min(float64(result), float64(ret)+1))
    }

    if result == math.MaxInt32 {
        result = -1
    }
    m[amount] = result

    return result
}

如下图对比可知,剪枝后减少了大量计算量。时间复杂度降至 O(nk) 。

图自《牛逼了,原来大神都是这样学动态规划的...》

3,动态规划解法

前文推导递归公式如下:

f(coins, amount) = min{f(coins, amount - coins[i]) + 1)}, 其中 i 的取值为所有硬币的序号

按照动态规划的套路,需要明确「状态」和「选择」。定义 dp[n] 为凑够零钱金额 n 需要的最小硬币值,此为状态。选择某一个硬币,此为选择。则状态转移方程如下:

dp[n] = min{dp[n - coins[i] + 1} = min{dp[n - coins[i]]} + 1, 其中 i 的取值为所有硬币的序号

以示例1为例(示例 1: 输入: coins = [1, 2, 5], amount = 11),我们只要自底向上根据以上状态转移方程依次求解 dp[1], dp[2], dp[3] ...... dp[11],最终的 dp[11],即为我们所求的解。数学公式表达为:

图自《动态规划详解(修订版)》

再注意初始状态是 dp[0] = 0,表示金额为0的方法为0种。

func coinChangeDP(coins []int, amount int) int {
    dp := make([]int, amount+1) //dp[i]表示amount为i时的最优解
    for i := 0; i <= amount; i++ {
        dp[i] = math.MaxInt32
    }

    //init
    dp[0] = 0

    for _, coin := range coins {
        for i := coin; i <= amount; i++ {
            //状态转移方程
            dp[i] = int(math.Min(float64(dp[i]), float64(dp[i-coin]+1)))
        }
    }

    if dp[amount] == math.MaxInt32 {
        return -1
    }
    return dp[amount]
}

下图就很直观了:

图自《动态规划详解(修订版)》

小结

回头来看,可以看到零钱兑换问题符合之前定义的动态规划问题特点,即可以拆分子问题、符合最优子结构、自下而上存在状态转移方程。

首先用普通迭代解法,存在大量重复计算;接着用剪枝消除重叠子问题;最后用动态规划解法。

其实我们来看动态规划解法的 dp[] 数组,和迭代剪枝解法中的备忘录结构是一样的,不同在于自下而上还是自顶而下的方向。

参考

原文载于动态规划套路

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

推荐阅读更多精彩内容

  • 来自公众号:码海 前言 动态规划(dynamic programming,简称 dp)是工程中非常重要的解决问题的...
    码农小光阅读 1,185评论 2 6
  • 0. 动态规划分析 0.1 动态规划、递归和贪心算法的区别 动态规划就是利用分治思想和解决冗余的办法来处理问题,所...
    dreamsfuture阅读 7,373评论 2 6
  • 动态规划入门 动态规划(Dynamic programming, 简称DP), 通过把原问题分解为相对简单的子问题...
    周闖阅读 612评论 0 1
  • 教您如何选购合适的晚礼服(婚礼) 首先,挑选礼服绝对不是追求时髦和流行的时候,流行一时的东西往往赶不上传统的典雅,...
    相由心生jojo阅读 238评论 0 0
  • 今天星期四,天气很好,早上终于把 (时间的朋友)看完了,与原来计划一个星期看一本推迟了4天, 最近几天我努力 搜索...
    李云蔓阅读 295评论 0 0