线段树笔记

概括

线段树是一种高级数据结构,和树状数组一样,被用来处理区间查询,修改问题,并且线段树的最大优点是对动态数据的处理十分高效。线段树是算法竞赛中常用的用来维护区间信息的数据结构。

性质

  • 线段树是一颗平衡二叉树,一个结点左右孩子的结点高度差值不超过 1
  • 线段树每个结点表示一段连续区间的信息(最值,和,区间的端点等),维护的信息应该为题中要求的信息
  • 结点与结点之间只存在两种关系,结点所代表的区间完全包含或者不相交,不存在部分包含
  • 一个结点所代表的区间包含所有的子孙结点信息,一个结点的信息可以由其子结点来进行维护
  • 对于线段树的操作时间复杂度为 log ⁡ n \log n logn

应用

线段树可以用来动态维护区间信息。如果要对区间进性操作,首先找到组成这个大区间的小区间,然后再小区间上进性操作,小区间信息更新完成之后,由小区间更新大区间的信息。

模板

全部以求和为例

结构体

struct op { //结构体保存每个结点信息
      int left, right, sum;
} tree[400100];

update

void updata(int p) { //由子结点信息更新父结点信息

 tree[p].left = tree[2 * p].left;
 tree[p].right = tree[2 * p + 1].right;
 tree[p].sum = tree[2 * p].sum + tree[2 * p + 1].sum;
}

单点修改

void change(int p, int x, int y) {
    if (x == tree[p].left && x == tree[p].right) {
        a[x] += y;
        tree[p].sum += y;
        return;
    }
    if (x <= tree[2 * p].right)
        change(2 * p, x, y);
    if (x >= tree[2 * p + 1].left)
        change(2 * p + 1, x, y);
    updata(p);
}

区间查询

int search(int p, int l, int r) {
    int s = 0;
    if (l == tree[p].left && r == tree[p].right)
        return tree[p].sum;
    if (r <= tree[2 * p].right)
        s += search(2 * p, l, r);
    else if (l >= tree[2 * p + 1].left)
        s += search(2 * p + 1, l, r);
    else {
        s += search(2 * p, l, tree[2 * p].right);
        s += search(2 * p + 1, tree[2 * p + 1].left, r);
    }
    return s;
}

区间修改

void change2(ll p, ll l, ll r, ll x) {  //区间修改
    if (l <= tree[p].left && tree[p].right <= r) {
        tree[p].lazy += x;  //修改区间的懒标记
        tree[p].sum += x * (tree[p].right - tree[p].left + 1);  //修改区间的sum值
        return;
    }
    if (tree[p].lazy != 0)
        pushdown(p);  //如果当前区间存在懒标记,向下更新
    if (tree[2 * p].right >= r)
        change2(2 * p, l, r, x);
    else if (tree[2 * p + 1].left <= l)
        change2(2 * p + 1, l, r, x);
    else {
        change2(2 * p, l, tree[2 * p].right, x);
        change2(2 * p + 1, tree[2 * p + 1].left, r, x);
    }
    updata(p);  //向上更新
}
void pushdown(ll p) {  //向下更新,由lazy_tag修改子结点信息
    tree[2 * p].lazy += tree[p].lazy;
    tree[2 * p + 1].lazy += tree[p].lazy;
    tree[2 * p].sum += tree[p].lazy * (tree[2 * p].right - tree[2 * p].left + 1);
    tree[2 * p + 1].sum += tree[p].lazy * (tree[2 * p + 1].right - tree[2 * p + 1].left + 1);
    tree[p].lazy = 0;  //向下更新完之后要清空当前结点的懒标记
}

优化

  • 在叶子节点处无需下放懒惰标记,所以懒惰标记可以不下传到叶子节点。
  • 下放懒惰标记可以写一个专门的函数pushdown,从儿子节点更新当前节点也可以写一个专门的函数maintain(或者对称地用pushup),降低代码编写难度。
  • 标记永久化:如果确定懒惰标记不会在中途被加到溢出(即超过了该类型数据所能表示的最大范围),那么就可以将标记永久化。标记永久化可以避免下传懒惰标记,只需在进行询问时把标记的影响加到答案当中,从而降低程序常数。具体如何处理与题目特性相关,需结合题目来写。这也是树套树和可持久化数据结构中会用到的一种技巧。
    (来源oi-wiki)

