树状数组、线段树

题目链接

P3374 【模板】树状数组 1
P3368 【模板】树状数组 2
P3372 【模板】线段树 1
P3373 【模板】线段树 2
P6242 【模板】线段树 3


P3374 【模板】树状数组 1

题目描述

如题,已知一个数列,你需要进行下面两种操作:

  • 将某一个数加上 \(x\)

  • 求出某区间每一个数的和

输入格式

第一行包含两个正整数 \(n,m\),分别表示该数列数字的个数和操作的总个数。

第二行包含 \(n\) 个用空格分隔的整数,其中第 \(i\) 个数字表示数列第 \(i\) 项的初始值。

接下来 \(m\) 行每行包含 \(3\) 个整数,表示一个操作,具体如下:

  • 1 x k 含义:将第 \(x\) 个数加上 \(k\)

  • 2 x y 含义:输出区间 \([x,y]\) 内每个数的和

输出格式

输出包含若干行整数,即为所有操作 \(2\) 的结果。

输入

5 5
1 5 4 2 3
1 1 3
2 2 5
1 3 -1
1 4 2
2 1 4

输出

14
16

说明/提示

【数据范围】

对于 \(30\%\) 的数据,\(1 \le n \le 8,1\le m \le 10\)
对于 \(70\%\) 的数据,\(1\le n,m \le 10^4\)
对于 \(100\%\) 的数据,\(1\le n,m \le 5\times 10^5\)

  • 时间复杂度:\(O(logn)\)

代码

#include<bits/stdc++.h>
using namespace std;
using LL=long long;
const int N=5e5+10;
int n,m;
int a[N],c[N],s[N];
LL ask(int x)
{
    LL res=0;
    for(;x;x-=x&-x)res+=c[x];
    return res;
}
void add(int x,int y)
{
    for(;x<=n;x+=x&-x)c[x]+=y;
}
int main()
{
    scanf("%d%d",&n,&m);
    int x;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&x);
        s[i]=s[i-1]+x;
    }
    while(m--)
    {
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        if(x==1)add(y,z);
        else
            printf("%lld\n",s[z]-s[y-1]+ask(z)-ask(y-1));
    }
    return 0;
}

P3368 【模板】树状数组 2

题目描述

如题,已知一个数列,你需要进行下面两种操作:

  • 将某区间每一个数加上 \(x\)

  • 求出某一个数的值。

输入格式

第一行包含两个整数 \(N、M\),分别表示该数列数字的个数和操作的总个数。

第二行包含 \(N\) 个用空格分隔的整数,其中第 \(i\) 个数字表示数列第 \(i\) 项的初始值。

接下来 \(M\) 行每行包含 \(2\)\(4\)个整数,表示一个操作,具体如下:

操作 \(1\): 格式:1 x y k 含义:将区间 \([x,y]\) 内每个数加上 \(k\)

操作 \(2\): 格式:2 x 含义:输出第 \(x\) 个数的值。

输出格式

输出包含若干行整数,即为所有操作 \(2\) 的结果。

输入

5 5
1 5 4 2 3
1 2 4 2
2 3
1 1 5 -1
1 3 5 7
2 4

输出

6
10

说明/提示

数据规模与约定

对于 \(30\%\) 的数据:\(N\le8,M\le10\)

对于 \(70\%\) 的数据:\(N\le 10000,M\le10000\)

对于 \(100\%\) 的数据:\(1 \leq N, M\le 500000,1 \leq x, y \leq n\),保证任意时刻序列中任意元素的绝对值都不大于 \(2^{30}\)

  • 时间复杂度:\(O(mlogn)\)

代码

#include<bits/stdc++.h>
using namespace std;
using LL=long long;
const int N=5e5+10;
int n,m;
int a[N],c[N];
LL ask(int x)
{
    LL res=0;
    for(;x;x-=x&-x)res+=c[x];
    return res;
}
void add(int x,int y)
{
    for(;x<=n;x+=x&-x)c[x]+=y;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    while(m--)
    {
        int op,x,y,k;
        scanf("%d%d",&op,&x);
        if(op==1)
        {
            scanf("%d%d",&y,&k);
            add(x,k);
            add(y+1,-k);
        }
        else
            printf("%lld\n",a[x]+ask(x));
    }
    return 0;
}

