动态规划

20 阅读7分钟

1. 什么是动态规划

  • 动态规划是递归的思想,用空间换时间,是一种寻找最优解的方法。
  • 动态规划有起始节点,有终止节点,每一个节点代表一个状态。任何一个非起始节点都可以从其他节点转移过来,即为状态转移。
  • 解决动态规划问题的关键:如何设计状态
    • 动态规划用空间换时间,可以利用内存的缓存能力,换取CPU的计算消耗,只要设计出状态转移图,就可以利用计算机的强大算力,通过初始状态计算出终止状态,从而求得问题的解。
  • 动态规划求解步骤
    • 设计状态
    • 确定状态转移方程
    • 确定初始状态
    • 执行状态转移
    • 计算最终的解
  • 有向无环图

2. 递推

  • 青蛙跳问题
//循环
const arr = []
function numWays(n) {
    if (n < 0) {
        throw new Error("n应该大于0~");
    }
    arr[0] = arr[1] = 1;
    for (let i = 2; i <= n; i++) {
        arr[n] = arr[n - 2] + arr[n - 1];
    }
    return arr[n];
}
//递归
function f(n) {
    if (n < 0) {
        throw new Error("n应该大于0~");
    }
    if (n <= 1) {
        return 1;
    }
    return f(n-2)+f(n-1);
}
  • 爬楼梯问题
//循环
function climbStairs(n) {
    const dp[i] = [];
    dp[1] = 1;
    dp[2] = 2;
    for (let i = 3; i <= n; i++) {
        dp[i] = dp[i- 1] + dp[i - 2];
    }
    return dp[n]
}
//递归
function fn(sum) {
    if (sum > n) {
        return 0;
    }
    if (sum == n) {
        return 1;
    }
    return fn(sum + 1) + fn(sum + 2)
}

3. 线性DP

  • 状态转移
    • 打家劫舍(首尾相连)
    //创建一个二维数组
    //dp[i][0]表示到第i个元素,且第0个元素不选的情况;dp[i][1]表示到第i个元素为止,且第0个元素选择的情况,前面所有元素选择的最大和
    const nums = [1, 2, 3, 1]
    const dp = new Array(nums.length).fill(0).map(item => new Array(2).fill(0));
    function rob(nums) {
        const n = nums.length;
        if (n == 1) {
            return nums[0];
        }else if (n == 2) {
            return Math.max(nums[0], nums[1]);
        } 
        dp[0][0] = 0;
        dp[0][1] = nums[0];
        for (let i = 1; i < n; i++) {
            for (let j = 0; j < 2; j++) {
                if (i == 1) {
                    if (j == 0) {
                        dp[i][j] = nums[1]
                    } else {
                        dp[i][j] = nums[0]
                    }
                } else if (i == n-1 && j == 1) {
                    dp[i][j] = dp[i-1][j]
                } else {
                    dp[i][j] = Math.max(dp[i-1][j], dp[i-2][j] + nums[i])
                }
            }
        }
        return Math.max(dp[n][0], dp[n][1])
    }
    
    • 分割数组以得到最大和
    const arr = [1, 15, 7, 9, 2, 5, 10];
    const k = 3;
    const dp = new Array(arr.length).fill(0);
    function maxSumAfterSplit(arr, k) {
        let max = 0;
        let current = 0;
        for (let i = 0; i < arr.length; i++) {
            max = 0;
            current = 0;
            for (j = i; j >= 0; j--) {
                current++
                if (arr[j] > max) {
                    max = arr[j];
                }
                if(current > k) {
                    break
                }
                if (j) {
                    dp[i] = Math.max(dp[i], dp[j-1] + current * max);
                } else {
                    dp[i] = Math.max(dp[i], current * max);
                }
            }
        }
        return dp[arr.length-1]
    }
    
  • 前缀和
    • 寻找数组的中心下标
    const nums = [1, 7, 3, 6, 5, 6]
    const sum = []
    function pivoIndex(nums) {
        const n = nums.length;
        for (let i = 0; i < n; i++) {
            sum[i] = nums[i]'
            if (i) {
                sum[i] += sum[i-1]
            }
        }
        if (sum[n-1] - sum[0]) return 0
        for (let i = 0; i < n; i++) {
            if (sum[i-1] == sum[n-1] - sum[i]) return i
        }
        return -1
    }
    

4. 二维DP

  • 统计全为1的矩阵
