深圳幻海软件技术有限公司 欢迎您!

在动态规划的海洋中遨游(二)

2023-04-27

前言:\textcolor{Green}{前言:}前言:💞本专栏用于本人刷算法的过程。主要包含刷题中的感受以及知识点缺陷。对于学习者来说可以作为参考。目前更新的算法内容会比较多,很多都是通过刷题来进行知识点的总结,其中部分来源于网络总结,如有侵权请联系。💞前几天参加了字节的青训营笔试,感受了一下

前言: \textcolor{Green}{前言:} 前言:

💞本专栏用于本人刷算法的过程。主要包含刷题中的感受以及知识点缺陷。对于学习者来说可以作为参考。
目前更新的算法内容会比较多,很多都是通过刷题来进行知识点的总结,其中部分来源于网络总结,如有侵权请联系。💞

前几天参加了字节的青训营笔试,感受了一下氛围,可以明显的感受到我可以通过前段时间的题目训练让我在笔试中不至于那么尴尬,相信大家也可以通过训练得到很大的提升,加油奥里给

文章存在时候,会随着博主的学习过程进行不间断更新,只增不减,请放心使用

目前作者正在刷题,如果想一起交流的可以添加联系方式互相学习~~欢迎大家

动态规划

  • 一、算法介绍
    • 原理
    • 适用的情况
    • 做题步骤
  • 二、算法实例
    • 1. 把数字翻译成字符串
    • 2. 兑换零钱(一)
    • 3. 最长上升子序列(一)
    • 4. 连续子数组的最大和
    • 5. 最长回文子串

一、算法介绍

原理

思想:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法。按顺序求解子阶段,前面子问题的解为后面子问题的求解提供信息。

如果某一问题有很多重叠子问题,使用动态规划是最有效的。

动态规划中每一个状态一定是由上一个状态推导出来。

动态规划算法的基本思想:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。

适用的情况

  1. 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,称该问题具有最优子结构,即满足最优化原理。
  2. 没有后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说:某状态以后的过程不会影响以前的状态,只与当前状态有关。
  3. 有重叠子问题:子问题之间是不独立的,一个子问题在下一个近阶段可能被多次遇到。(这条性质不是动态规划适用的必要条件但是具备这条性质那么动态规划相对于其他算法就具备一定的优势)。

做题步骤

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

二、算法实例

1. 把数字翻译成字符串

题目来源:牛客网
等级:中等 \textcolor{OrangeRed}{等级:中等} 等级:中等
涉及方法:动态规划

👉题目描述

有一种将字母编码成数字的方式: ′ a ′ − > 1 , ′ b − > 2 ′ , . . . , ′ z − > 2 6 ′ 'a'->1, 'b->2', ... , 'z->26' a>1,b>2,...,z>26

现在给一串数字,返回有多少种可能的译码结果

数据范围:字符串长度满足 0 < n ≤ 90 0 < n \le 90 0<n90
进阶:空间复杂度 ' O ( n ) ′ O(n)' O(n),时间复杂度 ' O ( n ) ′ O(n)' O(n)

示例1

输入:"12"
返回值:2
说明:2种可能的译码结果(”ab” 或”l”)  
  • 1
  • 2
  • 3

示例2

输入:"31717126241541717"
返回值:192
说明:192种可能的译码结果  
  • 1
  • 2
  • 3

👉代码编写

👉👉方法1

对于普通数组1-9,译码方式只有一种,但是对于11-19,21-26,译码方式有可选择的两种方案,因此我们使用动态规划将两种方案累计。

  1. 用辅助数组dp表示前i个数的译码方法有多少种。
  2. 对于一个数,我们可以直接译码它,也可以将其与前面的1或者2组合起来译码:如果直接译码,则 d p [ i ] = d p [ i − 1 ] dp[i]=dp[i−1] dp[i]=dp[i1];如果组合译码,则 d p [ i ] = d p [ i − 2 ] dp[i]=dp[i−2] dp[i]=dp[i2]
  3. 对于只有一种译码方式的,选上种 d p [ i − 1 ] dp[i−1] dp[i1]即可,对于满足两种译码方式(10,20不能)则是 d p [ i − 1 ] + d p [ i − 2 ] dp[i−1]+dp[i−2] dp[i1]+dp[i2]
  4. 依次相加,最后的 d p [ l e n g t h ] dp[length] dp[length]即为所求答案。
import java.util.*;


