剑指offer-动态规划入门篇

动态规划基本思想

动态规划算法

通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。

动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。

与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。

我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式.

简单题

文章题目来源于LeeCode

1.斐波那契数列

斐波那契的思想在动态规划中很常见,部分经典题目的都是斐波那契数列应用的延伸,往后可以看见这类题目的基本构造都是一样的。

写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:

F(0) = 0,   F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.

斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。

输入:n = 2 输出:1

----------------------------------分析----------------------------------------------

斐波那契数的边界条件是 F(0)=0和F(1)=1。当 n>1n>1n>1 时,每一项的和都等于前两项的和,因此有如下递推关系:

F(n)=F(n−1)+F(n−2)  

而关于递推,可以用递归或者循环来求解,这里值得注意的是,只需要F(n),F(n-1),F(n-2)三个值来进行运算,所以可以仅使用三个变量来储存,也就是滚动数组

class Solution {
    public int fib(int n) {
         int a=0;
         int b=1;
         int c=0;
         if(n==0) return a;
         else if(n==1) return b;
         for(int i=n-1;i>0;i--){
            c=a+b
            a=b;
            b=c;
         }
         return c;
    }
}

这类题目需要注意循环开始的条件。

2.青蛙跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

输入:n = 2 输出:2

----------------------------------------------分析---------------------------------------------------

这里需要注意的是,青蛙跳台阶的选择是有顺序的,比如先跳一步,再跳两步,与先跳两步再跳一步不同。

如果开始思考青蛙第一步跳一步还是两步就容易陷入死胡同,可以反方向思考,

假设青蛙现在已经跳上了第n个台阶,它回头思考自己是怎么上来了,是跳两步上来的还是跳一步呢,如果是跳一步上来的,那么它跳之前就在n-1的台阶上,同理,跳之前也可能在n-2的台阶上,又结合一开始所说的,顺序不同不能算一步,最后一步不同,青蛙的这两次跳法和之前的都是完全独立的。

也就是二叉树

很明显,青蛙的第n的台阶等于n-1的台阶加上n-2的台阶,这是一个斐波那契数列

同上面一个解法,但需要注意的是,a,b也就是滚动数组一开始的位置决定循环开始的位置

class Solution {
    public int numWays(int n) {
        if(n==0) return 1;
        int count=1;
        int a=1;
        int b=2;
        int c=1;
        if(n==2) return 2;
        for(int i=n-2;i>0;i--){
            c=a+b
            a=b;
            b=c;
        }
        return c;
    }
}

3.  连续子数组的最大和

输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。

输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6 
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

----------------------------------------分析----------------------------------------------------------------

第一次遇到这种题可能会陷入一个思想误区,比如遍历数组来看的话,这个数虽然是负数,加上它数组的值会变小,但它后面有一个比较大的正值,加上正值完全可以抵消负值,如何同时看两个值甚至更多的值来进行取舍,前面后面的边界到底应该在哪?

这种瞻前顾后左右为难的时候,就可以想想分解题目,用动态规划或者分治算法来代入。

我们可以看见,答案只需要最大的和,而不是连续子数组,那我们可不可以试试用一个变量存储最大值,然后遍历所有子数组求出来,显然是不行的,那我们可以延伸一下。

所有的子数组,而大部分子数组有许许多多重叠的部分,比如[1,2]和[1,2,3]这两个子数组,这个时候可以发现,后者仅比前者多一个3,而3是正值,所以后一个子数组肯定大于第一个。

在结合动态规划思想,把3看成i,我们用f(i) 代表以第 i 个数   结尾  的连续子数组的最大和

而根据前面讨论的,f(i)的值可以从f(i-1)得出,比如[1,2]与[1,2,3]

结论:f(i)= max{  f(i−1) + nums[i]  ,  nums[i]  }

num[i]的意义就是 nums[i]+0,加上前面的子数组反而小于自身,直接抛弃。

