算法沉淀——动态规划之子数组、子串系列(下)(leetcode真题剖析)

算法沉淀------动态规划之子数组、子串系列

01.等差数列划分

题目链接:https://leetcode.cn/problems/arithmetic-slices/

如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。

  • 例如,[1,3,5,7,9][7,7,7,7][3,-1,-5,-9] 都是等差数列。

给你一个整数数组 nums ,返回数组 nums 中所有为等差数组的 子数组 个数。

子数组 是数组中的一个连续序列。

示例 1:

输入:nums = [1,2,3,4]
输出:3
解释:nums 中有三个子等差数组:[1, 2, 3]、[2, 3, 4] 和 [1,2,3,4] 自身。

示例 2:

输入:nums = [1]
输出:0

提示:

  • 1 <= nums.length <= 5000
  • -1000 <= nums[i] <= 1000

思路

1. 状态表达: 定义动态规划数组 dp,其中 dp[i] 表示必须以 i 位置的元素为结尾的等差数列有多少种。

2. 状态转移方程: 对于 dp[i] 位置的元素 nums[i],分两种情况讨论:

  • 如果 nums[i - 2] + nums[i] != 2 * nums[i - 1],表示 nums[i - 2], nums[i - 1], nums[i] 三个元素不能构成等差数列。此时,以 nums[i] 为结尾的等差数列不存在,令 dp[i] = 0
  • 如果 nums[i - 2] + nums[i] == 2 * nums[i - 1],表示 nums[i - 2], nums[i - 1], nums[i] 三个元素可以构成等差数列。此时,以 nums[i - 1] 为结尾的所有等差数列后面填上一个 nums[i] 也是一个等差数列,令 dp[i] = 1 + dp[i - 1]

3. 初始化: 由于需要用到前两个位置的元素,但前两个位置的元素又无法构成等差数列,因此初始化 dp[0] = dp[1] = 0

4. 填表顺序: 从左往右遍历数组。

5. 返回值: 返回整个 dp 数组中的元素之和,即为所有等差数列的个数。

代码

cpp 复制代码
class Solution {
public:
    int numberOfArithmeticSlices(vector<int>& nums) {
        int n=nums.size();
        vector<int> dp(n);
        int sum=0;

        for(int i=2;i<n;i++){
            dp[i]=nums[i]-nums[i-1]==nums[i-1]-nums[i-2]?dp[i-1]+1:0;
            sum+=dp[i];
        }

        return sum;
    }
};

02.最长湍流子数组

题目链接:https://leetcode.cn/problems/longest-turbulent-subarray/

给定一个整数数组 arr ,返回 arr最大湍流子数组的长度

如果比较符号在子数组中的每个相邻元素对之间翻转,则该子数组是 湍流子数组

更正式地来说,当 arr 的子数组 A[i], A[i+1], ..., A[j] 满足仅满足下列条件时,我们称其为湍流子数组

  • i <= k < j
    • k 为奇数时, A[k] > A[k+1],且
    • k 为偶数时,A[k] < A[k+1]
  • 或若i <= k < j
    • k 为偶数时,A[k] > A[k+1] ,且
    • k 为奇数时, A[k] < A[k+1]

示例 1:

输入:arr = [9,4,2,10,7,8,8,1,9]
输出:5
解释:arr[1] > arr[2] < arr[3] > arr[4] < arr[5]

示例 2:

输入:arr = [4,8,12,16]
输出:2

示例 3:

输入:arr = [100]
输出:1

提示:

  • 1 <= arr.length <= 4 * 104
  • 0 <= arr[i] <= 109

思路

1. 状态表达: 定义两个动态规划数组 fg,其中 f[i] 表示以 i 位置元素为结尾的所有子数组中,最后呈现「上升状态」下的最长湍流数组的长度,而 g[i] 表示以 i 位置元素为结尾的所有子数组中,最后呈现「下降状态」下的最长湍流数组的长度。

