动态规划入门及经典基础算法

一、思想与性质

  • 动态规划的核心思想是把原问题分解成子问题进行求解,也就是分治的思想。

那么什么问题适合用动态规划呢?我们通过一个现实中的例子,来理解这个问题。大家可能在公司里面都有一定的组织架构,可能有高级经理、经理、总监、组长然后才是小开发,今天我们通过这个例子,来讲讲什么问题适合使用动态规划。又到了一年一度的考核季,公司要挑选出三个最优秀的员工。一般高级经理会跟手下的经理说,你去把你们那边最优秀的3个人报给我,经理又跟总监说你把你们那边最优秀的人报给我,经理又跟组长说,你把你们组最优秀的三个人报给我,这个其实就动态规划的思想!

在这里插入图片描述

首先是重叠子问题,不同的问题,可能都要求1个相同问题的解。
其次是最优子结构,最优解肯定是有最优的子解转移推导而来,子解必定也是子问题的最优解。
第三是无后效性,这个问题可能比较难理解,也就是求出来的子问题并不会因为后面求出来的改变。

二、动画规划过程

  1. 划分状态,即划分子问题,例如上面的例子,我们可以认为每个组下面、每个部门、每个中心下面最优秀的3个人,都是全公司最优秀的3个人的子问题
  2. 状态表示,即如何让计算机理解子问题。上述例子,我们可以实用f[i][3]表示第i个人,他手下最优秀的3个人是谁。
  3. 状态转移,即父问题是如何由子问题推导出来的。上述例子,每个人大Leader下面最优秀的人等于他下面的小Leader中最优秀的人中最优秀的几个。
  4. 确定边界,确定初始状态是什么?最小的子问题?最终状态又是什么。例如上述问题,最小的子问题就是每个小组长下面最优秀的人,最终状态是整个企业,初始状态为每个领导下面都没有最优名单,但是小组长下面拥有每个人的评分。

三、经典模型

1.线性模型

斐波那楔数列:每个数的值都是一个状态,可以用F[i]表示表示第i个数的值是多少,每个数都是由F[i-1]+F[i-2]转移而来

最长上升自序列(LIS):有一串序列,要求找出它的一串子序列,这串子序列可以不连续,但必须满足它是严格的单调递増的且为最长的。把这个长度输出。示例:1 7 3 5 9 4 8 结果为4

线性模式还可以拓展成二维问题,例如背包问题,用f[i][j]表示前i个物品,凑成大小为j的背包,最大的价值是多少。

这类问题非常的多,但是思路都是这样,无非就是从左往右,从上到下,从低维到高维进行转移。

2.区间模型

  • 对于每个问题,都是由子区间推导过来的,我们称之为区间模型,下面是一个例子:
    我们有一个连续的序列,每个序列上面都是一个数字c[i],每次我们都能够消灭一个连续的回文子序列,消灭之后左右会合并,成为一个新序列,问最少需要多少次才能够把整个序列消灭掉。回文就是从左到有从右到左读到的序列都是一样的。题目比较抽象,我们通过一些例子来说明这个问题吧?例如一开始的序列是1 4 4 2 3 2 1,那么我们最少需要2次,先消灭掉4 4 , 然后再消灭调1 2 3 2 1.第二个例子是 1 2 3 4 5 5 3 1,我们先消灭掉2 然后再消灭掉4, 最后消灭 1 3 5 5 3 1, 需要3次。

我们经常用f[i][j]来表示消灭i,j区间需要的代价

3.树状模型

我们在数据结构树上面进行最求最优解、最大值等问题,上述我们讲的这个绩效考核就是一个树状模型。

四、实现的方法

一个是自底向上,另外一个是自顶向下。无论是何种方式,我们都要明确动态规划的过程,把状态表示、状态转移、边界都考虑好。

1、自底向上

简单来说就是根据初始状态,逐步推导到最终状态,而这个转移的过程,必定是一个拓扑序。

自底向上一般用来解决什么问题呢?那就是可以轻松确定拓扑序的问题,例如线性模型,都是从左往右进行转移,区间模型,一般都是从小区间推导到大区间。自底向上的一个经典实现是斐波那楔数列的递推实现,即F[i] = F[i - 1] + F[i - 2] 。

2、自顶向下

也就是从最终状态出发,如果遇到一个子问题还未求解,那么就先求解子问题。如果子问题已经求解,那么直接使用子问题的解,所以自顶向下动态规划又有一个形象生动的名字,叫做记忆化搜索,一般我们采用递归的方式进行求解。

