线段树

Posted xuan01

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线段树相关的知识,希望对你有一定的参考价值。

线段树--解决区间问题的数据结构,相比于树状数组,更具有普适性;

完全二叉树的性质:根节下标为1,,节点为 i 的节点,左子节点为2*i,右子节点为2*i+1;

代表nums中单个元素的节点tree[x]应当在树的最底层,即叶子节点;更大的区间从叶子节点开始向上构成;

代表区间【L,R】的节点 tree【i】,左子节点tree【2*i】表示区间【L,(L+R)/2】的区间和;右子节点tree【2*i+1】表示区间 【(L+R)/2 +1, R】的区间和;

初始化tree数组的大小 总是 令 其 为 4*n;类似于归并排序、快排的分治算法,将原问题不断的划分为左右子问题;

代码摘自:线段树从入门到急停 - 力扣(LeetCode)非常详细;

 核心:一个就是注意单点修改时,递归结束条件的判断,是查询单点的话,就是 左右边界相等终止;查询区间和的话, 判断当前区间是否落在 所求范围内,是的话就加上这个区间;

另一个就是拓展:区间和数组可以替换为区间最值问题;求区间最大值、区间最小值等;

再一个就是区间修改问题,有两种,一种是增量式修改,因为注意到我们的区间和,若不关注每个具体的值,只是区间和的大小,我们无需每次都向下更新到叶子节点,否则会达到O(N)时间复杂度;因此 需要增加一个 数组,判断当前增量是否已经向下更新,我们称之为懒惰标记;; 第二种就是 覆盖式修改:将区间的值都修改为同一值,此时,我们不能依据增量式修改的方法,因为可能修改为0,而向下更新的标记 也是检查是否为0,从而 会产生冲突,所以另起一个数组,判断是否已经向下覆盖;;

代码实现:(此处将 两种修改方法写在一起了,应用时分开两个类 写);

#include <bits/stdc++.h>
using namespace std;

class Segement_tree
private:
    vector<int>nums;
    vector<int>tree;
    vector<int>lazy;
    vector<int>is_updated;
    int n;
    void pushUp(int i)
        tree[i] = tree[2*i] + tree[2*i + 1];
    
    void build(int left, int right, int i)
        if(left == right)
            tree[i] = nums[left];
        
        int mid = (left + right) / 2;
        build(left, mid, 2*i);
        build(mid + 1, right, 2*i + 1);
        pushUp(i);
    
    /*单点修改*/
    void add(int index, int x, int left, int right, int i)
        if(left == right)
            tree[i] += x;
            return;
        
        int mid = (left + right)/2;
        if(index <= mid)
            add(index,x,left,mid,2*i);
        else
            add(index,x,mid+1,right,2*i+1);
        
        pushUp(i);
    
    void update(int index,int x,int left, int right,int i)
        if(left == right)
            tree[i] = x;
            return;
        
        int mid = (left + right) /2;
        if(index <= mid)
            update(index,x,left,mid,2*i);
        else
            update(index,x,mid+1,right,2*i+1);
        
        pushUp(i);
    
    int query(int index,int left,int right, int i)
        if(left == right) return tree[i];
        int mid = (left + right) /2;
        if(index <= mid)
            return query(index,left,mid,2*i);
        else
            return query(index,mid+1,right,2*i+1);
        
    
    /*区间求和*/
    int sum(int left, int right, int s, int t, int i)
        if(left <= s && t <= right) return tree[i];
        int mid = (s + t) /2;
        int res = 0;
        if(left <= mid)
            res += sum(left,right, s, mid, 2*i);
        
        if(right > mid)
            res += sum(left,right, mid+1, t, 2*i+1);
        
        return res;
    
    /*区间修改: 增量式 */
    void add(int left, int right, int x,int s, int t, int i)
        if(left <= s && t <= right)
            tree[i] += (t-s+1)*x;
            if(s != t) lazy[i] += x;
            return;
        
        int mid = (s + t)/2;
        if(lazy[i] != 0) pushDown(s,mid,t,i);
        if(left <= mid) add(left,right,x,s,mid,2*i);
        if(right > mid) add(left,right,x,mid+1,t,2*i+1);
        pushUp(i);
    
    void pushDown(int s,int mid, int t,int i)
        tree[2*i] += (mid-s+1)*lazy[i];
        lazy[2*i] += lazy[i];
        tree[2*i+1] += (t-mid)*lazy[i];
        lazy[2*i+1] += lazy[i];
        lazy[i] = 0;
    
    /*区间修改: 覆盖式 */
    void update(int left, int right,int x,int s,int t,int i)
        if(left <= s && t <= right)
            tree[i] = (t-s+1)*x;
            if(s != t)
                lazy[i] = x;
                is_updated[i] = true;//未推送
            
            return;
        
        int mid = (s+t)/2;
        if(is_updated[i]) pushDown1(s,mid,t,i);
        if(left <= mid) update(left,right,x,s,mid,2*i);
        if(right > mid) update(left,right,x,mid+1,t,2*i+1);
        pushUp(i);
    
    void pushDown1(int s,int mid,int t,int i)
        tree[2*i] = (mid - s + 1)* lazy[i];
        lazy[2*i] = lazy[i];
        is_updated[2*i] = true;
        tree[2*i+1] = (t - mid) * lazy[i];
        lazy[2*i+1] = lazy[i];
        is_updated[2*i+1] = true;
        is_updated[i] = false;
        lazy[i] = 0;
    
