子数组系列一(数组中连续的一段)
点赞 👍👍收藏 🌟🌟关注 💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃
1.等差数列划分
题目链接: 413. 等差数列划分
题目分析:
如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。返回数组 nums 中所有为等差数组的 子数组 个数。
算法原理:
1.状态表示
经验 + 题目要求
以 i 位置为结尾,巴拉巴拉。
题目要求,求数组中为等差数组的子数组个数,也就是子数组中有多少个等差数列
dp[i] 表示:以 i 位置元素为结尾的所有子数组中有多少个等差数列。
2.状态转移方程
如果[a, b, c, d]已经构成一个等差数列,d后面在加一个e,与c、d、e构成等差数列,[a, b, c, d, e]也是构成等差数列。
dp[i] 表示:以 i 位置元素为结尾的所有子数组中有多少个等差数列。子数组要求是连续的,所以求 i 位置,要先去看 i -1 和 i - 2 的位置。
i - 2 位置元素设为a,i - 1 位置元素设为b、i 位置元素设为c
先考虑a、b、c是否构成一个等差数列。
如果abc能构成一个等差数列,那就是以ab为结尾的等差数列后面在加一个c,这些数列也是构成一个等差数列,以ab为结尾就相当于以b为结尾,以b为结尾的等差数列,就在dp[i-1]存着。别忘记 abc也能构成一个等差数列。
abc不能构成一个等差数列,即使a前面能构成等差数列,但是与 i 位置不连续,因此就构不成以 i 位置为结尾的等差数列
3.初始化
这里我们可以直接把dp[0] = dp[1] = 0
4.填表顺序
从左往右
5.返回值
注意并不是返回最后一个位置,题目要求找的是所有等差数列的个数,因此我们返回的是dp表所有元素的和。
cpp
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
// 1.创建 dp 表
// 2.初始化
// 3.填表
// 4.返回值
int n = nums.size();
vector<int> dp(n);
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;
}
int ret = 0;
for(auto e : dp)
ret += e;
return ret;
}
};
2.最长湍流子数组
题目链接: 978. 最长湍流子数组
题目分析:
给定一个整数数组 arr ,返回 arr 的 最大湍流子数组的长度 。
如果比较符号在子数组中的每个相邻元素对之间翻转,则该子数组是 湍流子数组 。
湍流子数组到底什么东西,我们举个例子,比如说下面,元素大小呈现一升一降的趋势这就是湍流数组。那就称这个数组是湍流子数组。
算法原理:
1.状态表示
经验 + 题目要求
以 i 位置为结尾,巴拉巴拉
要求找子数组中最大湍流长度。
dp[i] 表示:以 i 位置元素为结束的所有子数组中,最大湍流数组的长度
根据最近的一步分析问题,设 i - 1 位置元素为 a,i 位置元素为 b。此时会有三种情况。
a > b i位置最后呈现下降趋势
a < b i位置最后呈现上升趋势
a == b i位置最呈现平稳趋势
a > b i位置最后呈现下降趋势,我们是不是要找到以 i - 1 位置为结尾呈现升序趋势的湍流数组啊。
但是我们的状态表示只是表示 以 i 位置元素为结束的所有子数组中,最大湍流数组的长度,并没有分是上升趋势还是下降趋势。因此一个状态表示不能满足现在的情况。所以我们要换状态表示。
f[i] 表示:以 i 位置元素为结尾的所有子数组中,最后呈现 "上升" 状态下的最长湍流数组的长度
g[i] 表示:以 i 位置元素为结尾的所有子数组中,最后呈现 "下降" 状态下的最长湍流数组的长度
2.状态转移方程
先来分析f表,
当a > b i 位置最后呈现下降趋势,但是f表要的是 i 位置最后呈现上升趋势,别忘记本身也可以是湍流子数组,因此是1
当a < b i 位置最后呈现上升趋势,去 i - 1位置找到以 i - 1位置为结尾最后呈现下降趋势的最长湍流数组的长度这个就在 g[i - 1]存着,最后别忘记加上1
当 a == b,本身也可以是湍流子数组,因此是1
g表分析和f表分析一样,可以自己试着分析一下。
当a > b i 位置最后呈现下降趋势,去 i - 1位置找到以 i - 1位置为结尾最后呈现上升趋势的最长湍流数组的长度这个就在 f[i - 1]存着,最后别忘记加上1
当a < b i 位置最后呈现上升趋势,但是g表要的是 i 位置最后呈现下降趋势,别忘记本身也可以是湍流子数组,因此是1
当 a == b,本身也可以是湍流子数组,因此是1
3.初始化
填第一个位置会越界,可以把第一个位置初始化,注意本身也可以是湍流子数组,因此第一个位置可以初始化为1,不过这里我们可以把数组初始化都为1,这样的话
f表 a > b 和 a == b ,g表 a < b 和 a == b,填表的时候就不用考虑了,反正f[i]和g[i]都已经初始化为1了。
4.填表顺序
从左往右,两个表一起填
5.返回值
我们要的是子数组中最大湍流数组的长度,因此是找两个表里面的最大值。
cpp
class Solution {
public:
int maxTurbulenceSize(vector<int>& arr) {
// 1.创建 dp 表
// 2.初始化
// 3.填表
// 4.返回值
int n = arr.size();
vector<int> f(n, 1), 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;
else if(arr[i - 1] > arr[i]) g[i] = f[i - 1] + 1;
ret = max(ret, max(f[i],g[i]));
}
return ret;
}
};
3.单词拆分
题目链接: 139. 单词拆分
题目描述:
算法原理:
1.状态表示
经验 + 题目要求
以 i 位置为结尾,巴拉巴拉
题目要求看字符串能否被字典中单词拼接而成
dp[i] 表示:[0,i] 区间内的字符串,能否被字典中的单词拼接而成
2.状态转移方程
根据最近一个位置的情况,来划分问题
i的位置是最后一个单词的位置,那最后一个单词是什么样子呢?可能本身就是最后一个单词,或者往前几位然后和 i 位置组成一个单词。又或者前面所有位置构成最后一个单词。这样划分可以把0-i位置字符串划分成两部分,前面部分字符串+后面的单词。
如果我们能确定前面部分字符串能够拼接而成并且后面的单词在字典中,那 0 - i 位置字符串肯定能被拼接而成。
但是我们并不知道最后一个单词起始下标在哪里,因此设一个 j 为最后一个单词的起始位置下标。(0 <= j <= i)
所以我们的状态转移方程就有了
3.初始化
填第一个位置,dp[j -1]会越界,因此多开一个空间
- 虚拟节点里面的值要保证后序填表正确
- 下标的映射关系
虚拟节点如果给false,整个表不管字符串是什么样子都是false,因此给true。
下标的映射关系,对于普通数据我们都是下标减一然后才能找到原数组,但是字符串这里有特殊技巧,可以把原始字符前面多加一个辅助字符 ' ' 如 s = ' ' + s, 那原始字符串有效字符是不是从下标1开始了,正好就和多开一个空间的dp对应起来了。 因为字符串涉及找子串的问题,如果不这样搞,找子串非常头疼,因为下标都不对应了。
4.填表顺序
从左往右
5.返回值
看整个字符串能否被拼接成功,所以返回dp表最后一个位置,dp[i]
cpp
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
// 1.创建 dp 表
// 2.初始化
// 3.填表
// 4.返回值
//优化
unordered_set<string> us;
for(auto& str : wordDict)
us.insert(str);
int n = s.size();
vector<bool> dp(n + 1);
dp[0] = true; //保证后序填表是正确的
s = ' ' + s; //使原始字符串的下标统一+1,和填表顺序下标一样
for(int i = 1; i <= n; ++i)
{
for(int j = i; j >= 1; --j)
{
if(dp[j - 1] && us.count(s.substr(j, i - j + 1)))
{
dp[i] = true;
break;
}
}
}
return dp[n];
}
};
4.环绕字符串中唯一的子字符串
题目链接: 467. 环绕字符串中唯一的子字符串
题目分析:
给一个字符串s,返回字符串s中有多少非空子串在 base 中出现。
注意最后结果是去重的!
算法原理:
1.状态表示
经验 + 题目要求
以 i 位置为结尾,巴拉巴拉。
题目要求返回字符串s中有多少非空子串在 base 中出现。
以 i 位置为结尾我要先确定 以 i 位置为结尾的所有子串,要么是自己本身,要么就是和前面位置形成的子串,接下来找多少个在base中出现。所以状态表示:
dp[i] 表示:以 i 位置元素为结尾的所有子串里面,有多少个在base中出现
2.状态转移方程
单独本身就是一个子串,和前面元素结合构成子串。所以dp[i]也分这两种情况:
长度为1,长度大于1
长度为1,在base出现一次
长度大于1,前面元素都有一个共同点,都是以 i - 1 位置元素为结尾形成的子串,那我先找到以 i - 1 位置元素为结尾所有子串在base中出现的次数,然后在加上 i 位置元素构成的子串,看新的子串在base中出现的次数不就可以了吗。而dp[i-1] 就是以 i - 1位置元素为结尾所有子串在base中出现的次数。然后如果 i 位置元素s[i - 1] == s[i] || (s[i - 1] == z && s[i] ==a)说明长度大于1以 i 位置结束的字符串也在base出现了,出现次数是dp[i - 1],不能在后面 + 1!前面出现次数加上i位置这个字符次数还是一样的。
3.初始化
我们可以把dp表里面的值都初始化为1,首先本身肯定子串。其次初始化为1的话,上面dp表长度为1 填表时就不要在考虑了,只需要看是否满足长度为1的条件就行了。
4.填表顺序
从左往右
5.返回值
题目要求返回s 中有多少 不同非空子串 也在 base 中出现次数,
而dp[i] 表示:以 i 位置元素为结尾的所有子串里面,有多少个在base中出现。
因此返回 dp 表里面所有元素的和。
但是这样并不对!
我们初始化为1之后,这个例子我们最终返回的是3,但是答案最终是2,是去过重的。
思考一下如何去重?相同子串只统计一次!
看下面例子,两个字符串都是以c为结尾,dp表里面存的值,肯定是短的子串小,长的子串大。并且长的子串包含了短的子串的,因此长的子串里面统计的次数也包含了短的子串,因此当相同字符结尾的dp表里面的值较大的累加上,较小的直接舍去因为里面全都是重复的。
如何保证相同字符结尾的 dp 值,我们取最大的呢?
- 创建一个大小为 26 的数组
- 里面的的值保存相应字符结尾的最大 dp 值即可
最后返回数组里面的和
cpp
class Solution {
public:
int findSubstringInWraproundString(string s) {
// 1.创建 dp 表
// 2.初始化
// 3.填表
// 4.返回值
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];
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(auto x : hash)
sum += x;
return sum;
}
};