LeetCode刷题|算法归类|动态规划介绍及算法题合辑(持续补充)

一、算法介绍

1、介绍

  1. 动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法
  2. 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这
    些子问题的解得到原问题的解。
  3. 与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 ( 即下一个子
    阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )
  4. 动态规划可以通过填表的方式来逐步推进,得到最优解.

2、适用情况

能采用动态规划求解的问题的一般要具有3个性质:

(1)最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。

(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。

(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

3、基本步骤

动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。

初始状态→│决策1│→│决策2│→…→│决策n│→结束状态

​ 图1 动态规划决策过程示意图

(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。

(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。

(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。

(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。

实际应用中可以按以下几个简化的步骤进行设计:

(1)分析最优解的性质,并刻画其结构特征。

(2)递归的定义最优解。

(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值

(4)根据计算最优值时得到的信息,构造问题的最优解

二、LeetCode题

1、#370摆动序列

题目

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。

例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

示例 1:
输入: [1,7,4,9,2,5]
输出: 6
解释: 整个序列均为摆动序列。

示例 2:
输入: [1,17,5,10,13,15,10,5,16,8]
输出: 7
解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。

示例 3:
输入: [1,2,3,4,5,6,7,8,9]
输出: 2

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/wiggle-subsequence

代码实现1(动态规划)

为了更好地理解这一方法,用两个数组来 dp ,分别记作 upup 和 downdown 。

每当我们选择一个元素作为摆动序列的一部分时,这个元素要么是上升的,要么是下降的,这取决于前一个元素的大小。

up[i] 存的是目前为止最长的以第 i个元素结尾的上升摆动序列的长度。

类似的, down[i] 记录的是目前为止最长的以第 i 个元素结尾的下降摆动序列的长度。

我们每当找到将第 ii 个元素作为上升摆动序列的尾部的时候就更新 up[i] 。现在我们考虑如何更新 up[i],我们需要考虑前面所有的降序结尾摆动序列,也就是找到 down[j],满足j<i 且 nums[i]>nums[j] 。类似的,down[i] 也会被更新。

public class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums.length < 2)
            return nums.length;
        int[] up = new int[nums.length];//记录目前为止到第i个元素时最长上升摆动序列的长度
        int[] down = new int[nums.length];//记录目前为止到第i个元素时最长下降摆动序列的长度
        for (int i = 1; i < nums.length; i++) {
            for(int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    up[i] = Math.max(up[i],down[j] + 1);
                } else if (nums[i] < nums[j]) {
                    down[i] = Math.max(down[i],up[j] + 1);
                }
            }
        }
        return 1 + Math.max(down[nums.length - 1], up[nums.length - 1]);
    }
}

复杂度分析

  • 时间复杂度: O(n^2)。 循环内嵌套了一个循环。
  • 空间复杂度: O(n) 。 dp 需要两个同样长度的数组。

代码实现2(线性动态规划)

数组中的任何元素都对应下面三种可能状态中的一种:

上升的位置,意味着 nums[i] > nums[i - 1]nums[i]>nums[i−1]
下降的位置,意味着 nums[i] < nums[i - 1]nums[i]<nums[i−1]
相同的位置,意味着 nums[i] == nums[i - 1]nums[i]==nums[i−1]

更新的过程如下:

如果 nums[i]>nums[i−1] ,意味着这里在摆动上升,前一个数字肯定处于下降的位置。所以 up[i]=down[i−1]+1 , down[i] 与 down[i−1] 保持相同。

如果 nums[i]<nums[i−1] ,意味着这里在摆动下降,前一个数字肯定处于下降的位置。所以down[i]=up[i−1]+1 , up[i]up[i] 与 up[i-1]保持不变。

如果 nums[i]==nums[i−1] ,意味着这个元素不会改变任何东西因为它没有摆动。所以 down[i] 与 up[i] 与down[i−1] 和 up[i−1] 都分别保持不变。

最后,我们可以将 up[length−1] 和down[length−1] 中的较大值作为问题的答案,其中 length 是给定数组中的元素数目。

class Solution {
    public int wiggleMaxLength(int[] nums) {
        if(nums.length<2){
            return nums.length;
        }
        int[] up=new int[nums.length];
        int[] down=new int[nums.length];
        up[0]=down[0]=1;//只要有一个元素,那就有一个长度是摆动子序
        for(int i=1;i<nums.length;i++){
            if(nums[i]>nums[i-1]){
                //如果在这个地方是上升的,那么上次记录最长长度时肯定是在down数组上,所以up[i]是在down[i-1]的基础上+1,down[i]与down[i-1]保持不变
                up[i]=down[i-1]+1;
                down[i]=down[i-1];
            }
            else if(nums[i]<nums[i-1]){
                //如果在这个地方是下降的,那么上次记录最长长度时肯定是在up数组上,所以down[i]是在up[i-1]的基础上+1,up[i]与up[]i-1]保持不变
                down[i]=up[i-1]+1;
                up[i]=up[i-1];
            }else{
                up[i]=up[i-1];
                down[i]=down[i-1];
            }
        }
          //此时up和down最后一个元素就是累积下来的最大元素
           return Math.max(up[nums.length-1],down[nums.length-1]);
    }
}
  • 时间复杂度: O(n) 。只需要遍历数组一遍。
  • 空间复杂度: O(n) 。 dp需要两个相同长度的数组。

2、#392判断子序列

题目

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

示例 1:
s = “abc”, t = “ahbgdc”

返回 true.

示例 2:
s = “axc”, t = “ahbgdc”

返回 false.

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/is-subsequence

代码

class Solution {
    public boolean isSubsequence(String s, String t) {
         //将两个字符串都转为字符数组
        char[] chars01 = s.toCharArray();
        char[] chars02 = t.toCharArray();
        
        int n = chars01.length;
        int m = chars02.length;
        
        if (n == 0 ) return true;
        if (m == 0 ) return false;

        int temp = 0;
        int[][] dp = new int[n][m];
        for (int i = 0; i < n; i++) {
            for (int j = temp; j < m; j++) {
                if (chars01[i] != chars02[j]) continue;
                for (int k = j; k < m; k++) {
                    if ( i - 1 < 0){
                        dp[0][k] = 1;
                    }
                    else if (dp[i-1][j]==1){
                        dp[i][k] = 1;
                    }
                }
                temp = j + 1;
                break;
            }

        }
        return dp[n-1][m-1] == 1;
    }
}

持续补充对应算法题,欢迎关注

猜你喜欢

转载自blog.csdn.net/weixin_45759791/article/details/107445475