21天掌握动态规划 第一天

21天掌握动态规划

首先我觉得得弄明白『动态规划』算法到底是个什么?

上述这张图是摘自截取百度百科对动态规划的定义,我个人觉得主要是有如下几个特点

  • 和分治法类似,将待求解问题划分成若干类似原理子问题,然后从子问题入手最后得到原问题的解
  • 往往是题干出现“最大”“最小”“最好”“最多”等最优化性质的词的时候可以优先考虑动态规划思想(当然也不一定是这样,只是我个人刷题的经验而已)
  • 动态规划很多题往往是需要按照如下两个步骤
    • 确定状态表示的含义,同时确定好基本的初始值
    • 状态转移方程或者不同状态之间存在的依赖关系

以上大致确定了基本的『动态规划』思想,那么接下来就步入正题:

最近刚好看到LeetCode上有一个『21天掌握动态规划的刷题指南

1. 第一天 基础动态规划

1.1 509. 斐波那契数

斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1,F(n) = F(n - 1) + F(n - 2),其中 n > 1
给你 n ,请计算 F(n) 。

示例 1:

输入:2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1

示例 2:

输入:3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2

示例 3:

输入:4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3

提示:

0 < = n < = 30 0 <= n <= 30 0<=n<=30

  • 状态 d p [ i ] 表 示 斐 波 那 契 数 列 的 第 i 项 , i 从 0 开 始 dp[i]表示斐波那契数列的第i项,i从0开始 dp[i]ii0
  • 状态转移方程 d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] ( n > = 2 ) dp[i] = dp[i - 1] + dp[i - 2] (n >= 2) dp[i]=dp[i1]+dp[i2](n>=2),初始化 d p [ 0 ] = 0 , d p [ 1 ] = 1 dp[0] = 0, dp[1] = 1 dp[0]=0,dp[1]=1,先进行判断n的范围再初始化,因为这里要注意 n n n的取值范围是 [ 0 , 30 ] [0,30] [0,30]闭区间,所以可能只需要返回第0项。
  • 时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n)

C++代码如下:

class Solution {
public:
    int fib(int n) {
        int dp[n + 1];
        if (n <= 1) {
            return n;
        }
        dp[0] = 0, dp[1] = 1;
        for (int i = 2; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
};

Java代码如下:

class Solution {
    public int fib(int n) {
        int[] dp = new int[n + 1];
        if (n <= 1) {
            return n;
        }
        dp[0] = 0;
        dp[1] = 1;
        for (int i = 2; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
}

优化1:

可以看出,其实额外的 O ( n ) O(n) O(n)空间复杂度是可以避免的。因此 d p [ i ] dp[i] dp[i]只取决于它的前两位数,是可以不用将所有的状态都保存下来,因此我们是可以采用滚动数组的形式将其空间复杂度降为 O ( 1 ) O(1) O(1).具体代码如下:

C++代码如下:

class Solution {
public:
    int fib(int n) {
        if (n <= 1) {
            return n;
        }
        int a = 0;
        int b = 1;
        int sum = 0;
        for (int i = 2; i <= n; i++) {
            sum = a + b;
            a = b;
            b = sum;
        }
        return b;
    }
};

Java代码如下:

class Solution {
    
    
    public int fib(int n) {
    
    
        if (n <= 1) {
    
    
            return n;
        }
        int a = 0;
        int b = 1;
        int sum = 0;
        for (int i = 2; i <= n; i++) {
    
    
            sum = a + b;
            a = b;
            b = sum;
        }
        return b;
    }
}

优化2

对于这个题的公式而言,很容易联想到高中所学的求通项公式的知识。

查阅资料,找到了一个比较符合当时中学阶段求通项公式的资料

可以看到斐波那契数列通项公式为:

由此可以用写出空间复杂度为 O ( 1 ) O(1) O(1),时间复杂度也为 O ( 1 ) O(1) O(1)的算法啦。

C++代码:

class Solution {
public:
    int fib(int n) {
        double a = (1 + sqrt(5)) / 2;
        double qa = pow(a,n);
        double b = (1 - sqrt(5)) / 2;
        double qb = pow(b,n);
        double res = ( 1 / sqrt(5) * (qa - qb));
        return (int) res;
    }
};

Java代码:

class Solution {
    
    
    public int fib(int n) {
    
    
        double a = (1 + Math.sqrt(5)) / 2;
        double qa = Math.pow(a,n);
        double b = (1 - Math.sqrt(5)) / 2;
        double qb = Math.pow(b,n);
        double res = ( (1 / Math.sqrt(5)) * (qa - qb) % 1000000007);
        return (int)res;
    }
}

1.2 70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

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

注意:给定 n 是一个正整数。

示例 1:

输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.  1 阶 + 1 阶
2.  2 阶

示例 2:

输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1.  1 阶 + 1 阶 + 1 阶
2.  1 阶 + 2 阶
3.  2 阶 + 1 阶
  • 定义状态 d p [ i ] dp[i] dp[i]表示爬到第 i i i阶用的方法数,那么初始值 d p [ 1 ] = 1 , d p [ 2 ] = 2 dp[1] = 1,dp[2] = 2 dp[1]=1,dp[2]=2
  • 由于每次只可以爬1或者2个台阶,那么当 i > = 3 i >= 3 i>=3时爬到第 i i i阶只能是从第 i − 2 i - 2 i2阶或者第 i − 1 i - 1 i1阶经过一次操作过来的。当已经爬到第 i − 2 i - 2 i2阶有 d p [ i − 2 ] dp[i - 2] dp[i2]种方法,那么到第 i i i阶就可以是从第 i − 2 i - 2 i2阶一次跨两步或者第 i − 2 i - 2 i2阶跨两次,每次跨一步(这种情况可以归结到最终还是从第 i − 1 i - 1 i1阶跨一步到第 i i i阶的情况下)。假设当爬到第 i − 1 i - 1 i1阶有 d p [ i − 1 ] dp[i - 1] dp[i1]种方法时,那么爬到第 i i i阶可以是第 i − 1 i - 1 i1阶再跨一步,因此 d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] dp[i] = dp[i - 1] + dp[i - 2] dp[i]=dp[i1]+dp[i2] 可以发现当 i > = 3 i >= 3 i>=3的时候, d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] dp[i] = dp[i - 1] + dp[i - 2] dp[i]=dp[i1]+dp[i2],当前阶所用的方法数是前两阶梯所用的方法数总和。

Java代码如下:

class Solution {
    public int climbStairs(int n) {
        if (n <= 2) {
            return n;
        }
        int[] dp = new int[n + 1];
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
}

可以发现和509题斐波那契数很像,只是初始值不一样,那么用滚动数组优化空间复杂度之后如下:

C++代码如下:

class Solution {
public:
    int climbStairs(int n) {
        long a = 1, b = 2;
        long sum = 0;
        if (n <= 2) {
            return n;
        }
        for (int i = 3; i <= n; i++) {
            sum = a + b;
            a = b;
            b = sum;
        }
        return (int)b;
    }
};

Java代码如下:

class Solution {
    public int climbStairs(int n) {
        if (n <= 2) {
            return n;
        }
        long a = 1, b = 2;
        long sum = 0;
        for (int i = 3; i <= n; i++) {
            sum = a + b;
            a = b;
            b = sum;
        }
        return (int) b;
    }
}

同样的,这题也可以给出通项公式的做法,具体的代码可以参考官方题解的。

1.3 746. 使用最小花费爬楼梯

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。

每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。

请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

示例 1:

输入:cost = [10, 15, 20]
输出:15
解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。

示例 2:

输入:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出:6
解释:最低花费方式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,一共花费 6 。

这个题和上述两题『509.斐波那契数列』、『70.爬楼梯』很相似,不同点在于爬上一个阶梯需要花费对应的体力值,也就是需要多一个对比给定的 c o s t cost cost数组的值的步骤。

  • d p [ i ] dp[i] dp[i]定义为到达下标 i i i阶所需要的最低花费,题目给定的是“可以选择从下标为 0 或 1 的元素作为初始阶梯”,因此初始化 d p [ 0 ] = 0 , d p [ 1 ] = 0 dp[0] = 0,dp[1] = 0 dp[0]=0,dp[1]=0
  • 到达下标为 i i i阶只能是第 i − 1 i - 1 i1或者第 i − 2 i - 2 i2阶选择跳一步或者两步这样来的,因此 d p [ i ] = m i n ( d p [ i − 1 ] + c o s t [ i − 1 ] , d p [ i − 2 ] + c o s t [ i − 2 ] ) dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]) dp[i]=min(dp[i1]+cost[i1],dp[i2]+cost[i2])

Java代码如下:

class Solution {
    
    
    public int minCostClimbingStairs(int[] cost) {
    
    
        int n = cost.length;
        int[] dp = new int[n + 1];
        dp[0] = 0;
        dp[1] = 0;
        for (int i = 2; i <=n ; i++) {
    
    
            dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
        }
        return dp[n];
    }
}

滚动数组优化后的代码展示:

class Solution {
    
    
    public int minCostClimbingStairs(int[] cost) {
    
    
        int n = cost.length;
        int a = 0, b = 0;
        int sum = 0;
        for (int i = 2; i <=n ; i++) {
    
    
            sum = Math.min(a + cost[i - 1], b + cost[i - 2]);
            b = a;
            a = sum;
        }
        return a;
    }
}

猜你喜欢

转载自blog.csdn.net/qq_41688840/article/details/118425073