public:
    Segement_tree(vector<int>& nums)
        this->n = nums.size();
        this->nums = nums;
        this->tree.resize(4*n, 0);
        this->lazy.resize(4*n, 0);
        this->is_updated.resize(4*n, 0);
        build(0, n-1, 1);
    
    /*单点修改*/
    void add(int index, int x)
        add(index,x,0,n-1,1);
    
    void update(int index,int x)
        update(index,x,0,n-1,1);
    
    int query(int index)
        return query(index, 0, n-1,1);
    
    /*区间求和*/
    int sum(int left,int right)
        return sum(left,right,0, n-1,1);
    

    /*区间修改 : 增量式*/
    void add(int left,int right,int x)
        add(left,right,x,0,n-1,1);
    
    /*区间修改: 覆盖式 */
    void update(int left,int right, int x)
        update(left,right,x,0,n-1,1);
    
;

int main()

    system("pause");
    return 0;

 动态开点:需要增加一个addnode方法;

后续更新;

一般线段树与权值线段树

一般线段树与权值线段树

1.算法分析

  1. 一般还要开4N的数组
  2. 一般做单点修改、区间查询,加上懒标记后,可以做区间修改、区间查询

1.1 一般线段树

可以处理:区间加、区间乘、区间max/min、区间覆盖等问题

1.2 权值线段树

  1. 维护全局的值域信息,每个节点记录的是该值域的值出现的总次数。
  2. 使用二分的思想(离散化的时候,需要用到)
  3. 支持查询全局K小值,全局rank,前驱,后继等。
  4. 单词操作时间复杂度为O(logn)
  5. 空间复杂度为O(n)
  6. 相对于平衡树的优势:代码简单,速度快
  7. 劣势:值域较大时,我们需要离散化,变成离线数据结构

2.板子

2.1 线段树入门

2.1.1 单点修改+区间查询

// 该板子是求区间和
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
int const N = 5e5 + 10;
LL dat[N << 2];  // 4倍空间
int n, m, a[N];

// 上传操作
void pushup(int rt) { 
    dat[rt] = dat[rt << 1] + dat[rt << 1 | 1];
}

// 建树
void build (int rt, int l, int r) {
    if (l == r) {  // 如果当前到达叶节点,那么赋值
        dat[rt] = a[l];  // 赋值是a[l],表示那个叶节点
        return ;
    }
    int mid = (l + r) >> 1;  
    // 递归建立左右子树
    build(rt << 1, l, mid);  
    build(rt << 1 | 1, mid + 1, r);
    // 上传
    pushup(rt);
}

// 单点修改
void modify (int rt, int l, int r, int x, int y) {
    if (l == x && r == x) {  // 递归到叶节点且叶节点刚好为x节点
        dat[rt] += y;  // 修改
        return;
    }
    int mid = (l + r) >> 1;  
    if (x <= mid) modify(rt << 1, l, mid, x, y);  // 如果在左子树 
    else modify(rt << 1 | 1, mid + 1, r, x, y);  // 不在左子树,比在右子树
    pushup(rt);  // 上传
}

