Leetcode327.区间和的个数-归并妙用


前言

归并排序是一种大家都熟知的经典排序算法,我认为归并的含义就在于它先将问题分解到最小,再两两合并起来,这样一层层向上,最终可以解决原来的大问题,本质是递归。下面这个图可以直观地反映出归并的内核(图中描述的是将数组 [5,4,2,7,1,6,8,3] 升序排序的过程)。

了解了归并的核心,我们就可以把它的思想用到其他问题上,比如Leetcode327题。这是一道困难题,不过看了官方题解后感觉思路清楚了~

在这里插入图片描述


一、Leetcode327 题目描述

在这里插入图片描述
题目的意思是,在一个给定数组nums中,找到区间和在 [lower, upper] 范围内(包括边界)的区间个数。区间和指的是nums的一个区间 [a,b] 中所有元素的和,a、b 代表的是下标。

二、归并算法

1.前缀和求区间和

首先,要想找到满足条件的区间,就得求区间和,再看这个和能不能满足要求。看到求区间和,就要想到利用前缀和计算,而不是傻傻地真的列出所有区间求个sum。

  • 什么是前缀和呢?
    对于一个数组,它的前缀和是个和原数组等长的数组,每个位置处的值是原数组中该位置及之前位置所有元素的和。举个栗子:
    原数组是 [ 1 , 2 , 3 , 4 , 5 ] [1,2,3,4,5] [1,2,3,4,5] ,它的前缀和数组是 [ 1 , 3 , 6 , 10 , 15 ] [1,3,6,10, 15] [1,3,6,10,15]
  • 如何用前缀和计算区间和?
    假设前缀和数组为 p r e S u m preSum preSum,那么 p r e S u m [ j ] − p r e S u m [ i ] preSum[j]-preSum[i] preSum[j]preSum[i] 就表示数组中下标区间是 ( i , j ] (i, j] (i,j] 的元素和,也就是区间和,注意区间左开右闭。在求区间和的时候,要在 p r e S u m preSum preSum 数组第一位前加入一个元素0,这样才能正确计算包含原数组第一个元素的区间的和。即 p r e S u m = [ 0 ] + p r e S u m preSum = [0] + preSum preSum=[0]+preSum
    在求前缀和的时候, p r e S u m preSum preSum 的长度要比原数组长1 。

2.归并思想划分问题

现在我们知道了求区间和的简便做法,但是考虑到时间复杂度,不可能把全部能构成的区间都列出来求区间和。这时考虑用归并算法求解。
(获得前缀和后,我们直接用前缀和数组做后续的求解,不需要原数组了)

利用归并思想,我们将前缀和数组从中间一分为二,获得左右两个小数组,再分别对左右两数组求满足要求的区间数。这里对左边数组的求解结果,指的是全部落在左边且满足条件的区间个数;同理对右边数组的求解结果,是指全部落在右边且满足条件的区间个数。

这两个值相加并不完全,因为还没有考虑横跨左右两边的区间。我们再计算横跨左右两边且满足条件的区间个数即可,将这三个数加起来就是最终结果。

这里用递归实现将数组一层层划分到最小,再一层层返回结果的过程。递归的停止条件就是,在当前要解决的数组的左右边界相等时( l e f t = = r i g h t left==right left==right),我们直接返回0即可。

  • l e f t = = r i g h t left==right left==right)时,前缀和数组明明还有一个元素,那为什么返回0,而不是1呢?
    前面我们说过,求区间和的时候前缀和数组需要比原数组长度多1,如果前缀和数组中只有一个元素,那么我们可能认为它对应的原数组没有元素,那就不存在区间,自然要返回0。
    (我解释得有点抽象,大家自行理解叭==)

归并:向下,再向上!

通过上面的过程可以看出,将数组一分为二的过程就是在划分大问题为小问题。我们对划分后得到的左右数组分别求解,再将结果合并,这时就是在利用小问题的答案进一步解决大问题。这就是归并! 在这个问题中,我们可以直接求解的最小问题,就是前缀和数组中元素个数为1的情况。