例题

扶苏的问题

题目&题解

题面

题目描述

给定一个长度为 n n n 的序列 a a a,要求支持如下三个操作:

  1. 给定区间 [ l , r ] [l, r] [l,r],将区间内每个数都修改为 x x x
  2. 给定区间 [ l , r ] [l, r] [l,r],将区间内每个数都加上 x x x
  3. 给定区间 [ l , r ] [l, r] [l,r],求区间内的最大值。
输入格式

第一行是两个整数,依次表示序列的长度 n n n 和操作的个数 q q q
第二行有 n n n 个整数,第 i i i 个整数表示序列中的第 i i i 个数 a i a_i ai
接下来 q q q 行,每行表示一个操作。每行首先有一个整数 o p op op,表示操作的类型。

  • o p = 1 op = 1 op=1,则接下来有三个整数 l , r , x l, r, x l,r,x,表示将区间 [ l , r ] [l, r] [l,r] 内的每个数都修改为 x x x
  • o p = 2 op = 2 op=2,则接下来有三个整数 l , r , x l, r, x l,r,x,表示将区间 [ l , r ] [l, r] [l,r] 内的每个数都加上 x x x
  • o p = 3 op = 3 op=3,则接下来有两个整数 l , r l, r l,r,表示查询区间 [ l , r ] [l, r] [l,r] 内的最大值。
输出格式

对于每个 o p = 3 op = 3 op=3 的操作,输出一行一个整数表示答案。

样例 #1
样例输入 #1
6 6
1 1 4 5 1 4
1 1 2 6
2 3 4 2
3 1 4
3 2 3
1 1 6 -1
3 1 6
样例输出 #1
7
6
-1
样例 #2
样例输入 #2
4 4
10 4 -3 -7
1 1 3 0
2 3 4 -4
1 2 4 -9
3 1 4
样例输出 #2
0
提示
数据规模与约定
  • 对于 10 % 10\% 10% 的数据, n = q = 1 n = q = 1 n=q=1
  • 对于 40 % 40\% 40% 的数据, n , q ≤ 1 0 3 n, q \leq 10^3 n,q103
  • 对于 50 % 50\% 50% 的数据, 0 ≤ a i , x ≤ 1 0 4 0 \leq a_i, x \leq 10^4 0ai,x104
  • 对于 60 % 60\% 60% 的数据, o p ≠ 1 op \neq 1 op=1
  • 对于 90 % 90\% 90% 的数据, n , q ≤ 1 0 5 n, q \leq 10^5 n,q105
  • 对于 100 % 100\% 100% 的数据, 1 ≤ n , q ≤ 1 0 6 1 \leq n, q \leq 10^6 1n,q106 1 ≤ l , r ≤ n 1 \leq l, r \leq n 1l,rn o p ∈ { 1 , 2 , 3 } op \in \{1, 2, 3\} op{1,2,3} ∣ a i ∣ , ∣ x ∣ ≤ 1 0 9 |a_i|, |x| \leq 10^9 ai,x109
提示

请注意大量数据读入对程序效率造成的影响。

思路

使用线段树,其实它操作对应的是区间修改和区间查询。

首先对于修改又分为两种,一种是增加值,而另一种是直接修改。我们可以用三个懒标记,lazy_yao 来判断当前是否需要直接修改,lazy1 表示直接修改多少,lazy 表示加上多少。所以 pushdown 如下:

void pushdown(int p){
    if(tree[p].lazy_yao==1){
        tree[left_son].lazy1=tree[p].lazy1;
        tree[right_son].lazy1=tree[p].lazy1;
        tree[left_son].lazy_yao=1;
        tree[right_son].lazy_yao=1;
        // tree[p].lazy=0;
        tree[left_son].lazy=0;
        tree[right_son].lazy=0;
        tree[left_son].Max=tree[p].lazy1;
        tree[right_son].Max=tree[p].lazy1;
        tree[p].lazy1=0;
        tree[p].lazy_yao=0;
    }
    if(tree[p].lazy!=0){
        tree[left_son].lazy+=tree[p].lazy;
        tree[right_son].lazy+=tree[p].lazy;
        tree[left_son].Max+=tree[p].lazy;
        tree[right_son].Max+=tree[p].lazy;
        tree[p].lazy=0;
    }
}