// 区间查询
LL query(int rt, int l, int r, int L, int R) {
    if (L <= l && r <= R) return dat[rt];  // 如果当前rt管辖的点能够被[L, R]完全包含,返回
    int mid = (l + r) >> 1;
    LL res = 0;
    if (L <= mid) res += query(rt << 1, l, mid, L, R);  // 如果和左子树有关
    if (mid < R) res += query(rt << 1 | 1, mid + 1, r, L, R);  // 如果可能和右子树有关
    return res;
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
    build(1, 1, n);  // 建树
    for (int i = 1, op, x, y; i <= m; ++i) {
        scanf("%d%d%d", &op, &x, &y);  
        if (op == 1) modify(1, 1, n, x, y);  // 单点修改a[x]+=y
        else printf("%lld
", query(1, 1, n, x, y));  // 区间查询,求[x, y]的区间和
    }
    return 0;
}

2.1.2 区间修改+区间查询

// 该板子是求区间和
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

int const N = 5e5 + 10;
LL dat[N << 2], lazy[N << 2];
int a[N], n, m;

// 上传标记,每次左右子树建树/区间修改完都需要上传
void pushup(int rt) {
    dat[rt] = dat[rt << 1] + dat[rt << 1 | 1]; 
}

// 建树
void build(int rt, int l, int r) {
    if (l == r) {  // 递归到叶节点
        dat[rt] = a[l];
        lazy[rt] = 0;
        return;
    }
    // 递归建立左右子树
    int mid = (l + r) >> 1;
    build(rt << 1, l, mid);  
    build(rt << 1 | 1, mid + 1, r);
    pushup(rt);  // 上传
}

// 下传,下传标记,同时改变dat数组
void pushdown(int rt, int l, int r) {
    if (lazy[rt]) {  // 如果有标记
        int mid = (l + r) >> 1;
        
        // 把标记给左右子树
        lazy[rt << 1] += lazy[rt];  
        lazy[rt << 1 | 1] += lazy[rt];
        
        // 改变dat
        dat[rt << 1] += (mid - l + 1) * lazy[rt];
        dat[rt << 1 | 1] += (r - mid) * lazy[rt];
        
        // rt标记清空
        lazy[rt] = 0;
    }
    return;
}

// 区间修改: [L, R] += x
void modify(int rt, int l, int r, int L, int R, int x) {
    if (L <= l && r <= R) {  // 如果当前区间被完全包含
        dat[rt] += (r - l + 1) * x;  // 修改当前区间的dat值
        lazy[rt] += x;  // 改变懒标记
        return ;
    }
    
    pushdown(rt, l, r);  // 下传
    // 递归左右子树修改区间
    int mid = (l + r) >> 1;
    if (L <= mid) modify(rt << 1, l, mid, L, R, x);
    if (mid < R) modify(rt << 1 | 1, mid + 1, r, L, R, x);
    pushup(rt);  // 上传
    return;
}

// 区间查询:获得[L, R]的区间和
LL query(int rt, int l, int r, int L, int R) {
    if (L <= l && r <= R) return dat[rt];  // 如果[l, r]被完全包含于[L, R]
    pushdown(rt, l, r);  // 标记下传
    // 递归加上左右子树
    int mid = (l + r) >> 1;
    LL res = 0;
    if (L <= mid) res += query(rt << 1, l, mid, L, R);
    if (mid < R) res += query(rt << 1 | 1, mid + 1, r, L, R);
    return res;
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);  // 读入数组
    build(1, 1, n);  // 建树
    for (int i = 1, a, b, x, op; i <= m; ++i) {
        scanf("%d", &op);
        if (op == 1) {
            scanf("%d%d%d", &a, &b, &x);
            modify(1, 1, n, a, b, x);  // 区间修改, [a, b] += x
        }
        else {
            scanf("%d%d", &a, &b);
            printf("%lld
", query(1, 1, n, a, b));  // 区间查询,查询[a, b]的区间和
        }
    }
    return 0;
}

2.1.3 区间加乘操作

// 加乘模板
// x点原来的乘、加法标记为:mul1、add1,后来要加上的乘、加法标记为:mul2、add2
// 可以证明先乘后加最优方法
// x的值变为: x.dat => (x.dat * mul2) + (x.r - x.l + 1) * add2;
// x的乘法标记变为: x.mul1 => x.mul1 * mul2
// x的加法标记变为: x.add1 => x.add1 * mul2 + add2
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
int const N = 1e5 + 10;
LL dat[N << 2], mul[N << 2], add[N << 2];
int n, p, a[N], m;

// 上传,根的值为左子树的值和右子树的值之和
void pushup(int rt) {
    dat[rt] = (dat[rt << 1] + dat[rt << 1 | 1]) % p;
}

// 建树
void build(int rt, int l, int r) {
    if (l == r) {  // 如果是叶子
        dat[rt] = a[l] % p;
        add[rt] = 0;
        mul[rt] = 1;
        return;
    }
    
    // 如果不是叶子,那么乘法标记必须为1,加法标记为0
    mul[rt] = 1;
    add[rt] = 0;
    int mid = (l + r) >> 1;
    build(rt << 1, l, mid), build(rt << 1 | 1, mid + 1, r);  // 建立左子树和右子树
    pushup(rt);
}

// 加成的结果
void eval(int rt, int l, int r, LL add2, LL mul2) {
    dat[rt] = ((dat[rt] * mul2 % p) + ((r - l + 1) % p) * add2 % p) % p;
    mul[rt] = mul[rt] * mul2 % p;
    add[rt] = (add[rt] * mul2 % p + add2) % p;
}

// 标记下移
void pushdown(int rt, int l, int r) {
    int mid = (l + r) >> 1;
    eval(rt << 1, l, mid, add[rt], mul[rt]), eval(rt << 1 | 1, mid + 1, r, add[rt], mul[rt]);  // 左右子树分别得到根的标记
    add[rt] = 0, mul[rt] = 1;  // 清空根的标记
    return;
}

// 区间修改
void modify(int rt, int l, int r, int L, int R, LL add2, LL mul2) {
    if (L <= l && r <= R) {  // 如果[L, R]在[l, r]内,直接修改
        eval(rt, l, r, add2, mul2);
        return;
    }
    pushdown(rt, l, r);  // 如果不在[l, r]内,那么分裂,首先要把标记下移
    int mid = (l + r) >> 1;
    if (L <= mid) modify(rt << 1, l, mid, L, R, add2, mul2);  // 修改左子树
    if (mid < R) modify(rt << 1 | 1, mid + 1, r, L, R, add2, mul2);  // 修改右子树
    pushup(rt);  // 修改完子树需要把标记上移
    return;
}

// 询问区间和[L, R]
LL query(int rt, int l, int r, int L, int R) {
    if (L <= l && r <= R) return dat[rt] % p;
    pushdown(rt, l, r);  // 如果[L, R]不在[l, r]内,那么需要分裂,首先要把标记下移
    LL res = 0;
    int mid = (l + r) >> 1;
    if (L <= mid) res = query(rt << 1, l, mid, L, R) % p;  // 左子树
    if (mid < R) res = (res + query(rt << 1 | 1, mid + 1, r, L, R) % p) % p;  // 右子树
    return res;
}

int main() {
    cin >> n >> p;  // 输入数字的个数和模数
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);  // 输入数字
    build(1, 1, n);  // 建树
    cin >> m;  // 输入操作数
    for (int i = 1, op, t, g, c; i <= m; ++i) {  // 输入每次的具体操作
        scanf("%d", &op);
        if (op == 1) {  // 区间乘
            scanf("%d%d%d", &t, &g, &c);
            modify(1, 1, n, t, g, 0, c);
        }
        else if (op == 2) {  // 区间加
            scanf("%d%d%d", &t, &g, &c);
            modify(1, 1, n, t, g, c, 1);
        }
        else {  // 询问区间和
            scanf("%d%d", &t, &g);
            cout << query(1, 1, n, t, g) << endl;
        }
    }
    return 0;
}