P3372 【模板】线段树 1

题目描述

如题,已知一个数列,你需要进行下面两种操作:

  • 将某区间每一个数加上 \(k\)
  • 求出某区间每一个数的和。

输入格式

第一行包含两个整数 \(n, m\),分别表示该数列数字的个数和操作的总个数。

第二行包含 \(n\) 个用空格分隔的整数,其中第 \(i\) 个数字表示数列第 \(i\) 项的初始值。

接下来 \(m\) 行每行包含 \(3\)\(4\) 个整数,表示一个操作,具体如下:

  1. 1 x y k:将区间 \([x, y]\) 内每个数加上 \(k\)
  2. 2 x y:输出区间 \([x, y]\) 内每个数的和。

输出格式

输出包含若干行整数,即为所有操作 \(2\) 的结果。

输入

5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4

输出

11
8
20

说明/提示

对于 \(30\%\) 的数据:\(n \le 8,m \le 10\)
对于 \(70\%\) 的数据:\(n \le {10}^3 ,m \le {10}^4\)
对于 \(100\%\) 的数据:\(1 \le n, m \le {10}^5\)

保证任意时刻数列中任意元素的和在 \([u-2^{63}, 2^{63})\) 内。

树状数组

  • 解法
    我们不妨用树状数组维护前缀和,\(\sum_{i=1}^xc[i]\) 就是 \(a[x]\) 增加的值,而序列 \(a\) 的前缀和 \(a[1\sim x]\) 增加的值为:

\[\sum_{i=1}^x\sum_{j=1}^ic[j]\Leftrightarrow \sum_{i=1}^x(x-i+1)\times c[i]\Leftrightarrow (x+1)\sum_{i=1}^xc[i]-\sum_{i=1}^x i\times c[i] \]

  • 时间复杂度:\(O(mlogn)\)

代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
using LL=long long;
LL c[2][N],s[N];
int n,m;
LL ask(int x,int k)
{
    LL res=0;
    for(;x;x-=x&-x)res+=c[k][x];
    return res;
}
void add(int x,int y,int k)
{
    for(;x<=n;x+=x&-x)c[k][x]+=y;
}
int main()
{
    scanf("%d%d",&n,&m);
    LL x;
    for(int i=1;i<=n;i++)
    {
        scanf("%lld",&x);
        s[i]=s[i-1]+x;
    }
    while(m--)
    {
        int op,x,y;
        LL k;
        scanf("%d%d%d",&op,&x,&y);
        if(op==1)
        {
            scanf("%lld",&k);
            add(x,k,0);
            add(y+1,-k,0);
            add(x,x*k,1);
            add(y+1,-(y+1)*k,1);
        }
        else
        {
            LL res=s[y]-s[x-1];
            res+=(y+1)*ask(y,0)-ask(y,1)-(x*ask(x-1,0)-ask(x-1,1));
            printf("%lld\n",res);
        }
    }
    return 0;
}

线段树(懒标记)

  • 时间复杂度:\(O(mlogn)\)