const matrix = [
    [0, 1, 1, 1],
    [1, 1, 1, 1],
    [0, 1, 1, 1]
]
const m = martix.length;
const n = matrix[0].length;
const len = Math.min(m, n);
// mat[l][i][j]代表长度为l且从(i,j)开始的矩阵
const mat = new Array(len).fill(0).map(() => new Array(n).fill(0).map(() => new Array(n).fill(0)))
function countSquares(matrix) {
    let ans = 0;
    for (let l = 1; l <= len; l++) {
        for (let i = 0; i + 1 <= n; i++) {
            for (let j = 0; j + 1 <= m; j++) {
                if (l == 1) {
                    mat[l][i][j] = matrix[i][j]
                } else {
                    // 当l大于1时,需要满足matrix[i][j]为1并且长度为l-1的三个矩阵全都为1
                    mat[l][i][j] = matrix[i][j] && mat[l-1][i+1][j] && mat[l-1][i][j+1] && mat[l-1][i+1][j-1]
                }
                ans += mat[l][i][j]
            }
        }
    }
    return ans;
}

5. 经典DP

  • 最大子数组
    • 连续子数组的最大和
    const nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4];
    // dp[i]表示第i个数字结尾的数组的最大值
    const dp = new Array(nums.length).fill(0);
    function maxSubArray(nums) {
        let maxValue = nums[0];
        for (let i = 0; i < nums.length; i++) {
            dp[i] = nums[i]
            if (dp[i-1] > 0) {
                dp[i] += dp[i-1]
            }
            maxValue = Math.max(maxValue, dp[i])
        }
        return maxValue;
    }
    
  • 最长单调子序列
    • 最长递增子序列
    const nums = [10, 9, 2, 5, 3, 7, 101, 18];
    function lengthOfLIS(nums) {
        let dp = []
        let max = 0
        for (let i = 0; i < nums.length; i++) {
            dp[i] = 1
            for (let j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    if (dp[j] + 1 > dp[i]) {
                        dp[i] = dp[j] + 1
                    }
                }
            }
            max = Math.max(max, dp[i])
        }
        return max
    }
    lengthOfLIS(nums)
    
    • 最长递增子序列的个数
    const nums = [1, 3, 5, 4, 7];
    function findNumberOfLIS(nums) {
        let dp = []
        let cnt = []
        for (let i = 0; i < nums.length; i++) {
            dp[i] = cnt[i] = 1
            let str = ""
            for (let j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    // dp[i] = Math.max(dp[i], dp[j] + 1)
                    if (dp[j] + 1 > dp[i]) {
                        dp[i] = dp[j] + 1
                        cnt[i] = cnt[j]
                    } else if (dp[i] + 1 == dp[i]) {
                       cnt[i] += cnt[j] 
                    }
                }
            }
        }
        return cnt[nums.length - 1]
    }
    
    • 最长等差数列
    const nums = [3, 6, 9, 12]
    function logestArithSeqLength(nums) {
        let ans = 0
        for (let i = -500; i <= 500; i++) {
            ans = Math.max(ans, longestArithSeq(nums, i)
        }
        return ans
    }
    function longestArithSeq(nums, diff) {
        let dp = []
        let max = 0
        let val
        for (let i = 0; i < nums.length; i++) {
            dp[i] = 0
            // 如果当前的数减去diff小于0,那么当前的数只能是序列中第一个数 
            if (nums[i] - diff < 0) {
                val = 0
            } else {
                val = dp[nums[i] - diff]
            }
            if (val + 1 > dp[nums[i]]) {
                dp[nums[i]] = val + 1
                max = Math.max(max, val + 1)
            }
        }
        return max
    }
    
    • 最长斐波那契数列
    // 二分查找:查找给定区间是否存在这个数
    const arr = [1, 2, 3, 4, 5, 6, 7, 8]
    function lenLongestFibSubseq(arr) {
        const dp = new Array(arr.length).fill(1)
        let ans = 0
        for (let i = 0; i < arr.length; i++) {
            for(let j = i + 1; j < arr.length; j++) {
            // 在区间(0,i-1)中找到给定数字的下标
            const idx = searchIdx(arr, 0, i-1, arr[j]-arr[i])
            if (idx != -1) {
                dp[i][j] = dp[idx][j] + 1
            } else {
                dp[i][j] = 2
            }
            ans = Math.max(ans, dp[i][j]
            }
        }
        return ans
    }
    // 查找数组区间内给定值的下标
    function searchIdx(arr, target, start, end) {
        let mIdx = Math.floor((start + end) / 2)
        if (!arr.includes(target)) {
            return -1
        }
        while (start <= end) {
            if (arr[mIdx] < target) {
                searchIdx(arr, target, mIdx + 1, end)
            } else if (arr[mIdx] > target) {
                searchIdx(arr, target, start, mIdx - 1)
            } else {
                return mIdx
            }
        }
        return -1
    }
    
  • 最长公共子序列
    • 最长公共子序列
    // text1 = "abcde", text2 = "ace"
    function longestCommonSubsequence(text1, text2) {
        for (let i = 0; i < text1.length; i++) {
            for (let j = 0; j < text2.length; j++) {
                const same = (text1[i] == text2[j] ? 1 : 0)
                if (i == 0 && j == 0) {
                    dp[i][j] = same
                } else if (i == 0) {
                    dp[i][j] = dp[i][j-1] || same
                } else if (j == 0) {
                    dp[i][j] = dp[i-1][j] || same
                } else if (same) {
                    dp[i][j] = dp[i-1][j-1] + 1
                } else {
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][i-1])
                }
            }
        }
        return dp[text1.length-1][text2.length-1]
    }
    
    • 最长回文子序列
    // 可以转换成一个串和它的逆序串的最长公共子序列问题
    // s = "bbbab"
    function longestPalindromeSubseq(t1, t2) {
        const n = t1.length;
        const m = t2.length;
        const dp = []
        for (let i = 0; i < n; i++) {
            for (let j = 0; j < m; j++) {
                const same = (t1[i] == t2[j]) ? 1 : 0
                if (i == 0 && i == 0) {
                    dp[i][j] = same
                } else if (i == 0) {
                    dp[i][j] = dp[i][j-1] || same
                } else if (j == 0) {
                    dp[i][j] = dp[i-1][j] || same
                } else if (same) {
                    dp[i][j] = dp[i-1][j-1] + same
                } else {
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])
                }
            }
        }
        return dp[n-1][m-1]
    }
    longestPalindromeSubseq(s, s.split("").reverse().join(""))
    
  • 最短编辑距离
    • 编辑距离
    // word1 = "horse", word2 = "ros"
    const m = word1.length;
    const n = word2.length;
    const dp = new Array(m).fill(0).map(() => new Array(n).fill(0))
    const I = D = R = 1;
    function minDistance(word1, word2) {
        // 初始化数组
        // 删除的操作
        for (let i = 0; i < world1.length; i++) {
            setdp(i, -1, (i+1) * D)
        }
        // 插入的操作
        for (let i = 0; i < word2.length; i++) {
            setdp(-1, i, (i+1) * I)
        }
        for (let i = 0; i < word1.length; i++) {
            for (let j = 0; j < word2.length; j++) {
                const ICost = getdp(i, j-1) + I
                const DCost = getdp(i-1, j) + D
                const RCost = getdp(i-1, j-1) + ((word1[i] == word2[j]) ? 0 : R)
                const ans = Math.min(Math.min(ICost, DCost), RCost)
                setdp(i, j, ans)
            }
        }
        return dp[m-1][n-1]
    }
    // 下标有可能是-1,可以避免下标越界
    function setdp(r, c, val) {
        dp[r+1][c+1]] = val
    }
    function getdp(r, c) {
        if (r == -1 && c == -1) {
            return 0
        }
        return dp[r+1][c+1]
    }
    
  • 杨辉三角
    • 杨辉三角
    // numRows = 5
    function generate(numRows) {
       const dp = []
        for (let i = 0; i < numRows; i++) {
            const v = [];
            for (let j = 0; j <= i; j++) {
                //当j=0或者j=i时,代表是杨辉三角的两边
                if (!j || j == i) {
                    v.push(1)
                } else {
                    const val = dp[i-1][j-1] + dp[i-1][j]
                    v.push(val)
                }
            }
            dp.push(v)
        }
        return dp
    }
    
    • 路径的问题
    function uniquePaths(m, n) {
        const dp = []
        for (let i = 1; i <= m; i++) {
            for (let j = 1; j <= n; j++) {
                if (i == 1 || j == 1) {
                    dp[i][j] = 1
                } else {
                    dp[i][j] = dp[i-1][j] + dp[i][j-1]
                }
            }
        }
        return dp[m][n]
    }
    
  • 经典股票问题
    • 买入股票的最佳时机
    // [7, 1, 5, 3, 6, 4]
    function maxProfit(prices, n) {
        let ans = 0
        const dp = calcPrevMin(prices, n, [])
        // 寻找当前i之前的最小值的差,即为当前i的最大利润,再求取所有i的最大利润
        for (let i = 1; i < n; i++) {
            ans = Math.max(ans, price[i] - prev[i-1])
        }
        return ans
    }
    function calcPrevMin(nums, n, prevMin) {
        for (let i = 0; i < n; i++) {
            if (i == 0) {
                prevMin[i] = nums[i]
            } else {
                prevMin[i] = Math.min(prevMin[i-1], nums[i-1])
            }
        }
        return prevMin
    }
    
  • 零一背包
    • 分割等和子集
    function canPartition(nums) {
        const dp = []
        let sum = 0
        for (let i = 0; i <nums.length; i++) {
            sum += nums[i]
        }
        if (sum % 2 == 1) {
            return false
        }
        sum /= 2
        for (let i = 0; i < nums.length; i++) {
            for (let j = sum; j >= nums[i]; j--) {
                dp[j] = dp[j - nums[i]] || dp[j]
            }
            if (dp[sum] == 1) return true
        }
        return false
    }
    
  • 完全背包
  • 分组背包
  • 博弈DP
  • 记忆化搜索
  • 状态DP