一分钟一个小算法之前缀和

一、问题引入

        看如下问题:

        你有一个数组,长度为n,你要在数组中找到一个长度为len子数组,使得该子数组的和最大,并返回该子数组左边界的下标。

        对于这道题,我们最开始的想法是,定义一个左边界left,让left从0遍历到n-len,然后在每段长度为len的子数组中依次求和,再与最大值比较,并保存左边界。分析一下时间复杂度,左下标需要走的长度为n-len+1,每次在子数组求和的时间复杂度为O(len),总的时间复杂度为O(len(n-len+1)),最坏情况是当len取到\frac{n}{2}时,访问次数就达到了\frac{n^{2}}{4}+1次,这个最坏情况是一个O(n^{2})的算法,当n非常大时肯定接受不了,实际上n接近5000到10000这个量级就已经接受不了了。如果还有m次询问呢?那么时间复杂度直接来到了O(mn^{2}),m较大时直接爆炸,这个算法用都用不了。

        那么我们有没有方法可以改进呢?我们观察发现,m是询问次数,不能变,n为数组长度,这个是被动接受的,一般情况下变不了,那么就只剩下求和过程了,如何我能直接拿到该段子数组的和,那就省去了每次求和的时间,一次是len,可以省去计算m*len*(n-len-1)次,时间复杂度就变为O(mn)了,直接从O(n^{2})降到了O(n),这将会给算法性能带来巨大的提升。

二、什么是前缀和?

        前缀和,顾名思义,就是前面那部分的和。

        在这里,我们用一个数组来说明:

        上面图片准备了一个数组,下面的就是所谓的前缀和数组,通过观察发现,前缀和数组每个位置的值是原数组中该位置以前所有元素之和,也就是从第一个位置一直加到该位置的和。

        这样花费O(n)时间求出的前缀和数组有什么用呢?再回到刚开始的问题,我们发现,已经可以在O(1)的时间求出某一段区间的和了。怎样求出的呢?我们默认数组下标从0开始,假如我们要求[0,3]的和,我们可以直接查前缀和数组下标为3的值;假如我们要求[1,3]的值,我们只需要让前缀和数组中下标为3的值减去前缀和数组中下标为0的值(相当于把前面多加的和扣除)。

        通过这样一个前缀和数组,我们实现了任意区间和的快速查询,时间复杂度为O(1)。有聪明的小伙伴会发现,在求区间为[left,right]这个区间的和时,我们需要用前缀和数组的下标为right的值减去前缀和数组的下标为left-1的值,那么当left为0时,再求和会出现下标为-1的位置,这在C++里显然不合理,其实这时我们只需要判断一下,如果left为0,我们直接取right位置的值即可。或者想要更加模板,要套上面的公式,可以把数组用函数封装起来。等下代码中会体现出来。

三、代码实现

不用函数封装的情况,需要特判:

#include <iostream>
using namespace std;

//记录数组的长度
int n;

//准备一个大小为105、初始化全为0的数组
int arr[105];

//前缀和数组
int sum[105];

int main()
{
    cin >> n;
    //输入数组
    for(int i = 0; i < n; i++)
    {
        cin >> arr[i];
    }
    
    //求前缀和数组
    for(int i = 0; i < n; i++)
    {
        if(i == 0) //加到第一个位置就是第一个位置的值
        {
            sum[i] = arr[i];
        }
        else //其余位置为前i-1项的和加上第i项的值
        {
            sum[i] = sum[i-1] + arr[i];
        }
    }

    //求下标[0,4]的和
    int ans_0_4 = sum[4];
    
    //求下标[2,5]的和
    int ans_2_5 = sum[5] - sum[1];
    
    return 0;
}

 用函数封装,这里在函数中特判:

#include <iostream>
using namespace std;

//记录数组的长度
int n;

//准备一个大小为105、初始化全为0的数组
int arr[105];

//前缀和数组
int sum[105];

//函数的作用是查找下标为index的值
int qianzhuihe(int index)
{
    if(index == -1)
    {
        return 0;
    }
    else
    {
        return sum[index];
    }
}

int main()
{
    cin >> n;
    //输入数组
    for(int i = 0; i < n; i++)
    {
        cin >> arr[i];
    }
    
    //求前缀和数组
    for(int i = 0; i < n; i++)
    {
        if(i == 0) //加到第一个位置就是第一个位置的值
        {
            sum[i] = arr[i];
        }
        else //其余位置为前i-1项的和加上第i项的值
        {
            sum[i] = sum[i-1] + arr[i];
        }
    }

    //求下标[0,4]的和
    int ans_0_4 = qianzhuihe(4) - qianzhuihe(-1);
    
    //求下标[2,5]的和
    int ans_2_5 = qianzhuihe(5) - qianzhuihe(1);
    
    return 0;
}

四、一些变形

1.前缀积

        顾名思义,记录前面n项的积,这个时候应该减法为除法,此处不多赘述。

2.前缀异或和

        记录前n项的异或和,这里也不再赘述。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值