Problem: 3101. 交替子数组计数
前言
一个长度为n的数组,它的子数组总数为((1+n)*n)/2。

思路
一开始是当作定长的滑动窗口去做的,窗口长度从
1递增至n。但是很显然,超时了。为什么会超时,因为做了很多重复的计算。
这道题感觉确实和【数学】标签有关联。遇到这种题目,一般通用的解题思路一定要灵活调整。
和以前的美丽下标对 ,优雅集合一样,又是一道对概念下定义的题目。
交替数组:相邻的数字不同
因此我们就要思考,如何判断一个数组是交替数组 ?
我想最直接的思路就是,遍历此数组,看看是否全部的相邻数字对(比如[0,1,0]中的[0,1],[1,0])均不相同,只要存在1对相邻数字相同,则此数组就不算是交替数组。
那这个判断方法的时间复杂度是O(n)的。当我们在循环中使用这个方法去判断时,则整体的时间复杂是就变成了O(n^2),我们才看一下题目给的nums长度是10^5,显然,这样做,哪怕逻辑没问题,提交后肯定会超时的。

接下来我们就要考虑,我们是否需要每次都要对一个子数组进行一遍O(n)时间复杂度的判断呢?
上述暴力的做法,是否没有好好利用起来题干的条件?
答案是显然的。
考虑一个数组
[0,1,0]
它的子数组有这些:[0],[1],[0],[0,1],[1,0],[0,1,0]。
我们注意看[0,1]和[0,1,0]的关系
当我们知道[0,1]是交替数组时,那么这就表明它的每一对相邻数字已经不相同了。此时我们只要判断此数组的最后一位,是否和接下来要加进去的数字相同。
- 假如数组的最后一位和将要加进来的数字相同 ,则组装后的新数组一定不是 交替数组
- 假如数组的最后一位和将要加进来的数字不同 ,则组装后的新数组一定是 交替数组
OK,了解了之后,咱们的整体思路就和题目给的提示一样,用动态规划的思维进行编程。
我们要求[0,1,0]的交替数组总数
是不是存在子问题[0,1]的交替数组总数?
因此状态转移方程可以写成这样
dp[i] = dp[i-1] + 某个分类讨论后的数字
解题过程
接下来我们就是去解决分类讨论的问题了。
如果此数组符合交替数组的定义,比如0,1符合,那么假如新增的数字是0,0和1不同,则0,1,0一定是交替数组,它们的子数组全部都是交替数组。
此时dp[1] = 3,则dp[2] = dp[1] + 3 = 6,答案正确。
我们就可以写出这样的逻辑了
js
let nlen = nums.length;
let fn = Array(nlen).fill(0)
// 以第一项结尾的交替子数组总数一定是1
fn[0] = 1;
for (let i = 1; i < nums.length; i++) {
if (nums[i] !== nums[i - 1]) {
fn[i] = fn[i - 1] + i + 1
}
}
return fn[nlen - 1]
那么继续,假如新增的数字和之前数组的末尾数字相等,那么符合交替数组的子数组总数只增加了1个(即这个新增数字本身)。
举个例子,假如数组是[0,1,1]
那么[0,1,1]只比[0,1]多了1个交替数组,即[1]
于是我们可以把逻辑补充一下
js
let nlen = nums.length;
let fn = Array(nlen).fill(0)
// 以第一项结尾的交替子数组总数一定是1
fn[0] = 1;
for (let i = 1; i < nums.length; i++) {
if (nums[i] !== nums[i - 1]) {
fn[i] = fn[i - 1] + i + 1
}
else {
fn[i] = fn[i - 1] + 1
}
}
return fn[nlen - 1]
这样写完之后,我们提交,会发现,过不了几个用例就提交错误了,为什么?

显然,我们把满足交替数组的子数组总数的计算想得太简单了。
fn[i] = fn[i - 1] + i + 1
只在fn[i-1]的子数组全部都是交替数组时,才成立。
假如fn[i-1]是[0,1,0,0]
那么当fn[i]是[0,1,0,0,1]时,
就算1和[0,1,0,0]的末尾0不同,[0,1,0,0,1]本身也不是交替数组,并且[0,1,0,0,1]满足交替数字定义的子数组数量也一定不等于子数组总数。
因此我们就能得出一个新的case,即只有当fn[i-1]的子数组全是交替数组时,fn[i] = fn[i - 1] + i + 1才会真的成立。
于是我们把代码改成这样:
js
for (let i = 1; i < nums.length; i++) {
if (nums[i] !== nums[i - 1]) {
if (fn[i - 1] === (1 + i) * (i) / 2) {
fn[i] = fn[i - 1] + i + 1
}
else {
// todo
}
}
else {
fn[i] = fn[i - 1] + 1
}
}
终于,我们现在只剩下1种情况需要讨论
当前数字与末尾数字不同,并且前数组也并不是完全的交替数组时,如何计算fni?
我们需要思考,当数组多1位数字作为结尾,从原来的长度变成原来长度+1时,到底会多出来多少个子数组?
比如当[0,1,0,0]变成[0,1,0,0,1]时,它会多出来多少个子数组?
首先是[1]本身,然后是[0,1],[0,0,1],[1,0,0,1],[0,1,0,0,1]
即,当fn[i-1]变成fn[i]时,会多出i+1个子数组。
那么如何判断这些子数组是否是交替数组呢?
我们只要从后向前去遍历,判断当前项是否与当前项的前一项相同即可。相同,则count+1,不同,则立刻退出循环。
于是我们就能补充剩下的case,完整地写完代码了。
js
for (let i = 1; i < nums.length; i++) {
if (nums[i] !== nums[i - 1]) {
if (fn[i - 1] === (1 + i) * (i) / 2) {
fn[i] = fn[i - 1] + i + 1
}
else {
let count = 0;
let flag = i;
while (nums[flag] !== nums[flag - 1]) {
count++;
flag--;
}
fn[i] = fn[i - 1] + count + 1
}
}
else {
fn[i] = fn[i - 1] + 1
}
}
复杂度
- 时间复杂度

- 空间复杂度: O(n)
Code
js
/**
* @param {number[]} nums
* @return {number}
*/
var countAlternatingSubarrays = function (nums) {
let nlen = nums.length;
let fn = Array(nlen).fill(0)
// 以第一项结尾的交替子数组总数一定是1
fn[0] = 1;
for (let i = 1; i < nums.length; i++) {
if (nums[i] !== nums[i - 1]) {
if (fn[i - 1] === (1 + i) * (i) / 2) {
fn[i] = fn[i - 1] + i + 1
}
else {
let count = 0;
let flag = i;
while (nums[flag] !== nums[flag - 1]) {
count++;
flag--;
}
fn[i] = fn[i - 1] + count + 1
}
}
else {
fn[i] = fn[i - 1] + 1
}
}
return fn[nlen - 1]
};