其余的,我们根据题目做就行了!!!

Code

#include<bits/stdc++.h>
#define int long long int
#define left_son p*2
#define right_son (p*2)+1
using namespace std;
int n,m;
int a[10000005];
struct op{
    int Max,l,r,lazy,lazy1,lazy_yao;
}tree[10000005];
void build_tree(int p,int l,int r){
    if(l==r){
        tree[p].l=l,tree[p].r=r;
        tree[p].Max=a[l];
        return;
    }
    int mid=(l+r)>>1;
    build_tree(left_son,l,mid);
    build_tree(right_son,mid+1,r);
    tree[p].l=l,tree[p].r=r;
    tree[p].Max=max(tree[left_son].Max,tree[right_son].Max);
}
void pushdown(int p){
    if(tree[p].lazy_yao==1){
        tree[left_son].lazy1=tree[p].lazy1;
        tree[right_son].lazy1=tree[p].lazy1;
        tree[left_son].lazy_yao=1;
        tree[right_son].lazy_yao=1;
        // tree[p].lazy=0;
        tree[left_son].lazy=0;
        tree[right_son].lazy=0;
        tree[left_son].Max=tree[p].lazy1;
        tree[right_son].Max=tree[p].lazy1;
        tree[p].lazy1=0;
        tree[p].lazy_yao=0;
    }
    if(tree[p].lazy!=0){
        tree[left_son].lazy+=tree[p].lazy;
        tree[right_son].lazy+=tree[p].lazy;
        tree[left_son].Max+=tree[p].lazy;
        tree[right_son].Max+=tree[p].lazy;
        tree[p].lazy=0;
    }
}
void change(int p,int l,int r,int x){
    if(l<=tree[p].l&&tree[p].r<=r){
        tree[p].Max=x;
        tree[p].lazy=0;
        tree[p].lazy_yao=1;
        // if(tree[p].lazy1!=0) pushdown1(p);
        tree[p].lazy1=x;
        return;
    }
    pushdown(p);
    if(tree[left_son].r>=l)
		change(left_son,l,r,x);
	if(tree[right_son].l<=r)
		change(right_son,l,r,x);
    tree[p].Max=max(tree[left_son].Max,tree[right_son].Max);
}
void change1(int p,int l,int r,int x){
    if(l<=tree[p].l&&tree[p].r<=r){
        tree[p].Max+=x;
        tree[p].lazy+=x;
        return;
    }
    pushdown(p);
    if(tree[left_son].r>=l)
		change1(left_son,l,r,x);
	if(tree[right_son].l<=r)
		change1(right_son,l,r,x);
    tree[p].Max=max(tree[left_son].Max,tree[right_son].Max);
}
int ask(int p,int l,int r){
    if(l<=tree[p].l&&tree[p].r<=r){
        return tree[p].Max;
    }
    pushdown(p);
    int maxa=LONG_LONG_MIN;
    if(tree[left_son].r>=l)
		maxa=max(maxa,ask(left_son,l,r));
	if(tree[right_son].l<=r)
		maxa=max(maxa,ask(right_son,l,r));
    return maxa;
}
signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%lld",&a[i]);
    }
    build_tree(1,1,n);
    while(m--){
        int op;
        scanf("%lld",&op);
        if(op==1){
            int l,r,x;
            scanf("%lld%lld%lld",&l,&r,&x);
            change(1,l,r,x);
        }
        else if(op==2){
            int l,r,x;
            scanf("%lld%lld%lld",&l,&r,&x);
            change1(1,l,r,x);
        }
        else{
            int l,r;
            scanf("%lld%lld",&l,&r);
            printf("%lld\n",ask(1,l,r));
        }
    }
    return 0;
}