然后就是遍历数组,求出每个f(i), 这又包含斐波那契数列的方法,最后得出最大的f(i)

class Solution {
    public int maxSubArray(int[] nums) {
        int pre = 0, maxAns = nums[0];
        for (int x : nums) {
            pre = Math.max(pre + x, 0+x);
            maxAns = Math.max(maxAns, pre);
        }
        return maxAns;
    }
}

困难题

1.股票的最大利润

假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

-----------------------------------------------------分析-----------------------------------------------------

思考方式与上一题类似

首先暴力法是可以做出来的,就是非常麻烦,比如遍历每个值,然后再遍历这个值前面最小的值,比较每一个差值得出结论,遍历的逐步递进可以解决时间的问题。

从动态规划的角度,可以对暴力法进行修改,不需要遍历就可以得到前面所有值的最小值,不就非常完美了,那就在遍历整个数组的时候定义一个变量专门来储存最小值,然后在定义一个最差值最大值,用来比较差值。

重点就是如何完美和优雅地赋值和使用者两个值

public class Solution {
    public int maxProfit(int prices[]) {
        int minprice = Integer.MAX_VALUE;
        int maxprofit = 0;
        for (int i = 0; i < prices.length; i++) {
            if (prices[i] < minprice) {
                minprice = prices[i];
            } else if (prices[i] - minprice > maxprofit) {
                maxprofit = prices[i] - minprice;
            }
        }
        return maxprofit;
    }
}

2.礼物的最大价值

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

输入: 
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物

-----------------------------分析----------------------------------------------------------

首先可以确定我们必须要遍历完整个数组才能得出最大值,那我们从左上角开始思考的时候,走哪一步都不知道以后的路,不好做决定,当然遍历整个数组用二叉树存储也能解决问题

所以,我们从右下角开始思考,可以看出,到达右下角只有两条路,选哪一条路就看哪一条路的礼物多,但是看不见以前所有的路的话,不就和从左上角一样的困境吗?

我们可以根据前文提到的思想展开思考,每一步都与前面所有的步有关,而每一步都可以由前一步得出,这不就是斐波那契数列的方法,这题可以看成二维的连续子数组的和

先推导方程,要考虑边界问题

当 i=0 时,为起始元素;
当 i=0且 j≠0 时,为矩阵第一行元素,只可从左边到达;
当 i≠0且 j=0时,为矩阵第一列元素,只可从上边到达;
当 i≠0且 j≠0 时,可从左边或上边到达;

 然后就是代码的问题了

class Solution {
    public int maxValue(int[][] grid) {
        int m=grid.length;
        int n=grid[0].length;
        int max=grid[0][0];
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(i==0 && j==0){
                    continue;
                }else if(i==0){
                    grid[0][j]=grid[0][j-1]+grid[0][j];
                }
                else if(j==0){
                    grid[i][0]=grid[i-1][0]+grid[i][0];
                }else{
                    grid[i][j]=Math.max(grid[i-1][j],grid[i][j-1])+grid[i][j];
                }
            }
        }
    return grid[m-1][n-1];
    }
}

 3.把数字翻译成字符串

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法 

输入: 12258

输出: 5
解释: 12258,有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"

-------------------------------------分析------------------------------------------------------------------------

很明显,又是选一个还是两个的问题,也就是加强版的青蛙跳台阶,不同的就是如果大于25的“两个台阶”需要取消,同样用滚动数组可以节省空间。

class Solution {
    public int translateNum(int num) {
        String src = String.valueOf(num);
        int p = 0, q = 0, r = 1;
        for (int i = 0; i < src.length(); ++i) {
            p = q; 
            q = r; 
            r = 0;
            r += q;
            if (i == 0) {
                continue;
            }
            String pre = src.substring(i - 1, i + 1);
            if (pre.compareTo("25") <= 0 && pre.compareTo("10") >= 0) {
                r += p;
            }
        }
        return r;
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值