LeetCode算法:贪心策略到动态规划

今天主要通过一个经典的问题来讲解贪心策略和动态规划。


贪心策略概念就是:每一步都采取当前状态下最优的选择(局部最优解),从而希望推导出全局最优解。


动态规划的核心思想是:通过求解子问题的最优解,然后推导出原问题的最优解。


本文先介绍下贪心算法的缺点进而引出动态规划以及动态规划的解题中间的详细流程。


题目来源于 LeetCode 的第 206 题,难度为:中等。目前的通过率是43.6%。


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

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

 

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


1.贪心策略
◼ 贪心策略:每一次都优先选择面值最大的硬币

  • 选择 25 分的硬币,剩 16 分
  • 选择10分的硬币,剩6分
  • 选择5分的硬币,剩1分
  • 选择 1 分的硬币

最终的解是共 4 枚硬币:25 分、10 分、5 分、1 分硬币各一枚

let faces = [1, 5, 10, 25] //先调成升序
    var target = 41
    greedy(faces: faces, target: &target)   
 
     func greedy(faces: [Int], target: inout Int) -> Int {
        var count = 0
        var index = faces.count - 1
        while index >= 0 {
            while target >= faces[index]  {
                target -= faces[index]
                count += 1
            }
            index -= 1
        }
        return count
    }

实际上本问题的最优解是:2枚20分、1枚1分,共3枚硬币

 

贪心策略并不一定能得到全局最优解

因为一般没有测试所有可能的解,容易过早做决定,所以没法达到最佳解

贪图眼前局部的利益最大化,看不到长远未来,走一步看一步

◼ 优点:简单、高效、不需要穷举所有可能,通常作为其他算法的辅助算法来使用
◼ 缺点:鼠目寸光,不从整体上考虑其他可能,每次采取局部最优解,不会再回溯,因此很少情况会得到最优解

 

2. 动态规划(Dynamic Programng)
  动态规划 (dp) 是求解最优化问题的一种常用策略。它的核心思想是:通过求解子问题的最优解,然后推导出原问题的最优解。

关于思路,其核心主要是三步:

1.定义状态

2.设置状态的初始值

3.确定状态转移方程。

 

关于概念,我们先大致的了解到这一步,我接下来通过一个例子,来演示动态规划的解题思路,然后大家再反过来看概念,这样会好理解一些。

 

题目:coins = [1,5,10,20,25], amount = 41,求最少需要几枚硬币?

三步流程如下:

1.定义状态dp(i),i 表示amount的值

2.设置初始状态的值dp(0) = 0

3.确定状态转移方程dp(i) = min {dp(i-coin[x])} + 1 == min{ dp(i-1), dp(i-5), dp(i-10), dp(i-25))} + 1

 

解释:

状态定义的意思:dp(41) 表示凑够41需要的最少硬币数量。
那么 dp(41-1) = dp(40) 表示凑够40需要的最少硬币数量。
那么 dp(41-5) = dp(36) 表示凑够36需要的最少硬币数量。
那么 dp(41-10) = dp(31) 表示凑够31需要的最少硬币数量。
那么 dp(41-20) = dp(21) 表示凑够21需要的最少硬币数量。
那么 dp(41-25) = dp(16) 表示凑够16需要的最少硬币数量。
所以 dp(41) = min{ dp(40)、dp(36)、dp(31)、dp(21)、dp(16) } + 1

 

如何理解这种操作,以及为什么加1操作?
答:比如dp(41) = dp(21) + 1,表示凑够41,只需要在凑够21的基础上再加一枚20的硬币就行。加1是因为选择了20分这枚硬币,所以+1

 

以例子来分析:状态转移方程

amount = 1需要最少的硬币数是1,所以dp(1) = min{ dp(1-1), dp(1-5), dp(1-10),dp(1-20), dp(1-25))} + 1 == dp[0]+ 1 = 1

amount = 2需要最少的硬币数是2【2枚 1分】,所以dp(2) = min{ dp(2-1), dp(2-5), dp(2-10),dp(2-20), dp(2-25))} + 1 == dp[1]+ 1 = 2

amount = 3需要最少的硬币数是3【3枚 1分】,所以dp(3) = min{ dp(3-1), dp(3-5), dp(3-10),dp(3-20), dp(3-25))} + 1 == dp[2]+ 1 = 3

amount = 4需要最少的硬币数是4【4枚 1分】,所以dp(4)= min{ dp(4-1), dp(4-5), dp(4-10),dp(4-20), dp(4-25))} + 1 == dp[3]+ 1 = 4

amount = 5需要最少的硬币数是1【1枚 5分】,所以dp(5) = min{ dp(5-1), dp(5-5), dp(5-10),dp(5-20), dp(5-25))} + 1 == dp[0]+ 1 = 1

amount = 6需要最少的硬币数是1【2枚 1分 5分】,所以dp(6) = min{ dp(6-1), dp(6-5), dp(6-10),dp(6-20), dp(6-25))} + 1 == dp[5]+ 1 = 2

...

amount = 21需要最少的硬币数是2【2枚 1分 20分】,所以dp(21) = min{ dp(21-1), dp(21-5), dp(21-10),dp(21-20), dp(6-25))} + 1 == dp[1]+ 1 = 2

...

amount = 41需要最少的硬币数是3【3枚 1分 20分 20分】,所以dp(41) = min{ dp(41-1), dp(41-5), dp(421-10),dp(41-20), dp(41-25))} + 1 == dp[21]+ 1 = 3

局部总结:核心就是子问题的最优解,然后根据状态转移方程求出原问题的最优解

 

代码实现:

class Solution {
    func coinChange(_ coins: [Int], _ amount: Int) -> Int {
    if amount < 1 { return -1 }
    var dp = [Int](repeating: amount + 1, count: amount+1)
    dp[0] = 0
    for i in 1...amount {
        for coin in coins {
            if i >= coin  {
                dp[i] = min( dp[i], (dp[i-coin] + 1) )
            }
        }
    }
    return dp[amount] > amount ? -1 : dp[amount]
    }
}

 

代码解析:
  初始化dp[n+1],其中n=amount ,然后 dp[1..n]里的值都为amount+1即本例子中的 41+1 = 42,并设置dp[0] = 0。
  for循环中从1开始遍历,其中,i>=coin才可以求dp[i];求dp[1] = min(dp[1],dp[0]+1),然后一直到dp[n].
  最后返回结果

 

结尾
  动态规划这种类型的题目,理解概念很重要,但是通过去做题来理解概念可能会更好的掌握。如果大家有兴趣,可以做下这道题:leetcode上的剑指 Offer 42. 连续子数组的最大和。

 

欢迎关注【无量测试之道】公众号,回复【领取资源】
Python编程学习资源干货、
Python+Appium框架APP的UI自动化、
Python+Selenium框架Web的UI自动化、
Python+Unittest框架API自动化、
资源和代码 免费送啦~
文章下方有公众号二维码,可直接微信扫一扫关注即可。

备注:我的个人公众号已正式开通,致力于测试技术的分享,包含:大数据测试、功能测试,测试开发,API接口自动化、测试运维、UI自动化测试等,微信搜索公众号:“无量测试之道”,或扫描下方二维码:


添加关注,让我们一起共同成长!

 

posted on 2021-08-18 14:58  Wu_Candy  阅读(98)  评论(0编辑  收藏  举报