zoukankan      html  css  js  c++  java
  • Basic Thought / Data Structure: 前缀和 Prefix Sum

    Intro:

    在OI中,前缀和是一种泛用性很高的数据结构,也是非常重要的优化思想


    Function: 求静态区间和

    模板题:输入序列 \(a_{1..n}\) ,对于每一个输入的二元组 \((l,r)\) ,求 \(\sum_{i=l}^ra_i\)

    先想一想朴素算法怎么做吧

    对于输入的每一组 \((l,r)\) ,遍历序列 \(a_{l..r}\) 求和,代码如下

    int s(0);
    for(int i(l);i<=r;++i)s+=a[i];
    return s;
    

    Time complexity: \(O(n)\)

    Memory complexity: \(O(n)\)

    如果有m次询问,则总时间复杂度 \(O(nm)\)

    观察这个过程,可以发现有大量多余运算,比如说,对于两次询问,它们区间的交集就是被多余运算的

    那么有没有方法使得计算量减少呢?

    可以发现 \(\sum_{i=l}^ra_i=\sum_{i=1}^ra_i-\sum_{i=1}^{l-1}a_i\)

    也就是说如果存在数组 \(s_{1..n}\) 使得 \(s_i=\sum_{j=1}^ia_j\) ,则

    \(\sum_{i=l}^ra_i=s_r-s_{l-1}\)

    \(s_{1..n}\) 就是传说中的前缀和数组啦!


    Operation:

    First: 为数组 \(a_{1..n}\) 构造前缀和数组 \(s_{1..n}\)

    Second: 对于输入区间 \([l,r]\) ,直接计算出区间和 \(s_r-s_{l-1}\)

    关键就是前缀和数组如何构造

    使用递推思想

    \(s_i=\sum_{j=1}^ia_j=\sum_{j=1}^{i-1}a_j+a_i=s_{i-1}+a_i\)


    Code:

    \(s_{1..n}\)

    for(int i(1);i<=n;++i)s[i]=s[i-1]+a[i];
    

    询问

    return s[r]-s[l-1];
    

    Time complexity: \(O(n)\) 预处理 \(O(1)\) 查询

    Memory complexity: \(O(n)\)

    P.s 可以直接将原数组变成前缀和数组,则不需要额外空间,代码如下

    for(int i(2);i<=n;++i)a[i]+=a[i-1];
    

    Example:

    洛谷P1114 “非常男女”计划

    这是一道练习前缀和思想(然而不是特别明显)的经典题目

    如果将男生表示 \(1\) ,女生表示 \(-1\) ,那么题目就变成了求能使区间和为\(0\)的最大区间长度(是不是有点前缀和的味道了)

    但是单单枚举区间两端, \(O(n^2)\) 的时间复杂度明显超时,而且这种方法根本不需要前缀和(直接求和即可)

    这道题的要点在于 \(\sum_{i=l}^ra_i=\sum_{i=1}^ra_i-\sum_{i=1}^{l-1}a_i\) (是不是很眼熟)

    所以当 \(\sum_{i=l}^ra_i=0\) 时, \(\sum_{i=1}^ra_i=\sum_{i=1}^{l-1}a_i\)

    所以关键在于对于两个端点 \(l,r\) ,如果 \(s_l=s_r\) ,那么 \([l+1,r]\) 就是一个可行区间

    对所有端点\(i\),按照\(s_i\)分类,对每一类求极差,取最大值即可

    P.s 其中端点 \(0\) 属于 \(s_i=0\)

    具体见代码( \(s_{1..n}\) 被优化掉了,用 \(p_i,-n\leqslant i\leqslant n\) 表示每一个 \(s_j=i\) 的最小 \(j\) ,若不存在则 \(p_i=-1\)

    //This program is written by Brian Peng.
    #pragma GCC optimize("Ofast","inline","-ffast-math")
    #pragma GCC target("avx,sse2,sse3,sse4,mmx")
    #include<bits/stdc++.h>
    using namespace std;
    #define Rd(a) (a=read())
    #define Gc(a) (a=getchar())
    #define Pc(a) putchar(a)
    inline int read(){
    	register int x;register char c(getchar());register bool k;
    	while(!isdigit(c)&&c^'-')if(Gc(c)==EOF)exit(0);
    	if(c^'-')k=1,x=c&15;else k=x=0;
    	while(isdigit(Gc(c)))x=(x<<1)+(x<<3)+(c&15);
    	return k?x:-x;
    }
    void wr(register int a){
    	if(a<0)Pc('-'),a=-a;
    	if(a<=9)Pc(a|'0');
    	else wr(a/10),Pc((a%10)|'0');
    }
    signed const INF(0x3f3f3f3f),NINF(0xc3c3c3c3);
    long long const LINF(0x3f3f3f3f3f3f3f3fLL),LNINF(0xc3c3c3c3c3c3c3c3LL);
    #define Ps Pc(' ')
    #define Pe Pc('\n')
    #define Frn0(i,a,b) for(register int i(a);i<(b);++i)
    #define Frn1(i,a,b) for(register int i(a);i<=(b);++i)
    #define Frn_(i,a,b) for(register int i(a);i>=(b);--i)
    #define Mst(a,b) memset(a,b,sizeof(a))
    #define File(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout)
    #define N (200010)
    #define P(a) (p[a+n])
    int n,p[N],s,ans;
    signed main(){
    	Rd(n),Mst(p,-1),P(0)=0;
    	Frn1(i,1,n)s+=read()?1:-1,~P(s)?ans=max(ans,i-P(s)):P(s)=i;
    	wr(ans),exit(0);
    }
    

    到此为止前缀和的所有基本操作都讲完啦!


    Conclusion & Extension:

    前缀和是一种泛用性很高的数据结构,也是非常重要的优化思想

    它利用预处理和递推的方法减少多余运算,达到优化的目的

    不仅适用于加法,还适用于所有满足于结合律而且具有单位(对于加法就是 \(0\) )和逆元(对于加法 \(a\) 的逆元是 \(-a\) )的二元运算,如乘法

    但是对于求最值就不太靠谱了(对于这类问题(称为RMQ问题)也有很棒的算法)

    前缀和数组的online version:树状数组

    Particularly, 前缀和还有逆运算:差分

    到此为止本篇文章就圆满结束啦,请各位奆佬们多多指教和支持,THX!

  • 相关阅读:
    vue中的$nextTick()
    对SPA(单页面应用)的总结
    函数节流和函数防抖
    前端路由
    let、const
    深拷贝与浅拷贝
    小白浅谈Ajax基础
    关于BFC布局的那些事
    关于BFC的那些事
    Sass基础知识及语法
  • 原文地址:https://www.cnblogs.com/BrianPeng/p/12165425.html
Copyright © 2011-2022 走看看