利用前缀信息解决子数组问题(上)

本篇文章用七个问题来详细探讨一下前缀信息来解决子数组问题的技巧

P r o b l e m 1 Problem1 Problem1 构建前缀和数组,快速解决子数组范围求和的问题

这个问题可以看专栏【前缀树】里的关于前缀和的文章,讲的很详细,定义,应用都涉及地很深。

P r o b l e m 2 Problem2 Problem2 构建前缀和最早出现的位置,返回无序数组中累加和为定值的最长子数组长度
描述:

给定一个无序数组nums,其中元素可正、可负、可0,给定一个整数 K K K,求nums所有子数组中累加和为 K K K的最长子数组长度。

输入描述:

第一行两个整数 N , K N,K N,K, N N N表示数组长度, K K K表示给定整数,第二行 N N N个数表示数组内的数

示例:

输入: 5 0
	  1 -2 1 1 1
输出: 3
问题分析:

假设定值为 K K K,也就是说子数组 n u m s [ i , . . . , j ] nums[i,...,j] nums[i,...,j]的累加和为 K K K,最先想到的就是暴力枚举,用两个for循环枚举出所有子数组,找到累加和为K的子数组,并更新最长子数组长度。

暴力枚举的时间复杂度是 O ( n 2 ) O(n^2) O(n2),我们得另寻他法。

根据前缀和的定义,我们可以清楚地知道:
n u m s [ i ] + . . . + n u m s [ j ] = p r e x [ j ] − p r e x [ i − 1 ] nums[i]+...+nums[j] = prex[j] - prex[i-1] nums[i]+...+nums[j]=prex[j]prex[i1]
如果 n u m s [ i ] + . . . + n u m s [ j ] = K nums[i] + ... + nums[j] = K nums[i]+...+nums[j]=K,说明 p r e x [ j ] − p r e x [ i − 1 ] = K prex[j] - prex[i-1] = K prex[j]prex[i1]=K,假设 p r e x [ j ] prex[j] prex[j]已知,那我们只需要找 j j j位置之前存不存在 p r e x [ i − 1 ] = p r e x [ j ] − K prex[i-1] = prex[j] - K prex[i1]=prex[j]K,如果存在说明 n u m s [ i , . . . , j ] nums[i,...,j] nums[i,...,j]的和为 K K K

刚刚讨论是存不存在的问题,如果存在,可能存在多个 i − 1 i-1 i1,题目要求的是最长子数组长度,所以我们应该取最小的那个 i − 1 i-1 i1,其实也就是前缀和 p r e x [ i − 1 ] prex[i-1] prex[i1]最早出现的位置。

我们给出上述思考所对应的代码:

int solutionProblem2(int nums[],int N,int K) {
    int prex[N + 1];
    prex[0] = 0;

    int result = 0; //存储结果
    for (int j = 0;j < N;j++) {
        prex[j + 1] = prex[j] + nums[j];
        int aim = prex[j + 1] - K;
        //开始寻找符合条件的prex[i-1] i = 0,1,2,..,j
        for(int t = 0; t <= j ;t++){
            if(prex[t] == aim){
                result = max(result, j-t);
                break;
            }
        }
    }
	return result;
}

重点提一下, n u m s [ i ] + . . . + n u m s [ j ] = p r e x [ j ] − p r e x [ i − 1 ] nums[i]+...+nums[j] = prex[j] - prex[i-1] nums[i]+...+nums[j]=prex[j]prex[i1] 公式中的 i i i是编号,从1开始,而 n u m s nums nums数组的下标从0开始,所以 p r e x [ 0 ] = 0 prex[0] = 0 prex[0]=0,无数字相加,前缀和自然为0。

再解释一下prex[j+1] = prex[j] + nums[j]这段代码,编号1 - j+1所有数字和为 p r e x [ j + 1 ] prex[j+1] prex[j+1],而编号1 - j 所有数字和为 p r e x [ j ] prex[j] prex[j],编号为j+1的数字为 n u m s [ j ] nums[j] nums[j]

