前端面试算法篇——动态规划(Dynamic Programming)

696 阅读3分钟

简介

动态规划是大厂面试常考的算法题类型,这类问题类似于数学归纳法,又像求数列的通项公式,即假设[0, n-1]的情况已知,如何去求dp[n]。解决问题的关键自然是找到这个通项公式,也就是状态转移方程。而找到状态转移方程的三个个难点又在于,一是如何定义状态,即DP Table每个位置的含义;二是如何定义选择,即每个状态可以通过其他状态的哪些变化而来;三是,在能够解决问题的情况下,如何根据状态转移方程来缩小空间复杂度。我们在每一个例子中来理解这三个问题。

经典的斐波那契数列问题

求斐波那契数列的第n个数的值(leetcode#509)

var fib = function(n) {
    // 生成dp table
    const dp = new Array(n+1).fill(0)
    dp[0] = 0
    dp[1] = 1
    for(let i = 2; i <= n; i++){
        // 状态转移方程
        dp[i] = dp[i-1]+dp[i-2]
    }
    return dp[n]
};

其实非常的简单明了,dp[i]代表斐波那契数列的第i个数,即状态选择在这里只有一种,就是对前一个数和前前一个数求和;状态转移方程dp[i]=dp[i1]+dp[i2](i>1)dp[i] = dp[i-1]+dp[i-2] (i > 1)。观察发现其实dp[i]只和前两个数的状态有关,并没有必要开辟一个O(n)的空间,所以我们可以进一步状态压缩,每次只记录前两个数字。

var fib = function(n) {
    if(n === 0) return 0
    let prev = 0, cur = 1
    for(let i = 2; i <= n; i++){
        [cur,prev] = [prev + cur,cur]
    }
    return cur
};

换零钱问题

给定一个目标金额amount,以及可用的各种零钱额度coins,求凑够这个金额所使用的最少金币数(leetcode#300)

var coinChange = function(coins, amount) {
    const dp = new Array(amount+1).fill(Infinity)
    dp[0] = 0
    for(let i = 1; i <= amount; i++){
        for(let coin of coins) {
            if(i-coin < 0) continue
            dp[i] = Math.min(dp[i], dp[i-coin]+1)
        }
    }
    return dp[amount] === Infinity ? -1:dp[amount]
}

状态dp[i]代表凑够i金额所使用的最少金币数,选择当然是所有的coins,状态转移方程考虑每一个金额都可以由dp[i-k](k of coins)加上一枚k额度的金币获得,所以dp[i]=min(dp[amountk]+1)(i>=0;kcoins)dp[i] = min(dp[amount-k] + 1) (i >= 0;k∈coins)

最长递增子序列问题(LIS)

求一个数组的最长递增子序列的长度(leetcode#300),子序列的含义是A subsequence is a sequence that can be derived from an array by deleting some or no elements without changing the order of the remaining elements. For example, [3,6,2,7] is a subsequence of the array [0,3,1,6,2,2,7].

// dp[i]代表以第i个元素结尾的最长递增子序列长度
var lengthOfLIS = function(nums) {
    let dp = new Array(nums.length).fill(1)
    for(let i = 0; i < nums.length; i++){
        for(let j = 0; j < i; j++){
            if(nums[j] < nums[i]){
                dp[i] = Math.max(dp[i], dp[j]+1)
            }
        }
    }
    return Math.max.apply(null, dp)
};

状态dp[i]代表以第i个元素结尾的最长递增子序列长度,选择是找当前位置之前,比当前位置数字小的(因为是递增的嘛),状态转移方程是从找到的比当前位置小的位置的dp[j]的最大值加1,即dp[i]=max(dp[j]+1)(j取值为nums[j]<nums[i])dp[i] = max(dp[j] + 1) (j取值为nums[j] < nums[i])。需要注意的是最终的结果是对各个位置结尾的LIS再取一次最大值。