自顶向下,我们一般用在树上面,因为我们根据父亲结点,很容易找到所有的子问题,也就是所有的子结点,而自底向上的话,我们要去统计这个结点的所有兄弟结点是否已经实现。会稍微复杂一点,而且比较难理解。

五、经典入门算法及代码实现

1、爬楼梯

LeetCode #70 : 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到n楼呢?

动态规划过程:
1、划分状态:将爬n阶楼梯的问题划分为n个子问题,由于“ 每次你可以爬 1 或 2 个台阶“,所以第i个台阶的结果可能是由第i-1个台阶爬1个台阶或者是由第i-2个台阶爬2个台阶所得
2、状态表示:将i层阶梯的结果记作f(i),i-1层阶梯则为f(i-1),i-2层阶梯为f(i-2)
3、状态转移:f(i) = f(i−1) + f(i−2) ,它意味着爬到第 xx 级台阶的方案数是爬到第 x - 1x−1 级台阶的方案数和爬到第 x - 2x−2 级台阶的方案数的和
4、确定边界:从第 00 级爬到第 00 级我们可以看作只有一种方案,即f(0)=1;从第 00 级到第 11 级也只有一种方案,即爬一级,f(1) = 1。根据转移方程得到 f(2) = 2 ,f(3) = 3 ,f(4) = 5 …

这是一个线性模型(斐波那楔数列),采用自底向上从0到n递推最后的结果,以下为代码实现:

/**
 1. @param {number} n
 2. @return {number}
 */
var climbStairs = function(n) {
    if(n<=2)
        return n;
    let arr = [1,2];
    for(let i = 2;i<n;i++){
        arr[i] = arr[i-1] + arr[i-2];
    }
    return arr[n-1];
};

LeetCode 面试题 08.01. 三步问题
三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。
以下为代码实现:

/**
 * @param {number} n
 * @return {number}
 */
var waysToStep = function(n) {
    let arr = [1,2,4],t = 1000000007;
    if(n<=3)
        return arr[n-1];
    for(let i = 3;i<n;i++){
        arr[i] = (arr[i-1] + arr[i-2] + arr[i-3] )%t;
    }
    return arr[n-1] ;
};

2、连续子数组的最大和

LeetCode 剑指 Offer 42:输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)

在这里插入图片描述
自底向上,代码实现:

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function(nums) { 
    for(let i = 1;i<nums.length;i++){
        nums[i] = Math.max(nums[i],nums[i]+nums[i-1]);
    }
    return Math.max(...nums)
};

3、使用最小花费爬楼梯

LeetCode #746: 使用最小花费爬楼梯。数组的每个索引作为一个阶梯,第 i个阶梯对应着一个非负数的体力花费值,每当你爬上一个阶梯你都要花费对应的体力花费值,然后你可以选择继续爬一个阶梯或者爬两个阶梯。您需要找到达到楼层顶部的最低花费。在开始时,你可以选择从索引为 0 或 1 的元素作为初始阶梯

/**
 * @param {number[]} cost
 * @return {number}
 */
var minCostClimbingStairs = function(cost) {
    let f1 = 0,f2 = 0;
    for(let i = cost.length-1;i>=0;i--){
        let f0 = cost[i] + Math.min(f1,f2);
        f2 = f1;
        f1 = f0;
    }
    return Math.min(f1,f2) ;
};

4、按摩师

LeetCode 面试题 17.16. 按摩师: 一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数

/**
 * @param {number[]} nums
 * @return {number}
 */
var massage = function(nums) {  
    let cur = 0,pre = 0;
    for(let i =nums.length-1;i>=0;i--){
        let t = Math.max(cur,pre+nums[i]);
        pre = cur;
        cur = t;
    } 
};

5、打家劫舍

LeetCode #198. 打家劫舍:你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

/**
 * @param {number[]} nums
 * @return {number}
 */
var rob = function(nums) {
    let cur = 0,pre = 0;
    for(let i = nums.length-1;i>=0;i--){
        let temp =  Math.max(pre+nums[i],cur);
        pre = cur;
        cur = temp;
    }
    return Math.max(cur,pre)
};

6、买卖股票的最佳时机

LeetCode #121. 买卖股票的最佳时机:给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。 如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。 注意:你不能在买入股票前卖出股票。

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if(prices.length<=1)
        return 0;
    let min = prices[0];
    let res = 0;
    for(let i = 1;i<prices.length;i++){
        if(prices[i]<min){ 
            min = prices[i];
            continue;
        }
        res = res>(prices[i]-min)?res:prices[i]-min;
    }
    return res;
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值