2. 状态转移方程: 对于 arr[i] 位置的元素,有以下三种情况:

  • 如果 arr[i] > arr[i - 1],说明 i 位置的元素比 i - 1 位置的元素大,接下来应该去找 i - 1 位置结尾,并且 i - 1 位置元素比前一个元素小的序列,那就是 g[i - 1]。更新 f[i] 位置的值: f[i] = g[i - 1] + 1
  • 如果 arr[i] < arr[i - 1],说明 i 位置的元素比 i - 1 位置的元素小,接下来应该去找 i - 1 位置结尾,并且 i - 1 位置元素比前一个元素大的序列,那就是 f[i - 1]。更新 g[i] 位置的值: g[i] = f[i - 1] + 1
  • 如果 arr[i] == arr[i - 1],不构成湍流数组。

3. 初始化: 所有的元素「单独」都能构成一个湍流数组,因此可以将 fg 数组内所有元素初始化为 1。由于用到前面的状态,因此循环的时候从第二个位置开始即可。

4. 填表顺序: 从左往右遍历数组,同时填充 fg 两个数组。

5. 返回值: 返回两个数组中的最大值,即为湍流数组的最大长度。

代码

cpp 复制代码
class Solution {
public:
    int maxTurbulenceSize(vector<int>& arr) {
        int n=arr.size();
        vector<int> f(n,1);
        vector<int> g(n,1);
        int ret=1;

        for(int i=1;i<n;i++){
            if(arr[i-1]<arr[i]) f[i]=g[i-1]+1;
            if(arr[i-1]>arr[i]) g[i]=f[i-1]+1;
            ret=max(ret,max(g[i],f[i]));
        }
        return ret;
    }
};

03.单词拆分

题目链接:https://leetcode.cn/problems/word-break/

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

**注意:**不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。

示例 2:

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
     注意,你可以重复使用字典中的单词。

示例 3:

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

提示:

  • 1 <= s.length <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • swordDict[i] 仅由小写英文字母组成
  • wordDict 中的所有字符串 互不相同

思路

1. 状态表达:

定义动态规划数组 dp,其中 dp[i] 表示 [0, i] 区间内的字符串,能否被字典中的单词拼接而成。

2. 状态转移方程:

对于 dp[i],为了确定当前的字符串能否由字典中的单词构成,根据最后一个单词的起始位置 j,可以将其分解为前后两部分:

前面一部分 [0, j - 1] 区间的字符串;

后面一部分 [j, i] 区间的字符串。

其中前面部分我们可以在 dp[j - 1] 中找到答案,后面部分的子串可以在字典里找到。因此,我们得出一个结论:当我们在从 0 ~ i 枚举 j 的时候,只要 dp[j - 1] = true 并且后面部分的子串 s.substr(j, i - j + 1) 能够在字典中找到,那么 dp[i] = true

3. 初始化:

在最前面加上一个「辅助结点」,帮助初始化。可以将字符串前面加上一个占位符 s = ' ' + s,这样就没有下标的映射关系的问题了,同时还能处理「空串」的情况。设置 dp[0] = true,表示空串能够拼接而成。

4. 填表顺序:

从左往右遍历字符串。

5. 返回值:

返回 dp[n] 位置的布尔值,表示整个字符串能否被字典中的单词拼接而成。

代码

cpp 复制代码
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> hash;
        for(string& s:wordDict) hash.insert(s);

        int n=s.size();
        vector<bool> dp(n+1);
        dp[0]=true;
        s=' '+s;
        for(int i=1;i<=n;i++){
            for(int j=i;j>=1;j--){
                if(dp[j-1]&&hash.count(s.substr(j,i-j+1))){
                    dp[i]=true;
                    break;
                }
            }
        }
        return dp[n];
    }
};

04.环绕字符串中唯一的子字符串

题目链接:https://leetcode.cn/problems/unique-substrings-in-wraparound-string/