优化分析:

同学们写完代码之后最好再计算一下代码的时间复杂度,我们发现,利用了前缀和的代码的时间复杂度也是 O ( n 2 ) O(n^2) O(n2),并没有起到太大的优化作用,哪个地方还能进行优化呢?

回顾代码我们发现,我们在寻找符合条件的 p r e x [ i − 1 ] prex[i-1] prex[i1]时竟然还在用循环来寻找, p r e x [ i − 1 ] prex[i-1] prex[i1]有很多个【比如 p r e x [ i − 1 ] = M , M prex[i-1] = M, M prex[i1]=M,M 会在前缀和数组中出现多次】,我们只要出现最早的那一个,也就是说只要用到 p r e x [ i − 1 ] prex[i-1] prex[i1],就要出现最早的那一个。所以我们可以利用哈希表来存储出现最早的 p r e x [ i − 1 ] prex[i-1] prex[i1],哈希表的形式为:{ p r e x [ i − 1 ] prex[i-1] prex[i1]的值:此值出现的最早位置}

最终优化后的代码:
int solutionProblem2(int nums[],int N,int K) {
    int prex[N + 1];
    prex[0] = 0;
	unordered_map<int,int> map;
    map[0] = 0;	//编号0不存在数字,其值自然为0
    
    int result = 0; //存储结果
    for (int j = 0;j < N;j++) {
        prex[j + 1] = prex[j] + nums[j];
        if(mp.count(prex[j+1]) == 0){
            mp[prex[j+1]] = j+1; //值为prex[j+1]的前缀和出现在编号为j+1的位置
        }
        int aim = prex[j + 1] - K;
        //开始寻找符合条件的prex[i-1] i = 0,1,2,..,j
        if(mp.count(aim)){
            result = max(result, j+1 - mp[aim]);
        }
    }
	return result;
}
P r o b l e m 3 Problem3 Problem3 构建前缀和出现的次数,返回无序数组中累加和为定值的子数组数量

Leetcode 560

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数

子数组是数组中元素的连续非空序列。

示例 1:

输入:nums = [1,1,1], k = 2
输出:2

示例 2:

输入:nums = [1,2,3], k = 3
输出:2

提示:

  • 1 <= nums.length <= 2 * 104
  • -1000 <= nums[i] <= 1000
  • -107 <= k <= 107
问题分析:

其实这道题目我在【前缀树】专栏里的一篇前缀和文章中详细讲过,现在我大致讲一下思路。

累加和为定值,那我们就要用到:
p r e x [ i − 1 ] = p r e x [ j ] − K prex[i-1] = prex[j] - K prex[i1]=prex[j]K
前缀和数组构建完毕后,只需要遍历 j j j即可,同时利用哈希表**{ p r e x [ i − 1 ] prex[i-1] prex[i1] : 出现次数 }**,在遍历每个 j j j时对最终结果进行更新。解决代码和上一题很相似,同学们看一遍就懂了。

解决代码:
int solutionProblem3(int nums[],int N,int K) {
	int prex[N + 1];
	prex[0] = 0;
	unordered_map<int,int> map;  // prex[i-1] : 出现次数
	map[0] = 1; //无数字相加为0,算出现一次
    
	int result = 0; //存储结果
    for (int j = 0;j < N;j++) {
        //构建prex[]与map{:}
        prex[j + 1] = prex[j] + nums[j];
        map[prex[j + 1]]++;

        int aim = prex[j + 1] - K;
        if (map.count(aim) != 0) {
            result += map[aim];
        }
    }
    return result;
}
P r o b l e m 4 Problem4 Problem4 构建前缀和最早出现的位置,返回无序数组中正数和负数个数相等的最长子数组长度

给定一个无序数组nums,其中元素可正、可负、可0,给定一个整数 K K K,求arr所有子数组中正数和负数个数相等的最长子数组长度。

