美文网首页
动态规划(Dynamic Programming)

动态规划(Dynamic Programming)

作者: Bel李玉 | 来源:发表于2020-08-26 23:48 被阅读0次

动态规划简称DP,是一种求解最优化问题的一种常用策略,它会尝试每一种选择,来获取解决问题的最优解。
我们下面通过之前在贪心策略里面的提到的找零钱的例子,来探讨动态规划。我们先来回顾下找零钱这个问题。

  • 假设有25分、20分、5分、1分的硬币,现要找给客户41分的零钱,如何办到硬币个数最少?

贪心策略文章中,我们探讨了该问题使用贪心策略得到的解为需5枚硬币,并非最优解。

分析

我们假设dp(n)是凑到 n 分需要的最少硬币数,分析如下:

  • 如果第1次选择了 25 分的硬币,那么 dp(n) = dp(n - 25) + 1
  • 如果第1次选择了 20 分的硬币,那么 dp(n) = dp(n - 20) + 1
  • 如果第1次选择了5分的硬币,那么 dp(n) = dp(n - 5) + 1
  • 如果第1次选择了1分的硬币,那么 dp(n) = dp(n - 1) + 1

综上所述 dp(n) = min{ dp(n - 25), dp(n - 20), dp(n - 5), dp(n - 1)} + 1

实现

按照以上思路,我们可以先用 递归的思想来实现,我们新建一个类 DynamicPrograming

class DynamicProgramming {
     static let changes = [25, 20, 5, 1]
// coin Change
    static func coinChange(_ num: Int) -> Int{ // 1
        if num <= 0 {
            return Int.max  // 2
        }

        for change in changes {
            if num == change {
                return 1 // 3
            }
        }
        let one = coinChange(num - 25) //4
        let two = coinChange(num - 20)
        let three = coinChange(num - 5)
        let four = coinChange(num - 1)

        let five = min(one, two)
        let six = min(three, four)

        return  min(five, six) + 1 // 5
    }
}
  • 1,coinChange(:)->Int方法表示需要对 n 元进行找零需要的最少硬币个数。
  • 2,因为我们要取最小值,当需要找零的数值小于0时,此时我们返回最大的整数。
  • 3,当需找零的值和零钱的面值相等,此时,需要的最小零钱个数为1。
  • 4,分别计算出 num - 25num - 20num - 5num - 1所需的最小零钱个数。
  • 5, 返回 4中方案的最小值 + 1。

这样的话,我们就可以求出找零时,需要的最小零钱个数。但是,上面通过递归的方式计算,有可能会存在一些重复计算,假设对4元进行找零,我们会多次 计算 coinChange(1)。我们可以进行记忆化搜索,将计算结果保存起来,当需要再次计算的时候,直接将值返回即可。我们可以使用 字典 进行保存,也可以使用数组进行保存。在这里,我们先使用字典进行保存

static func coinChangeTwo(_ num: Int) -> Int {
        if num <= 0 {
            return Int.max
        }

        for change in changes {
            if change == num {
                return 1
            }
        }

        if dpDic[num - 25] == nil {
            dpDic[num - 25] = coinChange(num - 25)
        }

        if dpDic[num - 20] == nil {
            dpDic[num - 20] = coinChange(num - 20)
        }

        if dpDic[num - 5] == nil {
            dpDic[num - 5] = coinChange(num - 5)
        }

        if dpDic[num - 1] == nil {
            dpDic[num - 1] = coinChange(num - 1)
        }

        let one = dpDic[num - 25] ?? Int.max
        let two = dpDic[num - 20] ?? Int.max
        let three = dpDic[num - 5] ?? Int.max
        let four = dpDic[num - 1] ?? Int.max

        let minOne = min(one, two )
        let minTwo = min(three, four)
        return min(minOne, minTwo) + 1
    }

我们进一步分析该代码,如果我们想求出 dp(41):对41元进行找零,所需的最少硬币个数,我们需要 先求出 dp(16)、dp(21)、dp(36)、dp(40),由此,我们可以看出,我们可以先求 dp(1)、dp(2)、dp(3)直到 dp(n)。自底而上的递推出每个零钱方案。根据我们之前分析的结果 dp(n) = min{ dp(n - 25), dp(n - 20), dp(n - 5), dp(n - 1)} + 1,我们可以

 var dp = Array.init(repeating: 0, count: num + 1) // 1
        for i in 1...num {
            var minValue = Int.max
            minValue = min(dp[i - 1], minValue)
            minValue = min(dp[i - 5], minValue)
            minValue = min(dp[i - 20], minValue)
            minValue = min(dp[i - 25], minValue) // 2
            dp[i] = minValue + 1  // 3
        }
  • 1,初始化一个长度为 num + 1的数组,用来存储dp(n)的值。
  • 2,求出dp[i]的值。
  • 3,对dp[i]进行存储。

为防止数组越界,我们额外需要增加一些判断

    static func coinsChangeThree(_ num: Int) -> Int {

        if num <= 0 {
            return Int.max
        }
        var dp = Array.init(repeating: 0, count: num + 1)

        for i in 1...num {
            var minValue = Int.max
            //            dp[index] = min {dp[i-25], dp[i-20], dp[i-5], dp[i-1]} + 1
            if i >= 1 {
                minValue = min(dp[i - 1], minValue)
            }

            if i >= 5  {
                minValue = min(dp[i - 5], minValue)
            }

            if i >= 20 {
                minValue = min(dp[i - 20], minValue)
            }

            if i >= 25 {
                minValue = min(dp[i - 25], minValue)
            }

            dp[i] = minValue + 1
        }

        return dp[num]
    }

如上所示,我们先算出 dp(1) = 1,然后计算 dp(2) = dp(1) + 1 = 2,然后在由 dp(2)推到出dp(3),最后,推导出 dp(n)。我们将所有的找零需要的最少硬币数都存在了数组里面。

从这个过程中,我们可以看出,我们首先是要找到问题状态转移方程即上面的dp(n) = min{ dp(n - 25), dp(n - 20), dp(n - 5), dp(n - 1)} + 1,通过这个状态转移方程,我们可以使用递归的方式实现,如果递归算法中有许多的重复计算,我们采用记忆化搜索来避免重复计算。最后,采用自底而上的递推来避免递归调用。

相关文章

网友评论

      本文标题:动态规划(Dynamic Programming)

      本文链接:https://www.haomeiwen.com/subject/swptsktx.html