今天主要通过一个经典的问题来讲解贪心策略和动态规划。
贪心策略概念就是:每一步都采取当前状态下最优的选择(局部最优解),从而希望推导出全局最优解。
动态规划的核心思想是:通过求解子问题的最优解,然后推导出原问题的最优解。
本文先介绍下贪心算法的缺点进而引出动态规划以及动态规划的解题中间的详细流程。
题目来源于 LeetCode 的第 206 题,难度为:中等。目前的通过率是43.6%。
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -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枚硬币
贪心策略并不一定能得到全局最优解
◼ 优点:简单、高效、不需要穷举所有可能,通常作为其他算法的辅助算法来使用 ◼ 缺点:鼠目寸光,不从整体上考虑其他可能,每次采取局部最优解,不会再回溯,因此很少情况会得到最优解
动态规划 (dp) 是求解最优化问题的一种常用策略。它的核心思想是:通过求解子问题的最优解,然后推导出原问题的最优解。
关于思路,其核心主要是三步:
1.定义状态
2.设置状态的初始值
3.确定状态转移方程。
关于概念,我们先大致的了解到这一步,我接下来通过一个例子,来演示动态规划的解题思路,然后大家再反过来看概念,这样会好理解一些。
题目:coins = [1,5,10,20,25], amount = 41,求最少需要几枚硬币?
三步流程如下:
解释:
状态定义的意思: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
以例子来分析:状态转移方程
局部总结:核心就是子问题的最优解,然后根据状态转移方程求出原问题的最优解
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. 连续子数组的最大和。
end