要求】时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

示例:

输入:5
	 1 -2 1 1 1
输出:2
问题分析:

这个问题乍一看无从下手,因为正数和负数个数相等并不等价于累加和为某个值,前缀和自然也就用不了了。所以我们要适当地对原数组做一些变换,既然正数和负数的个数相等,记正数的个数为 a a a,那负数的个数也为 a a a,我们发现 a + − a = 0 a+ -a = 0 a+a=0

也就是说,我们把所有正数变为1,所有负数变为-1,0照样还是0,原数组变换之后,题目就可以简化成下面这种描述:

​ 返回无序数组中累加和为0的最长子数组长度。

有同学会问,累加和为0可以吗?如果数组里的元素全是0 呢?

其实也没问题,因为0既不是正数也不是负数,正数、负数的个数都为0,也符合正数和负数个数相等这个条件。

解决代码:
int solutionProblem4(int nums[],int N) {
    int newNums[N];
    for (int i = 0;i < N;i++) {
        if (nums[i] > 0) newNums[i] = 1;
        else if (nums[i] < 0)    newNums[i] = -1;
        else newNums[i] = 0;
    }
    
    int prex[N + 1];
	prex[0] = 0;
	unordered_map<int,int> map;  // prex[i-1] : 最早出现位置
    map[0] = 0;
    
	int result = 0; //存储结果
    for (int j = 0;j < N;j++) {
        //构建prex[]与map{:}
        prex[j + 1] = prex[j] + newNums[j];
        if (map.count(prex[j + 1]) == 0) {
            map[prex[j + 1]] = j + 1;
        }

        int aim = prex[j + 1] - 0;
        if (map.count(aim) != 0) {
            result = max(result, j + 1 - map[aim]);
        }
    }
    return result;
}
P r o b l e m 5 Problem5 Problem5 构建前缀和最早出现的位置,表现良好的最长时间段问题

LeetCode1124 表现良好时间段

给你一份工作表 h o u r s hours hours,上面记录某一位员工的每天的工作小时数。我们认为当员工一天中的工作小时数大于8小时的时候,那么这一天就是劳累的一天。所谓表现良好的时间段,意味着在这段时间内,劳累的天数是严格大于不劳累的天数,请你返回表现良好时间段的最大长度。

示例1:

输入: hours = [9,9,6,0,6,6]
输出: 3
问题分析:

我们对原数组做以下变换:

  • 大于8的数字转换为1
  • 小于或等于8的数字转换为-1

做变换之后,如果一个子数组 n u m s [ i , . . . , j ] nums[i,...,j] nums[i,...,j]是表现良好时间段,那么
n u m s [ i ] + n u m s [ i + 1 ] + . . . + n u m s [ j ] > 0 nums[i] + nums[i+1]+...+nums[j] > 0 nums[i]+nums[i+1]+...+nums[j]>0
转换为前缀和形式:
p r e x [ j ] − p r e x [ i − 1 ] > = 1 p r e x [ i − 1 ] < = p r e x [ j ] − 1 prex[j] - prex[i-1] >= 1 \\ prex[i-1] <= prex[j] - 1 prex[j]prex[i1]>=1prex[i1]<=prex[j]1
【注意】:这里的i,j都是编号,从1开始。

如果有 p r e x [ j ] > 0 prex[j] > 0 prex[j]>0,那么说明 n u m s [ 1 ] + . . . + n u m s [ j ] nums[1]+...+nums[j] nums[1]+...+nums[j]是表现良好时间段,长度为 j j j

如果 p r e x [ j ] < = 0 prex[j] <= 0 prex[j]<=0,那我们得借助前缀和来分析:

