1400分的dp汗流浃背之【交替子数组计数】

Problem: 3101. 交替子数组计数

  1. 前言
  2. 思路
  3. 解题过程
  4. 复杂度
  5. Code

前言

一个长度为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]

};
相关推荐
奋发向前wcx1 小时前
P2590 树的统计 题目解析
数据结构·算法·深度优先
imbackneverdie2 小时前
AI4S不止于分子药物:以MedPeer为代表的科研基建打开产业新增量
大数据·人工智能·算法·aigc·科研·学术·ai 4s
额鹅恶饿呃3 小时前
C语言中的数据结构和变量
c语言·数据结构·算法
运行时记录4 小时前
prompt-optimizer skill
算法
万法若空4 小时前
【数据结构-哈希表】哈希表原理
数据结构·算法·散列表
退休倒计时4 小时前
【每日一题】LeetCode 437. 路径总和 III TypeScript
算法·leetcode·typescript
学逆向的5 小时前
汇编——内存
开发语言·汇编·算法·网络安全
tachibana25 小时前
hot100 翻转二叉树(226)
java·数据结构·算法·leetcode
兰令水5 小时前
leecodecode【面试150】【2026.7.9打卡-java版本】
java·数据结构·leetcode·面试·职场和发展