动态规划学习总结

248 阅读3分钟

动态规划 (dynamic programming)

基本思想: 动态规划是一种将问题实例分解为更小的/相似的子问题,并存储子问题的解,使得每个子问题只求解一次,最终获得原问题的答案,以解决最优化问题的算法策略。

重要概念

  1. 最优子结构
  2. 边界
  3. 状态转移公式 -> 递推关系

过程

  • 1、根绝问题所求的那一项和变量的个数,确定是一维数组,二维数组或者多维数组。
  • 2、写出初始值,一般是某个变量为1或者0 的特殊情况时候的解。
  • 3、通过循环,一般是两个循环中间每一层循环表示一个变量的递增情况。
  • 4、在循环中间写出对应的递推关系表达式,求出问题的每一项。
  • 5、根据题意得到答案,一般是数组的最后一项。

常见问题

  1. 计数
  • 有多少种方式走到右下角
  • 有多少种方式选出K个数使得和是Sum
  1. 求最值
  • 从左上角到右下角路径的最大数字和
  • 最长上升子序列
  1. 存在性
  • 取石子游戏 先手是否必胜
  • 能不能选出k个数组成Sum

复杂度

  • 时间复杂度O(n^2)
  • 空间复杂度O(1)

解题思路

  1. 一般可以利用递归来解
  2. 通过递归找到递推关系
  3. 找到递归的出口
  4. 获得结果
    // 在一个数组中选择不相邻的数可以组成的最大值 假设数组元素都为正整数
    /**
    * 有两种方式 -> 递推关系
    *      选择第 n 个数
    *          f(n) = f(n-2) + arr[n]
    *      不选择第 n 个数
    *          f(n) = f(n-1)
    * 结束条件 n = 0 -> arr[0]
    *         n = 1 -> max(arr[0], arr[1])
    */

    // 递归解法
    function rec_opt(arr, n) {
        if (n === 0) return arr[0]
        else if (n === 1) return Math.max(arr[0], arr[1])
        else {
            let a = rec_opt(arr, n - 2) + arr[n]
            let b = rec_opt(arr, n - 1)
            return Math.max(a, b)
        }
    }

    // 动态规划解法
    function dp_opt(arr) {
        let l = arr.length
        let result = new Array(l).fill(0)
        // 初始值
        result[0] = arr[0] 
        result[1] = Math.max(arr[0], arr[1])
        for (let i = 2; i < l; i++) {
            // 条件判断 递推关系
            let a = result[i - 2] + arr[i]
            let b = result[i - 1]
            result[i] = Math.max(a, b)
        }
        return result[l - 1]
    }

    let a = [1, 2, 4, 2, 3]
    console.log('dp_opt(a)', dp_opt(a))
    console.log('dp_opt(a)', rec_opt(a, a.length - 1))


    // 长度为 n 的数组中是否存在可以组成 m 的数 假设数组元素都为正整数
    /**
    * 有两种方式 -> 递推关系
    *      选择第 n 个数
    *          subSet(arr, n, m) = subSet(arr, n - 1, m - arr[n])
    *      不选择第 n 个数
    *          subSet(arr, n, m) = subSet(arr, n - 1, m)
    * 结束条件 m = 0 -> true
    *         n = 0 -> arr[n] === m
    */

    // 递归解法
    function rec_subSet(arr, n, m) {
        if (m === 0) return true
        else if (n === 0) return arr[n] === m
        else if (arr[n] > m) return rec_subSet(arr, n - 1, m)
        else {
            let a = rec_subSet(arr, n - 1, m - arr[n])
            let b = rec_subSet(arr, n - 1, m)
            return a || b
        }
    }

    // 动态规划解法
    function dp_subSet(arr, m) {
        let l = arr.length
        // 设置初始值 填空
        let result = new Array(l).fill(0).map(item => new Array(m + 1).fill(false)).map(item => {
            item[0] = true
            return item
        })
        result[0][arr[0]] = true
        for (let i = 1; i < l; i++) {
            for (let j = 1; j < m + 1; j++) {
                if (arr[i] > m) result[i][j] = result[i - 1][j]
                else {
                    let a = result[i - 1][j - arr[i]]
                    let b = result[i - 1][j]
                    result[i][j] = a || b
                }
            }
        }
        return result[l - 1][m]
    }

    let b = [1, 2, 4, 2, 3]
    console.log('dp_subSet', dp_subSet(b, 13))
    console.log('rec_subSet', rec_subSet(b, b.length - 1, 13))


    // 爬楼梯问题 每次只能走一步或者两步 到第N个台阶有多少种组合
    /**
    * -> 递推关系
    *      选择第 n 个数
    *          f(n) = f(n-1) + f(n-2)
    * 结束条件 n = 1 -> 1
    *         n = 2 -> 2
    */

    // 递归解法
    function rec_climbStairs(n) {
        if (n === 1) return 1
        if (n === 2) return 2
        return rec_climbStairs(n - 1) + rec_climbStairs(n - 2)
    }

    // 动态规划解法
    function dp_climbStairs(n) {
        let arr = new Array(n + 1)
        arr[0] = 1
        arr[1] = 1
        arr[2] = 2
        for (let i = 3; i < n + 1; i++) {
            arr[i] = arr[i - 1] + arr[i - 2]
        }
        return arr[n]
    }
    
    console.log('rec_climbStairs', rec_climbStairs(33))
    console.log('dp_climbStairs', dp_climbStairs(33))
   

总结

学习资料