这时候有一个问题,我们最终只能得到 p r e x [ i − 1 ] < p r e x [ j ] prex[i-1] < prex[j] prex[i1]<prex[j],在对 j j j遍历时,我们可以知道 p r e x [ j ] prex[j] prex[j],但 p r e x [ i − 1 ] prex[i-1] prex[i1]怎么拿到呢?难道要对哈希表**{ p r e x [ i − 1 ] prex[i-1] prex[i1]:出现的最早位置}**里面的键“ p r e x [ i − 1 ] prex[i-1] prex[i1]”进行遍历吗?遍历的时间复杂度是 O ( n ) O(n) O(n),显然不能对哈希表进行遍历。

只要满足 p r e x [ i − 1 ] < = p r e x [ j ] − 1 prex[i-1] <= prex[j]-1 prex[i1]<=prex[j]1,那 n u m s [ i , . . . , j ] nums[i,...,j] nums[i,...,j]就是表现良好时间段,并且,我可以笃定地告诉你,当 p r e x [ i − 1 ] = p r e x [ j ] − 1 prex[i-1] = prex[j]-1 prex[i1]=prex[j]1时, n u m s [ i , . . . , j ] nums[i,...,j] nums[i,...,j]是以 j j j结尾最长的表现良好时间段。

举个例子,比如 n u m s [ 1 ] + n u m s [ 2 ] + . . . + n u m s [ j ] = − 3 nums[1]+nums[2]+...+nums[j] = -3 nums[1]+nums[2]+...+nums[j]=3,也就是 p r e x [ j ] = − 3 prex[j] = -3 prex[j]=3,那我们就要寻找前缀和数组中有没有 p r e x [ i − 1 ] < = − 4 prex[i-1] <= -4 prex[i1]<=4

我们假设有 p r e x [ m 1 ] = − 5 prex[m_1] = -5 prex[m1]=5,并且此时的 n u m s [ m 1 , . . . , j ] nums[m_1,...,j] nums[m1,...,j]是最长的表现良好的时间段。那么请问, p r e x [ m 1 ] prex[m_1] prex[m1]的值是怎么来的呢?是不是有许多个 1 , − 1 , 1 , − 1 1,-1,1,-1 1,1,1,1加来的?也就是说前缀和数组中的值是 + 1 , − 1 , − 1 , + 1 +1,-1,-1,+1 +1,1,1,+1这么变化的,每次都是做差值为 1 1 1或者 − 1 -1 1的运算。所以 − 5 -5 5的变化过程一定是这样的:
p r e x [ 0 ] = 0   − > . . . − >    − 1    − > . . . − >    − 2    − > . . . − >    − 3    − > . . . − >    − 4    − > . . . − >    − 5 prex[0] = 0 ~-> ...->~~-1~~ -> ...->~~-2~~->...->~~-3~~->...->~~-4~~->...->~~-5 prex[0]=0 >...>  1  >...>  2  >...>  3  >...>  4  >...>  5
所以 − 4 -4 4一定会比 − 5 -5 5出现的早,更比 − 6 , − 7 , . . . -6,-7,... 6,7,...出现的早。

所以我们直接找 p r e x [ i − 1 ] = p r e x [ j ] − 1 prex[i-1] = prex[j]-1 prex[i1]=prex[j]1就好了。

解决代码:
int solutionProblem5(int nums[], int N) {
    //对原数组进行处理
    int newNums[N];
    for (int i = 0; i < N;i++) {
        if (nums[i] > 8) newNums[i] = 1;
        else    newNums[i] = -1;
    }

    int prex[N + 1];
    prex[0] = 0;
    unordered_map<int, int> map;
    map[0] = 0;

    int result = 0; //存储结果
    for (int j = 0;j < N;j++) {
        //构建prex[]与map{:}
        prex[j + 1] = prex[j] + newNums[j];
        if (prex[j + 1] > 0)   result = max(result, j + 1);
        if (map.count(prex[j + 1]) == 0) {
            map[prex[j + 1]] = j + 1;
        }

        int aim = prex[j + 1] - 1;
        if (map.count(aim) != 0) {
            result = max(result, j + 1 - map[aim]);
        }
    }
    return result;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

东秦小熊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值