iOS算法刷题之动态规划_递归

动态规划解析

暴力穷举的另外一种算法就是动态规划了,一般有关动态规划的问题,大多是让你求最值的。

动态规划的框架为:

明确basecase -> 明确【状态】 -> 明确【选择】 -> 定义dp数组/函数的含义

在定义dp数组时不要陷入到二维数组、三维数组里,而是不同的状态,然后做不同的选择。

动态规划可以分为两种方式一种是自底向上一种是自顶向下:

# 自顶向下递归的动态规划 
def dp(状态1, 状态2, ...): 
    for 选择 in 所有可能的选择: 
        # 此时的状态已经因为做了选择而改变 
        result = 求最值(result, dp(状态1, 状态2, ...)) 
    return result 
# 自底向上迭代的动态规划 
# 初始化 base case 
dp[0][0][...] = base case 
# 进行状态转移 
for 状态1 in 状态1的所有取值: 
    for 状态2 in 状态2的所有取值: 
        for ... dp[状态1][状态2][...] = 求最值(选择1,选择2...)

求解动态规划的核心问题是穷举,只有列出正确的“状态转移方程”,才能正确地穷举。另外,动态规划问题存在“重叠子问题”,如果暴力穷举的话效率会很低,所以需要使用“备忘录”或者“DP table”来优化穷举过程,避免不必要的计算。最后还可以通过状态压缩来进一步优化复杂度,比如把二维的DP table压缩成一维。

509. 斐波那契数举例:

暴力递归

如果使用暴力递归的话,代码如下:

func fib(_ n: Int) -> Int {
    if n == 1 || n == 0 {
        return n
    }
    return fib(n-1) + fib(n-2)
}

这样虽然可以解决问题,但会有大量的重复计算,如果想计算f(20)就得先计算出子问题f(19)和f(18),然后计算f(19)就要先计算出子问题f(18)和f(17),直到计算到f(1)和f(0),递归结束。

带“备忘录”自顶向下

明确了问题所在之后,可以优化掉耗时的重复计算,可以先把答案存储到“备忘录”中,遇到值了直接取出即可。这样就能避免不必要的耗时计算了,这种方式也是自顶向下递归的方式。

class Solution {
func fib(_ n: Int) -> Int {
    // 备忘录全初始化为-1
    var memo = Array(repeating: -1, count: n+1)
    // 进行带备忘录的递归
    return dfs(memo: &memo, n: n)
}

func dfs(memo: inout [Int], n: Int) -> Int {
    // base case
    if n == 0 || n == 1 {
        return n
    }
    // 已经计算过,不用再计算了
    if memo[n] != -1 {
        return memo[n]
    }
    memo[n] = dfs(memo: &memo, n: n-1) + dfs(memo: &memo, n: n-2)
    return memo[n]
}
}

自底向上

有了“备忘录”的方式,可以吧这个“备忘录”独立出来成一张表,通常叫做DP table。实际上带备忘录的递归解法中的“备忘录”,最终完成后就是这个DP table,只不过是反过来算而已。 数学公式为:

iOS算法刷题之动态规划_穷举_02

class Solution {
    func fib(_ n: Int) -> Int {
        // base case
        if n == 0 || n == 1 {
            return n
        }
        var dp: [Int] = Array(repeating: 0, count: n+1)
        dp[0] = 0
        dp[1] = 1
        if n > 1 {
            for i in 2...n {
                dp[i] = dp[i-1] + dp[i-2]
            }
        }
        return dp[n]
    }
}

状态压缩

通过上面的状态转移方程可以发现,当前状态只和之前的两个状态有关,其实并不需要那么长的DP table来存储所有状态,只保存之前的两个状态即可。

class Solution {
func fib(_ n: Int) -> Int {
    if n == 0 || n == 1 {
        return n
    }
    var dp_i_1 = 1
    var dp_i_2 = 0
    for _ in 2...n {
        let dp_i = dp_i_1 + dp_i_2
        dp_i_2 = dp_i_1
        dp_i_1 = dp_i
    }
    return dp_i_1
}
}

这样就将空间从一个DP数组压缩到了两个值。当然,这种方式是建立在DP table的基础上的,个人建议是如果自顶向下和自底向上两种方式掌握一种之后再去考虑如果进行状态压缩,一般能解答出这种就可以了。

经典动态规划

动态规划的基本思路就是这样,动态规划的题非常多,也是非常高频的题,下面就列一些经典的题。

经典动态规划

70. 爬楼梯剑指Offer 47. 礼物的最大价值62. 不同路径64. 最小路径和编辑距离279. 完全平方数931. 下降路径最小和887. 鸡蛋掉落

子序列问题

516. 最长回文子序列53. 最大子数组和剑指Offer 42. 连续子数组的最大和300. 最长递增子序列

最长公共子序列LCS

1143. 最长公共子序列583. 两个字符串的删除操作712. 两个字符串的最小ASCII删除和

背包问题

322. 零钱兑换416. 分割等和子集518. 零钱兑换 II

股票买卖问题

121. 买卖股票的最佳时机122. 买卖股票的最佳时机 II123. 买卖股票的最佳时机 III188. 买卖股票的最佳时机 IV309. 最佳买卖股票时机含冷冻期714. 买卖股票的最佳时机含手续费

打家劫舍问题

198. 打家劫舍213. 打家劫舍 II337. 打家劫舍 III