2.1.4 区间染色

/*本题是区间覆盖问题,求指定区间内有多少的颜色数目,因为颜色的数目比较少,因此
可以使用一个int整数来表示所有的颜色数目,而后就是线段树的常规操作*/
#include <bits/stdc++.h>

using namespace std;

int const N = 1e5 + 10;
typedef long long LL;

LL add[N << 2], sum[N << 2];  // add为记录颜色的懒标记,sum为当前区间的颜色

// 向下传递操作
void pushup(int u) {
    sum[u] = sum[u << 1] | sum[u << 1 | 1];  // 当前颜色由子区间颜色得到
}

// 向上传递操作
void pushdown(int u) {
    if (add[u]) {  // 如果当前u节点有颜色的话

        // 给左右子节点标记都赋值
        add[u << 1] = add[u];  
        add[u << 1 | 1] = add[u];

        // 给左右节点的sum赋值,记录颜色
        sum[u << 1] = add[u];
        sum[u << 1 | 1] = add[u];

        // 去掉懒标记
        add[u] = 0;
    }
}

// 建树
void build(int u, int l, int r) {
    add[u] = 0;  // 初始每个节点都没有懒标记
    if (l == r) {  // 如果递归到叶节点
        sum[u] = 1;  // 叶节点的颜色赋值
        return;
    }
    int mid = l + r >> 1;
    build(u << 1, l, mid);  // 建立左右子树
    build(u << 1| 1, mid + 1, r);
    pushup(u);  // 标记上传
}