定义字符串 base 为一个 "abcdefghijklmnopqrstuvwxyz" 无限环绕的字符串,所以 base 看起来是这样的:

  • "...zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd....".

给你一个字符串 s ,请你统计并返回 s 中有多少 不同****非空子串 也在 base 中出现。

示例 1:

输入:s = "a"
输出:1
解释:字符串 s 的子字符串 "a" 在 base 中出现。

示例 2:

输入:s = "cac"
输出:2
解释:字符串 s 有两个子字符串 ("a", "c") 在 base 中出现。

示例 3:

输入:s = "zab"
输出:6
解释:字符串 s 有六个子字符串 ("z", "a", "b", "za", "ab", and "zab") 在 base 中出现。

提示:

  • 1 <= s.length <= 105
  • s 由小写英文字母组成

思路

算法思路:

1. 状态表达:

定义动态规划数组 dp,其中 dp[i] 表示以 i 位置的元素为结尾的所有子串中,有多少个在 base 中出现过。

2. 状态转移方程:

对于 dp[i],可以根据子串的「长度」划分为两类:子串的长度等于 1:此时这一个字符会出现在 base 中;子串的长度大于 1:如果 i 位置的字符和 i - 1 位置上的字符组合后,出现在 base 中的话,那么 dp[i - 1] 里面的所有子串后面填上一个 s[i] 依旧在 base 中出现。因此,dp[i] = dp[i - 1]

综上, dp[i] = 1 + dp[i - 1],其中 dp[i - 1] 是否加上需要先做一下判断。

3. 初始化:

可以根据「实际情况」,将表里面的值都初始化为 1

4. 填表顺序:

从左往右遍历字符串。

5. 返回值:

不能直接返回 dp 表里面的和,因为会有重复的结果。在返回之前,需要先「去重」:相同字符结尾的 dp 值,仅需保留「最大」的即可,其余 dp 值对应的子串都可以在最大的里面找到;可以创建一个大小为 26 的数组,统计所有字符结尾的最大 dp 值。

代码

cpp 复制代码
class Solution {
public:
    int findSubstringInWraproundString(string s) {
        int n=s.size();
        vector<int> dp(n,1);
        for(int i=1;i<n;i++)
            if(s[i]-1==s[i-1]||(s[i-1]=='z'&&s[i]=='a'))
                dp[i]=dp[i-1]+1;
        
        int hash[26]={0};
        for(int i=0;i<n;++i)
            hash[s[i]-'a']=max(hash[s[i]-'a'],dp[i]);
        
        int sum=0;
        for(int& x:hash) sum+=x;

        return sum; 
    }
};
相关推荐
Lenyiin16 分钟前
02.06、回文链表
数据结构·leetcode·链表
爪哇学长19 分钟前
双指针算法详解:原理、应用场景及代码示例
java·数据结构·算法
Dola_Pan23 分钟前
C语言:数组转换指针的时机
c语言·开发语言·算法
繁依Fanyi36 分钟前
简易安卓句分器实现
java·服务器·开发语言·算法·eclipse
烦躁的大鼻嘎1 小时前
模拟算法实例讲解:从理论到实践的编程之旅
数据结构·c++·算法·leetcode
C++忠实粉丝1 小时前
计算机网络socket编程(4)_TCP socket API 详解
网络·数据结构·c++·网络协议·tcp/ip·计算机网络·算法
祁思妙想1 小时前
10.《滑动窗口篇》---②长度最小的子数组(中等)
leetcode·哈希算法
用户37791362947552 小时前
【循环神经网络】只会Python,也能让AI写出周杰伦风格的歌词
人工智能·算法
福大大架构师每日一题2 小时前
文心一言 VS 讯飞星火 VS chatgpt (396)-- 算法导论25.2 1题
算法·文心一言
EterNity_TiMe_2 小时前
【论文复现】(CLIP)文本也能和图像配对
python·学习·算法·性能优化·数据分析·clip