代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
using LL=long long;
struct seg
{
    int l,r;
    LL sum,add;
    #define l(x) tr[x].l
    #define r(x) tr[x].r
    #define sum(x) tr[x].sum
    #define add(x) tr[x].add
}tr[N*4];
int n,m;
int a[N];
void build(int p,int l,int r)
{
    l(p)=l,r(p)=r;
    if(l==r)sum(p)=a[l];
    else
    {
        int mid=l+r>>1;
        build(p*2,l,mid),build(p*2+1,mid+1,r);
        sum(p)=sum(p*2)+sum(p*2+1);
    }
}
void spread(int p)
{
    if(add(p))
    {
        sum(p*2)+=add(p)*(r(p*2)-l(p*2)+1);
        sum(p*2+1)+=add(p)*(r(p*2+1)-l(p*2+1)+1);
        add(p*2)+=add(p);
        add(p*2+1)+=add(p);
        add(p)=0;
    }
}
void change(int p,int l,int r,int d)
{
    if(l<=l(p)&&r(p)<=r)
    {
        sum(p)+=1ll*d*(r(p)-l(p)+1);
        add(p)+=d;
        return ;
    }
    spread(p);
    int mid=l(p)+r(p)>>1;
    if(l<=mid)change(p*2,l,r,d);
    if(r>mid)change(p*2+1,l,r,d);
    sum(p)=sum(p*2)+sum(p*2+1);
}
LL ask(int p,int l,int r)
{
    if(l<=l(p)&&r(p)<=r)
        return sum(p);
    spread(p);
    LL res=0;
    int mid=l(p)+r(p)>>1;
    if(l<=mid)res+=ask(p*2,l,r);
    if(r>mid)res+=ask(p*2+1,l,r);
    return res;

}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    build(1,1,n);
    while(m--)
    {
        int op,x,y;
        LL k;
        scanf("%d%d%d",&op,&x,&y);
        if(op==1)
        {
            scanf("%lld",&k);
            change(1,x,y,k);
        }
        else
            printf("%lld\n",ask(1,x,y));
    }
    return 0;
}

P3373 【模板】线段树 2

题目描述

如题,已知一个数列,你需要进行下面三种操作:

  • 将某区间每一个数乘上 \(x\)

  • 将某区间每一个数加上 \(x\)

  • 求出某区间每一个数的和

输入格式

第一行包含三个整数 \(n,m,p\),分别表示该数列数字的个数、操作的总个数和模数。

第二行包含 \(n\) 个用空格分隔的整数,其中第 \(i\) 个数字表示数列第 \(i\) 项的初始值。

接下来 \(m\) 行每行包含若干个整数,表示一个操作,具体如下:

操作 \(1\): 格式:1 x y k 含义:将区间 \([x,y]\) 内每个数乘上 \(k\)

操作 \(2\): 格式:2 x y k 含义:将区间 \([x,y]\) 内每个数加上 \(k\)

操作 \(3\): 格式:3 x y 含义:输出区间 \([x,y]\) 内每个数的和对 \(p\) 取模所得的结果

输出格式

输出包含若干行整数,即为所有操作 \(3\) 的结果。

输入

5 5 38
1 5 4 2 3
2 1 4 1
3 2 5
1 2 4 2
2 3 5 5
3 1 4

输出

17
2

说明/提示

【数据范围】

对于 \(30\%\) 的数据:\(n \le 8,m \le 10\)
对于 \(70\%\) 的数据:\(n \le 10^3 ,m \le 10^4\)
对于 \(100\%\) 的数据:\(n \le 10^5,m \le 10^5\)

除样例外,\(p = 571373\)

解题思路

本题的关键在于加法和乘法的顺序问题:

  1. 加法优先级低,不会对前面的操作有影响
  2. 乘法优先级高,对前面的操作有影响,有三种影响:\(sum\)、懒标记 \(mul\)\(add\)

所以,采取先乘后加的方式

  • 时间复杂度:\(O(mlogn)\)

