算法沉淀------动态规划之子数组、子串系列
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. 状态表达: 定义两个动态规划数组 f
和 g
,其中 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. 初始化: 所有的元素「单独」都能构成一个湍流数组,因此可以将 f
和 g
数组内所有元素初始化为 1
。由于用到前面的状态,因此循环的时候从第二个位置开始即可。
4. 填表顺序: 从左往右遍历数组,同时填充 f
和 g
两个数组。
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
s
和wordDict[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;
}
};