public class Solution {
    /**
     * 解码
     * @param nums string字符串 数字串
     * @return int整型
     */
    public int solve (String nums) {
        // write code here
        if (nums.length() == 1 && nums.charAt(nums.length() - 1) == '0'){
            return 0;
        }
        if (nums.length() == 1){
            return nums.length();
        }
        if (nums.charAt(nums.length() - 1) == '0' && 
           nums.charAt(nums.length() - 2) != '1' && nums.charAt(nums.length() - 2) != '2') {
            return 0;
        }
        int dp[] = new int[nums.length() + 1];
        dp[0] = 1;
        dp[1] = 1;
        for (int i = 2; i <= nums.length(); ++i) {
            if ((nums.charAt(i - 2) == '1' && nums.charAt(i - 1) <= '9' && nums.charAt(i - 1) > '0')
                || (nums.charAt(i - 2) == '2' && nums.charAt(i - 1) <= '6' && nums.charAt(i - 1) > '0')) {
                    dp[i] = dp[i - 1] + dp[i - 2];
            } else {
                dp[i] = dp[i - 1];
            }
        }
        return dp[nums.length()];
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

👉👉方法2

👉 注意点

👉 可以提炼的知识

2. 兑换零钱(一)

题目来源:牛客网
等级:中等 \textcolor{OrangeRed}{等级:中等} 等级:中等
涉及方法:动态规划

👉题目描述

给定数组arr,arr中所有的值都为正整数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个 a i m aim aim,代表要找的钱数,求组成 a i m aim aim的最少货币数。
如果无解,请返回-1.

数据范围:数组大小满足 0 ≤ n ≤ 10000 0 \le n \le 10000 0n10000, 数组中每个数字都满足 0 < v a l ≤ 10000 0 < val \le 10000 0<val10000 0 ≤ a i m ≤ 5000 0 \le aim \le 5000 0aim5000

要求:时间复杂度 O ( n × a i m ) O(n \times aim) O(n×aim) ,空间复杂度 O ( a i m ) O(aim) O(aim)

示例1

输入:[5,2,3],20
返回值:4
  • 1
  • 2

示例2

输入:[5,2,3],0
返回值:0
  • 1
  • 2

示例3

输入:[3,5],2
返回值:-1
  • 1
  • 2

备注:
0 ≤ n ≤ 10   000 0 \leq n \leq 10\,000 0n10000
0 ≤ a i m ≤ 5   000 0 \leq aim \leq 5\,000 0aim5000

👉代码编写

  1. 可以用 d p [ i ] dp[i] dp[i]表示要凑出i元钱需要最小的货币数。
  2. 一开始都设置为最大值 a i m + 1 aim+1 aim+1,因此货币最小1元,即货币数不会超过 a i m aim aim.
  3. 初始化 d p [ 0 ] = 0 dp[0]=0 dp[0]=0
  4. 后续遍历1元到aim元,枚举每种面值的货币都可能组成的情况,取每次的最小值即可,转移方程为 d p [ i ] = m i n ( d p [ i ] , d p [ i − a r r [ j ] ] + 1 ) dp[i]=min(dp[i],dp[i−arr[j]]+1) dp[i]=min(dp[i],dp[iarr[j]]+1).
  5. 最后比较 d p [ a i m ] dp[aim] dp[aim]的值是否超过aim,如果超过说明无解,否则返回即可。

👉👉方法1

import java.util.*;


public class Solution {
    /**
     * 最少货币数
     * @param arr int整型一维数组 the array
     * @param aim int整型 the target
     * @return int整型
     */
    public int minMoney (int[] arr, int aim) {
        // write code here
        if (aim < 1) {
            return 0;
        }
        int[] dp = new int[aim + 1];
        Arrays.fill(dp, aim + 1);
        dp[0] = 0;
        for (int i = 1; i <= aim; ++i) {
            for (int j = 0; j < arr.length; ++j) {
                if (arr[j] <= i) {
                    dp[i] = Math.min(dp[i], dp[i - arr[j]] + 1);
                }
            }
        }
        return dp[aim] > aim ? -1 : dp[aim];
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

👉 注意点

搞清楚这个最小值是为什么就明白这道题了。
首先要遍历 1 − a i m 1-aim 1aim元,每种面值都要进行枚举,如果面值没有超过要凑的钱数( a r r [ j ] < = i arr[j] <= i arr[j]<=i)才可以对该值进行维护( d p [ i ] = M a t h . m i n ( d p [ i ] , d p [ i − a r r [ j ] ] + 1 ) dp[i] = Math.min(dp[i], dp[i - arr[j]] + 1) dp[i]=Math.min(dp[i],dp[iarr[j]]+1))。

d p [ i − a r r [ j ] ] + 1 ) dp[i - arr[j]] + 1) dp[iarr[j]]+1) 这个的意思是 获得缺少当前面值的张数,再加上当前面值的张数 1 1 1 最后得出的结果。

👉 可以提炼的知识

3. 最长上升子序列(一)

题目来源:牛客网
等级:中等 \textcolor{OrangeRed}{等级:中等} 等级:中等
涉及方法:动态规划

👉题目描述

给定一个长度为 n 的数组 arr,求它的最长严格上升子序列的长度。
所谓子序列,指一个数组删掉一些数(也可以不删)之后,形成的新数组。例如 [1,5,3,7,3] 数组,其子序列有:[1,3,3]、[7] 等。但 [1,6]、[1,3,5] 则不是它的子序列。
我们定义一个序列是 严格上升 的,当且仅当该序列不存在两个下标 i i i j j j 满足 i < j i<j i<j a r r i ≥ a r r j arr_i \geq arr_j arriarrj
数据范围: 0 ≤ n ≤ 1000 0\leq n \leq 1000 0n1000
要求:时间复杂度 O ( n 2 ) O(n^2) O(n2), 空间复杂度 O ( n ) O(n) O(n)

示例1

输入:[6,3,1,5,2,3,7]
返回值:4
说明:该数组最长上升子序列为 [1,2,3,7] ,长度为4
  • 1
  • 2
  • 3

👉代码编写

👉👉方法1

import java.util.*;


public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 给定数组的最长严格上升子序列的长度。
     * @param arr int整型一维数组 给定的数组
     * @return int整型
     */
    public int LIS (int[] arr) {
        // write code here
        int len = arr.length;
        if (len == 0) {
            return 0;
        }
        int[] dp = new int[len];
        Arrays.fill(dp, 1);
        int max = 1;
        for (int i = 1; i < len; ++i) {
            for (int j = i - 1; j >= 0; j--) {
                if (arr[i] >= arr[j]) {
                    dp[i] = Math.max(dp[j] + 1, dp[i]);
                }
            }
            max = Math.max(dp[i], max);
        }
        return max;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

👉 注意点

这道题就非常的有意思,我刚开始也没有想明白,后来看了下大佬的做法,原谅我第一次。

第一层循环我们遍历数组的时候从第二个数开始遍历。
第二成循环倒序遍历,从 i − 1 i-1 i1开始 到 0 0 0结束,每次都将判断序列以 i i i结尾的元素,向前查找,比 a r r [ i ] arr[i] arr[i]小的元素,得到它们的dp数组记录上升序列的个数 + 1 + 1 +1,与 d p [ i ] dp[i] dp[i] 取最大值即为当前i索引位置 最大上升序列
max的值是当前得到的最大上升子序列与原来的值进行比较。得到我们的最后结果。

👉 可以提炼的知识

4. 连续子数组的最大和

题目来源:牛客网
等级:简单 \textcolor{OrangeRed}{等级:简单} 等级:简单
涉及方法:动态规划

👉题目描述

输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组,子数组最小长度为1。求所有子数组的和的最大值。
数据范围: 1 < = n < = 2 × 1 0 5 1 <= n <= 2\times10^5 1<=n<=2×105

− 100 < = a [ i ] < = 100 -100 <= a[i] <= 100 100<=a[i]<=100

要求:时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)
进阶:时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

示例1

输入:[1,-2,3,10,-4,7,2,-5]
返回值:18
说明:经分析可知,输入数组的子数组[3,10,-4,7,2]可以求得最大和为18        
  • 1
  • 2
  • 3

示例2

输入:[2]
返回值:2
  • 1
  • 2

示例3

输入:[-10]
返回值:-10
  • 1
  • 2

👉代码编写

👉👉方法1

public class Solution {
    public int FindGreatestSumOfSubArray(int[] array) {
        int len = array.length;
        int[] dp = new int[len];
        dp[0] = array[0];
        int max = array[0];
        for (int i = 1; i < len; ++i) {
            dp[i] = Math.max(dp[i - 1] + array[i], array[i]);
            max = Math.max(dp[i], max);
        }
        return max;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

时间复杂度O(n),空间复杂度O(n)

👉👉方法2

优化上一个动态规划

public int FindGreatestSumOfSubArray(int[] array) {
        int sum = 0;
        int max = array[0];
        for(int i = 0; i < array.length; i++){
            // 优化动态规划,确定sum的最大值
            sum = Math.max(sum + array[i], array[i]);
            // 每次比较,保存出现的最大值
            max = Math.max(max, sum);
        }
        return max;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

最终我们的时间复杂度O(n),空间复杂度O(1)

这里我们也是用了动态规划但是我们的空间复杂度降低了。仅使用一个 s u m sum sum m a x max max 来进行保存值即可。

👉 注意点

转移方程 d p [ i ] = M a t h . m a x ( d p [ i − 1 ] + a r r a y [ i ] , a r r a y [ i ] ) ; dp[i] = Math.max(dp[i - 1] + array[i], array[i]); dp[i]=Math.max(dp[i1]+array[i],array[i]); 这个是上一个值与当前值相加与当前值进行比较从而得到目前的最大值。
max的值获得是通过我们原来保存的值上面得到的当前值进行比较。最终得到我们需要的值。

👉 可以提炼的知识

5. 最长回文子串

题目来源:牛客网
等级:中等 \textcolor{OrangeRed}{等级:中等} 等级:中等
涉及方法:动态规划

👉题目描述

对于长度为n的一个字符串A(仅包含数字,大小写英文字母),请设计一个高效算法,计算其中最长回文子串的长度。

数据范围: 1 ≤ n ≤ 1000 1 \le n \le 1000 1n1000
要求:空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( n 2 ) O(n^2) O(n2)
进阶: 空间复杂度 O ( n ) O(n) O(n),时间复杂度 O ( n ) O(n) O(n)

示例1

输入:"ababc"
返回值:3
说明:最长的回文子串为"aba""bab",长度都为3
  • 1
  • 2
  • 3

示例2

输入:"abbba"
返回值:5
  • 1
  • 2

示例3

输入:"b"
返回值:1
  • 1
  • 2

👉代码编写

👉👉方法1

import java.util.*;


public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param A string字符串 
     * @return int整型
     */
    public int getLongestPalindrome (String A) {
        // write code here
        int len = A.length();
        if (len == 1) {
            return 1;
        }
        boolean[][] dp = new boolean[len][len];
        for (int i = 0; i < len; ++i) {
            dp[i][i] = true;
        }
        int maxLen = 1;
        for (int j = 1; j < len; ++j) {
            for (int i = 0; i < j; ++i) {
                if (A.charAt(j) == A.charAt(i)) {
                    if (j - i == 1) {
                        dp[j][i] = true;
                    }else {
                        dp[j][i] = dp[j - 1][i + 1];
                    }
                    if (dp[j][i]) {
                        maxLen = Math.max(j - i + 1, maxLen);
                    }
                }
            }
        }
        return maxLen;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

空间复杂度 O ( n 2 ) O(n^2) O(n2) 时间复杂度 O ( n 2 ) O(n^2) O(n2)

👉 注意点

这道题也非常有趣,我们也会经常见过,后面我会将类似的也会弄上来让大家学习。

d p [ j ] [ i ] dp[j][i] dp[j][i]的含义为: 从 i − j i - j ij 的子串是否为回文子串.

  1. 初始化的dp数组含义为 从 i − i i - i ii 的 位置的 dp 为true, 也就是单个字符都可以是回文子串
  2. 第一层循环代表的是右边界,我们使用了dp数组的一半,另一半我们是不使用的。
  3. 第二层循环代表的是左边界, i i i每次从0开始,递增到 j − 1 j - 1 j1的位置,依次和 j j j进行比较,如果两者的索引字符是相等的,并且两个字符是挨着的,直接设置为true即可;如果不是挨着的我们就需要看前面是否是回文字符串,转移方程为 d p [ j ] [ i ] = d p [ j − 1 ] [ i + 1 ] dp[j][i] = dp[j - 1][i + 1] dp[j][i]=dp[j1][i+1]
  4. 最后我们会看如果 d p [ j ] [ i ] dp[j][i] dp[j][i]为true,那么我们获得最大的长度也就是maxLen;
文章知识点与官方知识档案匹配,可进一步学习相关知识
算法技能树首页概览45136 人正在系统学习中
有问题直接来私聊
微信名片