冉宝的每日一题--8月8日--前缀和+拓扑排序复习

本文通过每日一题1137. 第 N 个泰波那契数引入,讲解了如何利用前缀和解决数组问题。讨论了前缀和的概念、适用场景及模版,并给出了1365. 有多少小于当前数字的数字和1248. 统计「优美子数组」两个实例,详细阐述了前缀和在求解子数组和与奇数个数问题上的应用。同时强调了掌握模版的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天想学习前缀和,然后把昨天的拓扑排序学习了。
但是呢? 我们先惯例打开每日一题,看看难不难哈哈哈哈

然后呢,真的是无比简单的一题啊,那就让我们来练手吧

每日一题:1137. 第 N 个泰波那契数

https://leetcode-cn.com/problems/n-th-tribonacci-number/

题目描述

泰波那契序列 Tn 定义如下:

T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2

给你整数 n,请返回第 n 个泰波那契数 Tn 的值。

示例 1:

输入:n = 4
输出:4
解释:
T_3 = 0 + 1 + 1 = 2
T_4 = 1 + 1 + 2 = 4

示例 2:

输入:n = 25
输出:1389537

提示:

0 <= n <= 37
答案保证是一个 32 位整数,即 answer <= 2^31 - 1。

思路分析

非常非常典型的动态规划题,
时间复杂度:O(n)
空间复杂度:如果用滑动数组的话,O(1)
需要注意边界处理: 0,1,2 的赋值

用时: 21:28 - 21:31

class Solution:
    def tribonacci(self, n: int) -> int:
        if n == 0:return 0
        if n==1 or n == 2: return 1 

        pre_list = [0,1,1]
        for i in range(3,n+1):
            pre_list = pre_list[1:] + [sum(pre_list)]
        return pre_list[-1]

真的就so easy。

那么接下来我们就开始干活吧,虽然很晚了,但是我还是挺想吃板栗的,但是要忍住呀!
– 21:50的时候还是没忍住,吃了一袋。。

怎么说呢,经过我最近的反思,特别是从 课程表2 那题痛的领悟,我发现了,还是要先背模版再做题目,效率贼高,增益也大。所以我们先来看看前缀和的定义,以及它的模版是个什么玩意儿~

前缀和:

假设我们有一个字符串ABCDE,什么是这个单词的前缀,A、AB、ABC、ABCD、ABCDE就是这个单词的前缀,就是从第一个字母开始,依次往后拼接。E、ED、EDC、EDCB、EDCBA被称为这个单词的后缀。

前缀和大致分为两类,一维前缀和、二维前缀和。

适用场景:

一维前缀和的最主要目的就是求子数组的和的大小.
例如元素a[1]到a[3]的和。
a[1] + a[2] + a[3] = sum[3] - sum[0]
预处理数组的前缀和,可以达到 O ( 1 ) O(1) O(1)时间得到区间和

模版

一维前缀和

数组nums : 
1- 定义一个数组s:长度为 len(nums)+1, 其中s[i]代表:前i个元素的和 
2- 初始化 s[0] = 0, 原始元素的下标从1开始。
3-求l 到r 区间和: 前r个元素的和减去前l-1个元素的和即: s[r] - s[l-1] 

二维前缀和

学习了模版之后,我们现在就是勇敢牛牛啦,冲呀!

一维前缀和例题:1365. 有多少小于当前数字的数字

https://leetcode-cn.com/problems/how-many-numbers-are-smaller-than-the-current-number/

题目描述:

给你一个数组 nums,对于其中每个元素 nums[i],请你统计数组中比它小的所有数字的数目。

换而言之,对于每个 nums[i] 你必须计算出有效的 j 的数量,其中 j 满足 j != i 且 nums[j] < nums[i] 。

以数组形式返回答案。

示例 1:

输入:nums = [8,1,2,2,3]
输出:[4,0,1,1,3]
解释: 
对于 nums[0]=8 存在四个比它小的数字:(1,2,2 和 3)。 
对于 nums[1]=1 不存在比它小的数字。
对于 nums[2]=2 存在一个比它小的数字:(1)。 
对于 nums[3]=2 存在一个比它小的数字:(1)。 
对于 nums[4]=3 存在三个比它小的数字:(1,2 和 2)。

示例 2:

输入:nums = [6,5,4,8]
输出:[2,1,0,3]

示例 3:

输入:nums = [7,7,7,7]
输出:[0,0,0,0]

提示:

2 <= nums.length <= 500
0 <= nums[i] <= 100

思路:

说实话,我没明白这道题和前缀和的关系,
最简单的方法:

计数+ 排序

用时: 9:52- 10:00

