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]

};
相关推荐
大熊背29 分钟前
树莓派相机自动白平衡详解(四)
算法·树莓派·白平衡·isppipeline
一米阳光866133 分钟前
软考(中级)软件设计师核心笔记(9)算法——背包问题
笔记·算法·职场发展·软考·软件设计师·中级职称
晚风醉蝶44 分钟前
1-11-奇偶排序-OddEvenSort
java·数据结构·算法
旖旎夜光1 小时前
LeetCode 852:山脉数组的峰顶索引(二分查找) —— 题解
数据结构·c++·算法·leetcode·二分查找
白露与泡影3 小时前
解密 Pi 的 Harness 工程:Agent 会话如何实现持久化与恢复
java·人工智能·算法
happyprince4 小时前
01-整体观-Codex全貌解读(源码)
算法·ai编程
俊基科技5 小时前
别自己写算法了,让这颗37.5毫米的模组替你“闭嘴干活”
神经网络·算法·硬件开发·ai降噪·语音模块·回音消除
_Narcissus_5 小时前
动态规划初探(含完整代码实现)
c语言·数据结构·c++·算法·动态规划·背包问题·最短路径
世人万千丶6 小时前
鸿蒙项目实战 - 社区活动编排板:标签云布局算法与自动换行
学习·算法·华为·harmonyos·鸿蒙