[JSOI2008] 最大数

题目&题解

题面

题目描述

现在请求你维护一个数列,要求提供以下两种操作:

1、 查询操作。

语法:Q L

功能:查询当前数列中末尾 L L L 个数中的最大的数,并输出这个数的值。

限制: L L L 不超过当前数列的长度。 ( L > 0 ) (L > 0) (L>0)

2、 插入操作。

语法:A n

功能:将 n n n 加上 t t t,其中 t t t 是最近一次查询操作的答案(如果还未执行过查询操作,则 t = 0 t=0 t=0),并将所得结果对一个固定的常数 D D D取模,将所得答案插入到数列的末尾。

限制: n n n 是整数(可能为负数)并且在长整范围内。

注意:初始时数列是空的,没有一个数。

输入格式

第一行两个整数, M M M D D D,其中 M M M 表示操作的个数, D D D 如上文中所述。

接下来的 M M M 行,每行一个字符串,描述一个具体的操作。语法如上文所述。

输出格式

对于每一个查询操作,你应该按照顺序依次输出结果,每个结果占一行。

样例 #1

样例输入 #1
5 100
A 96
Q 1
A 97
Q 1
Q 2
样例输出 #1
96
93
96

提示

数据规模与约定

对于全部的测试点,保证 1 ≤ M ≤ 2 × 1 0 5 1 \leq M \leq 2 \times 10^5 1M2×105 1 ≤ D ≤ 2 × 1 0 9 1 \leq D \leq 2 \times 10^9 1D2×109

思路

不难看出,这题可以使用线段树来做(用于维护区间最大值),我们先建一棵树,让它的所有叶子节点全部都为 0 0 0。当我们每次输入为 A 时,就更改下一个叶子节点,这就变成了单点修改。如果输入为 Q 时,就用区间查找的方法寻找末尾 L L L 个数中的最大的数。

Code

#include <bits/stdc++.h>
using namespace std;
#define int long long
int n, m;
int last;
struct op {
    int left, right, maxnum;
} tree[5000005];
inline int read() {
    int x = 0, f = 1;
    char ch = getchar();
    while (ch < '0' || ch > '9') {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') {
        x = x * 10 + ch - '0';
        ch = getchar();
    }
    return x * f;
}
inline void make_tree(int q, int l, int r) {//建树
    if (l == r) {
        tree[q].left = l;
        tree[q].right = r;
        tree[q].maxnum = 0;
        return;
    }
    int mid = (l + r) >> 1;
    make_tree(q * 2, l, mid);
    make_tree(q * 2 + 1, mid + 1, r);
    tree[q].left = l;
    tree[q].right = r;
    tree[q].maxnum = max(tree[q * 2].maxnum, tree[q * 2 + 1].maxnum);
}
inline int getmax(int q, int l, int r) {//区间查找
    if (l == tree[q].left && r == tree[q].right)
        return tree[q].maxnum;
    if (l >= tree[q * 2 + 1].left)
        return getmax(q * 2 + 1, l, r);
    else if (r <= tree[q * 2].right)
        return getmax(q * 2, l, r);
    else
        return max(getmax(q * 2, l, tree[q * 2].right), getmax(q * 2 + 1, tree[q * 2 + 1].left, r));
}
inline void change(int q, int x, int sum) {//单点修改
    if (tree[q].left == x && x == tree[q].right) {
        tree[q].maxnum += sum;
        return;
    }
    if (x >= tree[q * 2 + 1].left)
        change(q * 2 + 1, x, sum);
    else if (x <= tree[q * 2].right)
        change(q * 2, x, sum);
    tree[q].maxnum = max(tree[q * 2].maxnum, tree[q * 2 + 1].maxnum);
}
int www;
signed main() {
    n = read(), m = read();
    make_tree(1, 1, n);
    while (n--) {
        char aa = getchar();
        if (aa == 'A') {
            int xx = read();
            xx = (xx + last) % m;
            www++;
            change(1, www, xx);
        } else {
            int xx = read();
            last = getmax(1, www - xx + 1, www);
            printf("%lld\n", last);
        }
    }
    return 0;
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值