// 区间赋值操作
void modify(int u, int l, int r, int c, int L, int R) {
    if (L <= l && r <= R) {  // 如果[l, r]完全被包含在要赋值的区间[L, R]的话,那么直接修改
        add[u] = 1 << (c - 1);
        sum[u] = 1 << (c - 1);
        return;
    }
    pushdown(u);  // 下传标记,因为因为标记要分裂
    int mid = l + r >> 1;
    if (L <= mid) modify(u << 1, l, mid, c, L, R);  // 递归修改左右子树
    if (mid < R) modify(u << 1 | 1, mid + 1, r, c, L, R);
    pushup(u);  // 上传操作
}

// 区间查询多少个颜色
LL query(int u, int l, int r, int L, int R) {
    if (L <= l && r <= R) return sum[u];  // 如果[l, r]完全被包含在要赋值的区间[L, R]的话,那么返回
    pushdown(u);  // 标记下移
    int mid = l + r >> 1;
    LL res = 0;

    // 递归查询左右子树
    if (L <= mid) res |= query(u << 1, l, mid, L, R);
    if (mid < R) res |= query(u << 1 | 1, mid + 1, r, L, R);
    return res;
}

int main() {
    int L, T, O, a, b, c; 
    cin >> L >> T >> O;  // 读入节点数、颜色总数、操作数
    build(1, 1, L);
    while (O--) {
        char op[2];
        scanf("%s", op);  // 读入操作类型
        if (op[0] == ‘P‘) {
            scanf("%d %d", &a, &b);
            if (a > b) swap(a, b);  // 保证a要比b小
            LL ans = query(1, 1, L, a, b);  // 查询a到b的颜色总数,颜色总数用一个int型数表示
            LL res = 0;  
            while (ans) {  // 记录这个int型数有多少个1
                if (ans & 1) res++;
                ans >>= 1;
            }
            printf("%lld
", res);
        }
        else {
            scanf("%d%d%d", &a, &b, &c);  // 读入[a, b]和修改为的值
            if (a > b) swap(a, b);
            modify(1, 1, L, c, a, b);  // 修改操作
        }
    }
    return 0;
}

2.2 权值线段树

2.2.1 求第k大、前驱、后继等

/*
本题由于一开始dat维护的全为0,所以不需要建树的操作。dat维护每个数出现的次数
数据较大,需要先离散化,然后在每个离散化后的数字上建立线段树维护每个数出现的次数。
1. 插入数值x:x的次数加一
2. 删除数值x(若有多个相同的数,应只删除一个):x的次数减一
3. 查询数值x的排名(若有多个相同的数,应输出最小的排名):区间查询[l, x - 1]的次数,然后加一
4. 查询排名为x的数值:看x是否小于等于左子树的次数,如果小于在左子树;否则就算右子树的k-左子树次数
5. 求数值x的前驱(前驱定义为小于x的最大的数):求出x的排名t,然后查询排名为t-1的数
6. 求数值x的后继(后继定义为大于x的最小的数):求出x的排名t,然后查询排名为t+1的数
*/
#include <bits/stdc++.h>

using namespace std;

const int N=100005;

int num[N];
struct A{
    int opt, x;
}q[N];
int dat[N << 2];
 
void pushup(int rt){
    dat[rt] = dat[rt << 1] + dat[rt << 1 | 1];
}
 
// 单点修改
void modify(int rt, int l, int r, int p, int c){
    if(l == r){
        dat[rt] += c;
        return;
    }
    int mid = (l + r) >> 1;
    if(p <= mid) modify(rt << 1, l, mid, p, c);
    else modify(rt << 1 | 1, mid + 1, r, p, c);
    pushup(rt);
}
 
// 区间查询
int query1(int rt, int l, int r, int L, int R){//区间求和
    if (L <= l && r <= R) return dat[rt];
    int mid = (l + r) >> 1;
    int res = 0;
    if(L <= mid) res += query1(rt << 1, l, mid, L, R);
    if (mid < R) res += query1(rt << 1 | 1, mid + 1, r, L, R);
    return res;
}

// 查询排名为k的数
int query2(int rt, int l, int r, int k)
{
    if(l == r) return l;
    int mid = (l + r) >> 1;
    if(k <= dat[rt << 1]) return query2(rt << 1, l, mid, k);
    else return query2(rt << 1 | 1, mid + 1, r, k-dat[rt<<1]);
}
 
int main(){
    int m, k=0;
    scanf("%d", &m);
    for(int i = 0; i < m; i++){
        scanf("%d%d", &q[i].opt, &q[i].x);
        if(q[i].opt != 4) num[k++] = q[i].x;
    }
    sort(num, num+k);
    int n = unique(num, num+k) - num;
 
    for(int i = 0; i < m; i++){
        int x = lower_bound(num, num+n, q[i].x) - num + 1;
        if(q[i].opt == 1){//插入
            modify(1, 1, n, x, 1);
        }
        if(q[i].opt == 2){//删除
            modify(1, 1, n, x, -1);
        }
        if(q[i].opt == 3){//查询x的排名
            if(x - 1 == 0) printf("1
");
            else printf("%d
", query1(1, 1, n, 1, x - 1) + 1);
        }
        if(q[i].opt == 4){//查询排名为x的数
            printf("%d
", num[query2(1, 1, n, q[i].x) - 1]);
        }
        if(q[i].opt == 5){//求小于x的最大的数的值
            int rk = query1(1, 1, n, 1, x - 1);
            printf("%d
", num[query2(1, 1, n, rk) - 1]);
        }
        if(q[i].opt == 6){//求大于x的最小的数的值
            int sum = query1(1, 1, n, 1, x);
            printf("%d
", num[query2(1, 1, n, sum + 1) - 1]);
        }
    }
    return 0;
}

3. 例题

3.1 线段树入门

luogu P1047 校门外的树
题意: 有一个数轴,长度为l+1,从0~l上每个点都种树。现在有m个操作,每个操作输入a和b,表示要把[a, b]上的树砍掉,问m次操作后,数轴上还剩下多少棵树?
题解: 只需要改区间修改+区间查询的板子即可,当砍掉[a,b]上的树时,就算把[a, b]赋值为0,最后统计还剩多少棵树,就算计算[1,n]的区间求和。
代码:

// 该板子是求区间和
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

int const N = 5e5 + 10;
LL dat[N << 2], lazy[N << 2];
int a[N], n, m;

// 上传标记,每次左右子树建树/区间修改完都需要上传
void pushup(int rt) {
    dat[rt] = dat[rt << 1] + dat[rt << 1 | 1]; 
}

// 建树
void build(int rt, int l, int r) {
    if (l == r) {  // 递归到叶节点
        dat[rt] = a[l];
        lazy[rt] = 1;
        return;
    }
    lazy[rt] = 1;
    // 递归建立左右子树
    int mid = (l + r) >> 1;
    build(rt << 1, l, mid);  
    build(rt << 1 | 1, mid + 1, r);
    pushup(rt);  // 上传
}

// 下传,下传标记,同时改变dat数组
void pushdown(int rt, int l, int r) {
    if (lazy[rt] == 0) {  // 如果有标记
        
        // 把标记给左右子树
        lazy[rt << 1] = 0;  
        lazy[rt << 1 | 1] = 0;
        
        // 改变dat
        dat[rt << 1] = 0 ;
        dat[rt << 1 | 1] = 0;
        
        // rt标记清空
        lazy[rt] = 1;
    }
    return;
}

// 区间修改: [L, R] += x
void modify(int rt, int l, int r, int L, int R) {
    if (L <= l && r <= R) {  // 如果当前区间被完全包含
        dat[rt] = 0;  // 修改当前区间的dat值
        lazy[rt] = 0;  // 改变懒标记
        return ;
    }
    
    if (lazy[rt] == 0) pushdown(rt, l, r);  // 下传
    // 递归左右子树修改区间
    int mid = (l + r) >> 1;
    if (L <= mid) modify(rt << 1, l, mid, L, R);
    if (mid < R) modify(rt << 1 | 1, mid + 1, r, L, R);
    pushup(rt);  // 上传
    return;
}

// 区间查询:获得[L, R]的区间和
LL query(int rt, int l, int r, int L, int R) {
    if (L <= l && r <= R) return dat[rt];  // 如果[l, r]被完全包含于[L, R]
    if (lazy[rt] == 0) pushdown(rt, l, r);  // 标记下传
    // 递归加上左右子树
    int mid = (l + r) >> 1;
    LL res = 0;
    if (L <= mid) res += query(rt << 1, l, mid, L, R);
    if (mid < R) res += query(rt << 1 | 1, mid + 1, r, L, R);
    return res;
}

int main() {
    cin >> n >> m;
    n ++;
    for (int i = 1; i <= n; ++i) a[i] = 1;
    build(1, 1, n);  // 建树
    // cout << query(1, 1, n, 1, n) << endl;
    for (int i = 1, a, b; i <= m; ++i) {
        scanf("%d%d", &a, &b);
        a++, b++;
        modify(1, 1, n, a, b);
    }
    cout << query(1, 1, n, 1, n) << endl;
    return 0;
}

luogu P5057 [CQOI2006]简单题
题意: 有一个 n 个元素的数组,每个元素初始均为 0。有 m 条指令,要么让其中一段连续序列数字反转——0 变 1,1 变 0(操作 1),要么询问某个元素的值(操作 2)。 1 ≤ n ≤ 10^5^, 1 ≤ m ≤ 5 × 10^5^
题解: 线段树维护,每次给定反转区间[a, b],那么把[a, b]区间中每个数字加1,而后每次询问x的时候,只需要query(1,1,n,x,x),而后判断这个值是奇数还是偶数,奇数输出1,偶数输出0即可
代码:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

int const N = 1e5 + 10;
LL dat[N << 2], lazy[N << 2];
int n, m;

// 上传标记,每次左右子树建树/区间修改完都需要上传
void pushup(int rt) {
    dat[rt] = dat[rt << 1] + dat[rt << 1 | 1]; 
}

// 建树
void build(int rt, int l, int r) {
    if (l == r) {  // 递归到叶节点
        dat[rt] = 0;
        lazy[rt] = 0;
        return;
    }
    // 递归建立左右子树
    int mid = (l + r) >> 1;
    build(rt << 1, l, mid);  
    build(rt << 1 | 1, mid + 1, r);
    pushup(rt);  // 上传
}

// 下传,下传标记,同时改变dat数组
void pushdown(int rt, int l, int r) {
    if (lazy[rt]) {  // 如果有标记
        int mid = (l + r) >> 1;
        
        // 把标记给左右子树
        lazy[rt << 1] += lazy[rt];  
        lazy[rt << 1 | 1] += lazy[rt];
        
        // 改变dat
        dat[rt << 1] += (mid - l + 1) * lazy[rt];
        dat[rt << 1 | 1] += (r - mid) * lazy[rt];
        
        // rt标记清空
        lazy[rt] = 0;
    }
    return;
}

// 区间修改: [L, R] += x
void modify(int rt, int l, int r, int L, int R, int x) {
    if (L <= l && r <= R) {  // 如果当前区间被完全包含
        dat[rt] += (r - l + 1) * x;  // 修改当前区间的dat值
        lazy[rt] += x;  // 改变懒标记
        return ;
    }
    
    pushdown(rt, l, r);  // 下传
    // 递归左右子树修改区间
    int mid = (l + r) >> 1;
    if (L <= mid) modify(rt << 1, l, mid, L, R, x);
    if (mid < R) modify(rt << 1 | 1, mid + 1, r, L, R, x);
    pushup(rt);  // 上传
    return;
}

// 区间查询:获得[L, R]的区间和
LL query(int rt, int l, int r, int L, int R) {
    if (L <= l && r <= R) return dat[rt];  // 如果[l, r]被完全包含于[L, R]
    pushdown(rt, l, r);  // 标记下传
    // 递归加上左右子树
    int mid = (l + r) >> 1;
    LL res = 0;
    if (L <= mid) res += query(rt << 1, l, mid, L, R);
    if (mid < R) res += query(rt << 1 | 1, mid + 1, r, L, R);
    return res;
}

int main() {
    cin >> n >> m;
    build(1, 1, n);  // 建树
    for (int i = 1, a, b, x, op; i <= m; ++i) {
        scanf("%d", &op);
        if (op == 1) {
            scanf("%d%d", &a, &b);
            modify(1, 1, n, a, b, 1);  // 区间修改, [a, b] += 1
        }
        else {
            scanf("%d", &a);
            printf("%lld
", (query(1, 1, n, a, a) & 1) == 1);  // 区间查询,查询[a, b]的区间和
        }
    }
    return 0;
}

luogu P4588 [TJOI2018]数学计算
题意: 小豆现在有一个数x,初始值为1.小豆有Q次操作,操作有两种类型:
1 m: x = x * m, 输出x%mod;
2 pos:x= x = x / 第pos次操作所乘的数(保证第pos次操作一定为类型1,对于每一个类型1的操作至多会被除一次)输出x % mod;Q <= 10^5^
题解: 使用线段树维护1~Q这Q个数字的区间乘,如果当前是1类型操作,那么进行单点修改modify(1, 1, n, i, x);如果是2类型操作,那么进行单点修改modify(1, 1, n, pos, 1); 每次输出都是所有的成绩, 即dat[1];
代码:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
int const N = 1e5 + 10;
LL dat[N << 2];
int n, p, t;

void pushup(int rt) {
    dat[rt] = dat[rt << 1] * dat[rt << 1 | 1] % p;
}

void build(int rt, int l, int r) {
    if (l == r) {
        dat[rt] = 1;
        return;
    }
    int mid = (l + r) >> 1;
    build(rt << 1, l, mid), build(rt << 1 | 1, mid + 1, r);
    pushup(rt);
}

void modify(int rt, int l, int r, int x, int y) {
    if (l == r && l == x) {
        dat[rt] = y;
        return;
    }
    int mid = (l + r) >> 1;
    if (x <= mid) modify(rt << 1, l, mid, x, y);
    else modify(rt << 1 | 1, mid + 1, r, x, y);
    pushup(rt);
}

int main() {
    cin >> t;
    while (t--) {
        cin >> n >> p;
        for (int i = 1; i <= n * 4; ++i) dat[i] = 0;
        build(1, 1, n);
        for (int i = 1, op, x; i <= n; ++i) {
            scanf("%d%d", &op, &x);
            if (op == 1) modify(1, 1, n, i, x % p);
            else modify(1, 1, n, x, 1);
            printf("%lld
", dat[1] % p);
        }
    }
    return 0;
}

3.2 权值线段树

luogu P1908 逆序对
题意: 求出一个数列的逆序对.数列长度n ≤ 5×10^5^
题解: 权值线段树维护每个数字出现的次数,然后每个数字x出现的时候只需要区间查询[1, x - 1]的出现次数即可
代码:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
int const N = 500050;

int n;
LL a[N], b[N];
int dat[N << 2];

LL ans = 0;

void modify(int rt, int l, int r, int x)
{
	if(l == r)
	{
		dat[rt]++;
		return;
	}
	int mid = (l + r) >> 1;
	if(x <= mid) modify(rt << 1, l, mid, x);
	else modify(rt << 1 | 1, mid + 1, r, x);
	dat[rt] = dat[rt << 1] + dat[rt << 1 | 1];
}

int query(int rt,int l,int r,int L,int R)
{
	if(L <= l && r <= R) return dat[rt];
	int mid = (l + r) >> 1;
	int res = 0;
	if(L <= mid) res += query(rt << 1, l, mid, L, R);
	if(mid < R) res += query(rt << 1 | 1, mid + 1, r, L, R);
	return res;
}

int main()
{
	cin >> n;
	for(int i = 1; i <= n; i++)
	{
	    cin >> a[i];
	    b[i] = a[i];
	}
	sort(b + 1, b + 1 + n);
	int len = unique(b + 1, b + n + 1) - b - 1;
	for(int i = 1; i <= n; i++)
	{
		int pos = lower_bound(b + 1, b + n + 1, a[i]) - b;
		a[i] = pos;
	}
	for(int i = 1; i <= n; i++)
	{
		int x = a[i];
		ans += query(1, 1, n, x + 1, n);
		modify(1, 1, n, x);
	} 
	printf("%lld", ans);
	
	return 0;
}














以上是关于线段树的主要内容,如果未能解决你的问题,请参考以下文章

一般线段树与权值线段树

详解权值线段树

权值线段树&&线段树合并

zkw线段树

权值线段树

#树# #线段树#