代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
using LL=long long;
struct seg
{
    int l,r;
    LL sum,add,mul;
    #define l(x) tr[x].l
    #define r(x) tr[x].r
    #define add(x) tr[x].add
    #define mul(x) tr[x].mul
    #define sum(x) tr[x].sum
}tr[N*4];
int n,m,mod;
int a[N];
void build(int p,int l,int r)
{
    l(p)=l,r(p)=r;
    mul(p)=1;
    if(l==r)
    {
        sum(p)=a[l]%mod;
        return ;
    }
    int mid=l+r>>1;
    build(p*2,l,mid),build(p*2+1,mid+1,r);
    sum(p)=(sum(p*2)+sum(p*2+1))%mod;
}
void spread(int p)
{
    sum(p*2)=(sum(p*2)*mul(p)+add(p)*(r(p*2)-l(p*2)+1))%mod;
    sum(p*2+1)=(sum(p*2+1)*mul(p)+add(p)*(r(p*2+1)-l(p*2+1)+1))%mod;
    add(p*2)=(add(p*2)*mul(p)+add(p))%mod;
    add(p*2+1)=(add(p*2+1)*mul(p)+add(p))%mod;
    mul(p*2)=mul(p*2)*mul(p)%mod;
    mul(p*2+1)=mul(p*2+1)*mul(p)%mod;
    add(p)=0;
    mul(p)=1;
}
void change1(int p,int l,int r,int d)
{
    if(l<=l(p)&&r(p)<=r)
    {
        add(p)=add(p)*d%mod;
        mul(p)=mul(p)*d%mod;
        sum(p)=(sum(p)*d)%mod;
        return ;
    }
    spread(p);
    int mid=l(p)+r(p)>>1;
    if(l<=mid)change1(p*2,l,r,d);
    if(r>mid)change1(p*2+1,l,r,d);
    sum(p)=(sum(p*2)+sum(p*2+1))%mod;
}
void change2(int p,int l,int r,int d)
{
    if(l<=l(p)&&r(p)<=r)
    {
        add(p)=(add(p)+d)%mod;
        sum(p)=(sum(p)+(r(p)-l(p)+1)*d)%mod;
        return ;
    }
    spread(p);
    int mid=l(p)+r(p)>>1;
    if(l<=mid)change2(p*2,l,r,d);
    if(r>mid)change2(p*2+1,l,r,d);
    sum(p)=(sum(p*2)+sum(p*2+1))%mod;
}
int ask(int p,int l,int r)
{
    if(l<=l(p)&&r(p)<=r)return sum(p);
    spread(p);
    int res=0;
    int mid=l(p)+r(p)>>1;
    if(l<=mid)res+=ask(p*2,l,r)%mod;
    if(r>mid)res+=ask(p*2+1,l,r);
    return res%mod;
}
int main()
{
    scanf("%d%d%d",&n,&m,&mod);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    build(1,1,n);
    while(m--)
    {
        int op,x,y,k;
        scanf("%d%d%d",&op,&x,&y);
        if(op==1)
        {
            scanf("%d",&k);
            change1(1,x,y,k);
        }
        else if(op==2)
        {
            scanf("%d",&k);
            change2(1,x,y,k);
        }
        else
            printf("%d\n",ask(1,x,y));
    }
    return 0;
}

P6242 【模板】线段树 3

题目描述

给出一个长度为 \(n\) 的数列 \(A\),同时定义一个辅助数组 \(B\)\(B\) 开始与 \(A\) 完全相同。接下来进行了 \(m\) 次操作,操作有五种类型,按以下格式给出:

1 l r k:对于所有的 \(i\in[l,r]\)],将 \(A_i\) 加上 \(k\)\(k\) 可以为负数)。
2 l r v:对于所有的 \(i\in[l,r]\),将 \(A_i\) 变成 \(\min(A_i,v)\)
3 l r:求 \(\sum_{i=l}^{r}A_i\)
4 l r:对于所有的 \(i\in[l,r]\),求 \(A_i\) 的最大值。
5 l r:对于所有的 \(i\in[l,r]\),求 \(B_i\) 的最大值。
在每一次操作后,我们都进行一次更新,让 \(B_i\gets\max(B_i,A_i)\)

输入格式

第一行包含两个正整数 \(n,m\),分别表示数列 \(A\) 的长度和操作次数。

第二行包含 \(n\) 个整数 \(A_1,A_2,\cdots,A_n\) ,表示数列 \(A\)

接下来 \(m\) 行,每行行首有一个整数 \(op\),表示操作类型;接下来两个或三个整数表示操作参数,格式见【题目描述】。

输出格式

对于 \(op\in\{3,4,5\}\) 的操作,输出一行包含一个整数,表示这个询问的答案。

输入

5 6
1 2 3 4 5
3 2 5
1 1 3 3
4 2 4
2 3 4 1
5 1 5
3 1 4

输出

14
6
6
11

解题思路

代码


posted @ 2021-09-22 18:28  zyy2001  阅读(40)  评论(0编辑  收藏  举报