class Solution:
    def smallerNumbersThanCurrent(self, nums: List[int]) -> List[int]:
        # 元素计数
        nums_dict = Counter(nums)
        # 对元素大小排列
        nums_sort = sorted(nums_dict.keys())
        
        # 前缀和计算前面元素的数量
        nums_sort_dict = {nums_sort[0]:0}
        for ind,num in enumerate(nums_sort[1:],start=1):
            nums_sort_dict[num] = nums_sort_dict[nums_sort[ind-1]] + nums_dict[nums_sort[ind-1]]
        
        # 返回
        return [nums_sort_dict[num] for num in nums]

请添加图片描述
其实写着写着就明白为啥用前缀和了。

简单题做完了,我们来一题中等题:,话说山姆的有机板栗可真好吃呀

一维前缀和:1248. 统计「优美子数组」

题目描述

给你一个整数数组 nums 和一个整数 k。

如果某个 连续 子数组中恰好有 k 个奇数数字,我们就认为这个子数组是「优美子数组」。

请返回这个数组中「优美子数组」的数目。

示例 1:

输入:nums = [1,1,2,1,1], k = 3
输出:2
解释:包含 3 个奇数的子数组是 [1,1,2,1] 和 [1,2,1,1] 。

示例 2:

输入:nums = [2,4,6], k = 1
输出:0
解释:数列中不包含任何奇数,所以不存在优美子数组。

示例 3:

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

思路

用时:10:05- 10:13
咋么说呢,如果不告诉我是一个前缀和的题目,我绝对想不到。
感觉确实要滑窗啊。
所以这个前缀和的定义可以是啥呢:答案呼之欲出,前i的元素中奇数的数量。
然后计算之后计算区间和,然后返回相等的数量。
那么就开搞吧~

特殊用例: k==1

class Solution:
    def numberOfSubarrays(self, nums: List[int], k: int) -> int:
        
        # 前缀和pre_sum[i]表示前i个元素中奇数的数量
        pre_sum = [0] + [0 for num in nums] 
        for ind,num in enumerate(nums,start=1):
            flg = 1 if num %2 ==1 else 0
            pre_sum[ind] = pre_sum[ind-1] + flg
        
        # 计算区间的数量,并且区间和数量为k的之后计数
        len_nums = len(nums)
        res = 0 
        # 注意所有下标从1开始
        # i->j的区间和 j>= i
        for i in range(1,len_nums+1): # 这里的边界需要注意
            for j in range(i,len_nums+1):
                tmp_sum = pre_sum[j] - pre_sum[i-1]
                if tmp_sum == k:
                    res += 1 
        return res 
        

时间复杂度: o ( n 2 ) o(n^2) o(n2),前缀和遍历o(n),区间和遍历 o ( n 2 ) o(n^2) o(n2),取 o ( n 2 ) o(n^2) o(n2)
空间复杂度: o ( n ) o(n) o(n) 记录前缀和的空间

然后依然超时了: 18 / 38 个通过测试用例

是一个特别大的,所以我们可以优化,去掉一些不必要的遍历,比如:
(1)区间长度小于k的,奇数的个数肯定小于k
(2)当区间长度为m的时候,奇数的个数大于k了,那么更长的肯定更大于,也没必要判断啦。
然后做了这两个优化之后:20 / 38 个通过测试用例

怎么说呢,就很扯淡。
那就再加个优化,从后缀和大于的地方开始遍历。

然后又扯淡的通过了23 / 38 个通过测试用例

– 我不想优化了,看题解吧。
然后我自己想到的优化,就是分成两半,分别计算。

题解思路:前缀和+ 查分

[i…j] 子数组中奇数个数恰好为k,条件可以转化为:
p r e s u m [ j ] − p r e s u m [ i − 1 ] = = k pre_sum[j]-pre_sum[i-1] ==k presum[j]presum[i1]==k
即:
p r e s u m [ i − 1 ] = = p r e s u m [ j ] − k pre_sum[i-1] == pre_sum[j] - k presum[i1]==presum[j]k
所以可以建立频次数组:cnt记录pre_sum[j] 出现的次数,从左往右更新答案,cnt[pre_sum[j-1]-k] 可以O(1) 拿到。

同时 pre_sum[i]的计算只与前一项有关可以使用滑动数组。

时间复杂度: O(n),其中 n为数组的大小。我们只需要遍历一遍数组即可求得答案。
**空间复杂度:**其n 为数组的大小。频次数组 c n t cnt cnt 记录的最大值不会超过 n n n ,因此只需要额外的 O ( n ) O(n) O(n) 的空间。

class Solution:
    def numberOfSubarrays(self, nums: List[int], k: int) -> int:
    	cnt = [0] * (len(nums) +1 )
    	cnt[0] = 1
    	odd ,ans = 0,0
    	for num in nums:
    		if num%2==1:
    			odd += 1
    		if odd >= k:
    			ans += cnt[odd-k]
    		cnt[odd] += 1
    	return ans

我只能说官方题解这个写的太妙了,比如那一长串好太多了,还是要多学习呀!

参考链接:
1- 前缀和+模版 https://blog.youkuaiyun.com/yl_puyu/article/details/109173844

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值