3.横跨左右两边的区间

这里还有个技巧点,就是如何获得横跨左右两边且满足条件的区间个数?我认为官方解答很巧妙,需要先对两边的数组进行排序,再在右边数组中设定两个指针 l , r l,r l,r,作为满足条件的边界标志。具体操作直接看下图:

在这里插入图片描述

这里可能有个疑问,对于前缀和数组排序不就打乱了原来数组的顺序了吗,这样求得的区间和怎么会是正确的呢? 如果没有发现这个疑问,就不用看下面这个解释了,可能越看越乱 ≡(▔﹏▔)≡

要想解决这个困惑,就要了解排序出现的位置。大家可以先看完整代码,再来理解这个问题。通过代码可以看出,我们是在求解完左右数组的小问题之后,分别进行排序操作。它影响的是往上一层的更大数组的求解。这个排序相当于是对当前大问题下左数组和右数组分别排了个序,再去求大问题中横跨左右两边的区间个数,这样就根本不会有影响。因为原本在左边的前缀和还在左边,在右边的前缀和也还在右边,在利用前缀和求区间和时,不会出现区间和对应的区间的左端点在右端点的右边。

三、完整代码(python版)

代码如下(示例):

class Solution:
    def merge(self, presum, left, right, lower, upper):
        if left==right: return 0
        mid = (right-left)//2+left
        # 计算左右两个数组中满足条件的区间个数和
        res = self.merge(presum, left, mid, lower, upper)+self.merge(presum, mid+1, right, lower, upper)
		
		#计算横跨左右两边、满足要求的区间个数
        l, r = mid+1, mid+1
        for i in range(left, mid+1):
            while l<=right and presum[l]-presum[i]<lower: l+=1
            r = l
            while r<=right and presum[r]-presum[i]<=upper: r+=1
            res += (r-l)
        
        # 排序合并左右两个数组
        sort = []
        p1, p2 = left, mid+1
        while p1<mid+1 or p2<=right:
            if p1>mid: 
                sort.append(presum[p2])
                p2 += 1
            elif p2>right: 
                sort.append(presum[p1])
                p1 += 1
            else:
                if presum[p1]>presum[p2]: 
                    sort.append(presum[p2])
                    p2 += 1
                else: 
                    sort.append(presum[p1])
                    p1 += 1
        for i in range(left, right+1):
            presum[i] = sort[i-left]

        return res


    def countRangeSum(self, nums: List[int], lower: int, upper: int) -> int:
        presum = [0]
        cur = 0
        length = len(nums)
        # 获得前缀和数组
        for num in nums:
            cur += num
            presum.append(cur)
        # 对前缀和数组进行归并——利用前缀和数组,来确定满足条件的区间
        return self.merge(presum, 0, length, lower, upper)

其中下面这段代码是归并排序中的核心片段,希望自己能不过脑子地写出来:

sort = []  # 存放排序后数组
p1, p2 = left, mid+1                 # 设定两个指针辅助
while p1<mid+1 or p2<=right:
    if p1>mid:                       # 表示第一个数组的元素已经全部放入新数组
        sort.append(presum[p2])
        p2 += 1
    elif p2>right:                   # 表示第二个数组的元素已经全部放入新数组
        sort.append(presum[p1])
        p1 += 1
    else:                            # 把当前更小的元素放入新数组,并将对应指针下移一位
        if presum[p1]>presum[p2]: 
            sort.append(presum[p2])
            p2 += 1
        else: 
            sort.append(presum[p1])
            p1 += 1
# 将排序后的片段赋值给原数组片段
for i in range(left, right+1):
    presum[i] = sort[i-left]

总结

困难题需要用到的知识点好多啊~我认为这道题比较重要的是归并算法和前缀和的使用。
官方题解写的简洁,需要自己多理解!

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值