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!

  • 相关阅读:
    【郑轻邀请赛 G】密室逃脱
    【郑轻邀请赛 C】DOBRI
    【郑轻邀请赛 F】 Tmk吃汤饭
    【郑轻邀请赛 I】这里是天堂!
    【郑轻邀请赛 B】base64解密
    【郑轻邀请赛 A】tmk射气球
    【郑轻邀请赛 H】 维克兹的进制转换
    解决adb command not found以及sdk环境配置
    adb shell 命令详解,android, adb logcat
    Unexpected exception 'Cannot run program ... error=2, No such file or directory' ... adb'
  • 原文地址:https://www.cnblogs.com/BrianPeng/p/12165425.html
Copyright © 2011-2022 走看看