线段树、主席树

本文详细介绍了线段树和主席树的概念、种类及应用,包括点查询、点修改、区间查询、区间修改等操作的实现方法。通过具体代码示例,讲解了如何构建线段树和主席树,以及如何进行查询和更新操作。特别强调了懒惰标记在区间修改中的作用,以及主席树在处理动态数据结构时的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线段树

种类

分为点查询、点修改、区间查询、区间修改

点查询、点修改

都是沿树的一条路走,时间复杂度也就是树的深度logn

区间查询

时间复杂度为(logn)^2的情况是每一次向下查询的时候都恰好往两边分了,例如线段树[1,10],查询目标区间[3,8],每次都是平均地分开了两个区间。

区间修改

由于修改某个区间的时候要同时修改所有与该区间有交叉关系的区间的值,

模板

建树
利用递归,注意递归有跳出条件,就是当到达最底层(也就是区间的左等于右)要跳出,返回。每次首先确定mid = (l+r)>>1,然后再递归建左儿子,build(k<<1,l,mid),再递归建右儿子,build(k<<1|1,mid+1,r);

访问某点
利用递归,注意递归跳出条件。递归找下边的区间

访问某区间

改变某个点的值
到这个点的所经过的所有的节点的值都要变化,因此要通过递归按照路径一点一点去找

改变某个区间
一般是这个区间的所有的成员都做一个相同的变换操作。区间修改要利用的是对所有节点都就一个lazy成员标记,用来表示该节点已经被修改了但是他的子节点都未被修改。然后这个lazy标记主要用在搭配查询时使用的。
(在区间修改、变化的过程中不会用到lazy标记。在区间查询的过程中会有用到lazy标记)

主席树

通过循环建树,建树是一直添加支链的过程,有多少个状态就有多少次build或update的过程。(如果是update的话就有上来的先建一棵空树,然后通过update不断添值)

理解:主席树与线段树在建树和查找上都是通过递归然后找mid、和mid 进行的,区别在于线段树最一开始的放入查找的是1,也就是唯一的那个头节点,但是在主席树中丢进函数的第一个节点不一定是1。

习题:hdu2665

//
//  main.cpp
//  主席树_区间第k大
//
//  Created by 陈冉飞 on 2019/7/26.
//  Copyright © 2019 陈冉飞. Aint rights reserved.
//

#include <stdio.h>
#include <string.h>
#include <iostream>
#include <algorithm>
#define maxn 100005
using namespace std;

int tl;

int a[maxn],b[maxn],T[maxn];   //a是储存初始的,b是离散化后的,T是储存根节点序号的

int sum[maxn<<5];  //sum是用来储存每个点下面所储存的数的个数

int L[maxn<<5],R[maxn<<5];//L与R用于储存树形结构

//先创建一颗空的树
int build(int l,int r){
    int root = tl++;
    int mid = (l+r)>>1;
    sum[root] = 0;  //每个节点底下都只有一个元素
    if(l<r){
        L[root] = build(l, mid);
        R[root] = build(mid+1, r);
    }
    return root;
}

int update(int pre,int l,int r,int pos){    //pos 为当前这个数要往这颗空的树插入的位置
    int root = tl++;
    int mid = (l+r)>>1;
    //赋上上个节点的
    L[root] = L[pre];
    R[root] = R[pre];
    sum[root] = sum[pre]+1;
    if(l < r){    //更新树的操作
        if (pos <= mid) {    //在左边,刷新左边的值
            L[root] = update(L[pre], l, mid, pos);
        }else{     //在右边,刷新右边的值
            R[root] = update(R[pre], mid+1, r, pos);
        }
    }
    return root;
}

int query(int u,int v, int l,int r ,int k){   //x为要查找的第几大,lr为左右端点,判断是否应该返回这个值
    if (l>=r) {
        return l;
    }
    int mid = (l+r)>>1;   //此时的mid l r  就是用来缩短检索的区间的长度的
    //如果左子树的放的数的值大于要查找的k 则在k中
    if(sum[L[v]]-sum[L[u]] >= k){
        return query(L[u],L[v],l,mid,k);
    }else{
        return query(R[u], R[v], mid+1, r, k-(sum[L[v]]-sum[L[u]]));
    }
}

int main(int argc, const char * argv[]) {
    int T1,total,qu_total,nn,pos,l,r,k,i;
    scanf("%d",&T1);
    while (T1--) {
        scanf("%d%d",&total,&qu_total);
        //先build一颗空的树
        for (i = 1; i <= total ; i++) {
            scanf("%d",&a[i]);
            b[i] = a[i];
        }
        tl = 1;
        //离散化
        sort(b+1, b+1+total);
        nn = unique(b+1, b+total+1)-b-1;   //nn是离散化出来发现有多少个不同的数
        T[0] = build(1, nn);
        for (i = 1; i <= total; i++) {
            //得到了此时第i个的位置   ,  再用得到的位置进行update ,刷新此时的树
            pos = lower_bound(b+1,b+nn+1,a[i])-b;
//            cout<<i<<" 输出位置 "<<pos<<endl;
            T[i] = update(T[i-1],1, nn, pos);
        }
//        for (int i = 1; i <= total*4; i++) {
//            cout<<T[i]<<"  "<<sum[i]<<"  "<<L[i]<<"  "<<R[i]<<endl;
//        }
        while (qu_total--) {
            scanf("%d%d%d",&l,&r,&k);
            printf("%d\n",b[query(T[l-1], T[r], 1, nn, k)]);
        }
    }
    return 0;
}

博客参考:
参考1
参考2

hdu1754
注意在写区间查找的时候要记得是区间概括,不是正好区间对应,即gl <= l && r <= gr ,不是两边都相等

//
//  main.cpp
//  线段树_hdu1754_单点修改、区间最值
//
//  Created by 陈冉飞 on 2019/8/13.
//  Copyright © 2019 陈冉飞. All rights reserved.
//

#include <iostream>
using namespace std;
char s[10];
int n,m,b,c;
#define maxn 200050
int a[maxn*4],maxx[maxn*4];

void build(int k,int l,int r){
    if (l == r) {scanf("%d",&a[k]);maxx[k] = a[k];return;}
    int mid = (l+r)>>1;
    build(k<<1, l, mid);
    build(k<<1|1, mid+1, r);
    maxx[k] = max(maxx[k<<1],maxx[k<<1|1]);
}

void change(int k,int l,int r,int pos,int x){
    if (l == pos && l == r) {a[k] = x;maxx[k] = x;return;}
    int mid = (l+r)>>1;
    if (pos <= mid) change(k<<1, l, mid, pos, x);
    else change(k<<1|1, mid+1, r, pos, x);
    maxx[k] = max(maxx[k<<1],maxx[k<<1|1]);
}

int query(int k,int l,int r,int gl,int gr){
    if(gl <= l && r <= gr) return maxx[k];
    int mid = (l+r)>>1;
    if (gr<=mid) return query(k<<1, l, mid, gl, gr);
    else if(gl >= mid+1) return query(k<<1|1, mid+1, r, gl, gr);
    else return max(query(k<<1, l, mid, gl, gr),query(k<<1|1, mid+1, r, gl, gr));
}

int main(int argc, const char * argv[]) {
    while (~scanf("%d%d",&n,&m)) {
        build(1,1,n);
        while (m--) {
            scanf("%s%d%d",s,&b,&c);
            if (s[0] == 'Q') printf("%d\n",query(1, 1, n, b, c));
            else change(1, 1, n, b, c);
        }
    }
    return 0;
}

hdu1166
下面这两个一开始都TLE了,后来发现是输入的时候出了点问题,

while(scanf("%s%d%d",s,&b,&c) && s[0] != ''E){}   //这样跳不出来,所以一直tle
后来吧scanf bc 放到循环里边就ok了

写一个数组,直接在这一个数组中改动

//
//  main.cpp
//  线段树_单点更新、区间求和_hdu1166
//
//  Created by 陈冉飞 on 2019/8/13.
//  Copyright © 2019 陈冉飞. All rights reserved.
//

#include <iostream>
using namespace std;
#define maxn 50050
int a[maxn*4];
char s[10];
int T,n,i,b,c,kase = 1;

void build(int k,int l,int r){
    if (l == r) {scanf("%d",&a[k]);return;}
    int mid = (l+r)>>1;
    build(k<<1, l, mid);
    build(k<<1|1, mid+1, r);
    a[k] = a[k<<1]+a[k<<1|1];
}

void add(int k,int l,int r,int pos,int x){
    if (l == r) {a[k] += x;return;}
    int mid = (l+r)>>1;
    if (pos <= mid) add(k<<1,l,mid,pos,x);
    else add(k<<1|1, mid+1, r, pos, x);
    a[k] = a[k<<1]+a[k<<1|1];
}

int query(int k,int l,int r,int gl,int gr){
    if (gl <= l && r <= gr) return a[k];
    int mid = (l+r)>>1;
    if (gr <= mid) return query(k<<1, l, mid, gl, gr);
    else if(gl >= mid+1) return query(k<<1|1, mid+1, r, gl, gr);
    else return query(k<<1, l, mid, gl, gr)+query(k<<1|1, mid+1, r, gl, gr);
}

int main(int argc, const char * argv[]) {
    for (scanf("%d",&T); T; T--) {
        printf("Case %d:\n",kase++);
        scanf("%d",&n);
        build(1,1,n);
        while (scanf("%s",s) && s[0] != 'E') {
        	scanf("%d%d",&b,&c);
            if (s[0] == 'Q') printf("%d\n",query(1, 1, n, b, c));
            else if(s[0] == 'A') add(1, 1, n, b, c);
            else add(1, 1, n, b, -c);
        }
    }
    return 0;
}

多写一个sum数组,原来储存所有基础数据的数组不变动

//
//  main.cpp
//  线段树_单点更新、区间求和_hdu1166
//
//  Created by 陈冉飞 on 2019/8/13.
//  Copyright © 2019 陈冉飞. All rights reserved.
//

#include <iostream>
using namespace std;
#define maxn 50050
int a[maxn*4],sum[maxn*4];
char s[10];
int T,n,i,b,c,kase = 1;

void build(int k,int l,int r){
    if (l == r) {scanf("%d",&a[k]);sum[k] = a[k];return;}
    int mid = (l+r)>>1;
    build(k<<1, l, mid);
    build(k<<1|1, mid+1, r);
    sum[k] = sum[k<<1]+sum[k<<1|1];
}

void add(int k,int l,int r,int pos,int x){
    if (l == r) {a[k] += x;sum[k] += x;return;}
    int mid = (l+r)>>1;
    if (pos <= mid) add(k<<1,l,mid,pos,x);
    else add(k<<1|1, mid+1, r, pos, x);
    sum[k] = sum[k<<1]+sum[k<<1|1];
}

int query(int k,int l,int r,int gl,int gr){
    if (gl <= l && r <= gr) return sum[k];
    int mid = (l+r)>>1;
    if (gr <= mid) return query(k<<1, l, mid, gl, gr);
    else if(gl >= mid+1) return query(k<<1|1, mid+1, r, gl, gr);
    else return query(k<<1, l, mid, gl, gr)+query(k<<1|1, mid+1, r, gl, gr);
}

int main(int argc, const char * argv[]) {
    for (scanf("%d",&T); T; T--) {
        printf("Case %d:\n",kase++);
        scanf("%d",&n);
        build(1,1,n);
        while (scanf("%s",s) && s[0] != 'E') {
        	scanf("%d%d",&b,&c);
            if (s[0] == 'Q') printf("%d\n",query(1, 1, n, b, c));
            else if(s[0] == 'A') add(1, 1, n, b, c);
            else add(1, 1, n, b, -c);
        }
    }
    return 0;
}

区间修改

如果用单点修改,这样会tle
所以此时要再开一个数组(也就是所说的lazy标记,然后在改变的时候进行pushdown,向下改值,也就是通过开的这个数组,想下改,这样再改的话只向下走了一次,也就是logn,再结合原来的logn,时间复杂度是(logn)^2,区分于单点修改,每次都往下走logn,走n次,就是n*logn)

poj3468

一开始wa了两发,看了半天才发现是change函数忘记向上刷新值了。。。枯了,光记得在build的时候刷新了

//
//  main.cpp
//  线段树_区间修改、区间求和_poj3468
//
//  Created by 陈冉飞 on 2019/8/13.
//  Copyright © 2019 陈冉飞. All rights reserved.
//

#include <iostream>
using namespace std;
typedef long long ll;
#define maxn 100050
ll sum[maxn*8],add[maxn*8];
int n,m,i,b,c,d;
char s[10];

void build(int k,int l,int r){
    add[k] = 0; //所有将add标记数组都初始化为零
    if (l == r) {scanf("%lld",&sum[k]);return;}
    int mid = (l+r)>>1;
    build(k<<1, l, mid);
    build(k<<1|1, mid+1, r);
    sum[k] = sum[k<<1]+sum[k<<1|1];
}

void pushdown(int k,int lnum,int rnum){   //传过来的是对下面的区间长度的改变
    if(add[k]){
        sum[k<<1] += (ll)lnum*add[k];   //把左右枝都加上相应的值
        sum[k<<1|1] += (ll)rnum*add[k];
        //把值传到底下
        add[k<<1] += add[k];
        add[k<<1|1] += add[k];
        add[k] = 0;
    }
    return;
}

void change(int k,int l,int r,int gl,int gr,int x){
    //直接区间覆盖即可,此时只要区间包含
    if (gl <= l && r <= gr) {sum[k] += (r-l+1)*x;add[k] += x;return;}
    int mid = (l+r)>>1;
    //在此处有一个向下pushdown的操作,将底下的区间都改变
    pushdown(k, mid-l+1, r-mid);
    if (gl <= mid) change(k<<1, l, mid, gl, gr, x);  //只要gl<=mid,就更改左半段,因为代表左半段有他的部分
    if (gr >= mid+1) change(k<<1|1, mid+1, r, gl, gr, x);
    sum[k] = sum[k<<1] + sum[k<<1|1];  //!!!
    return;
}

ll query(int k,int l,int r,int gl,int gr){
    if (gl <= l && r <= gr) return sum[k];
    int mid = (l+r)>>1;
    //注意查询的时候也有pushdown的操作
    pushdown(k, mid-l+1, r-mid);
    ll ans = 0;
    if (gl <= mid) ans+= query(k<<1, l, mid, gl, gr);
    if (gr >= mid+1) ans += query(k<<1|1, mid+1, r, gl, gr);
    return ans;
}

int main(int argc, const char * argv[]) {
    scanf("%d%d",&n,&m);
    build(1,1,n);
    while (m--) {
        scanf("%s",s);
        if (s[0] == 'Q'){scanf("%d%d",&b,&c);printf("%lld\n",query(1, 1, n, b, c));}
        else {scanf("%d%d%d",&b,&c,&d);change(1, 1, n, b, c, d);}
    }
    return 0;
}

主席树

hdu6621
题意:找区间减去某一个值之后的所有数的绝对值的第k小是多少。

  • 通过主席树持久化保存了原来的数据
  • 利用二分快速查找降低复杂度,即
    • 如果mid的第x小的值>=k,就要往左边走,令ans = mid,rr = mid-1;
    • 如果mid的第x小的值<=k,就要往右边走,令ll = mid+1

注意这道题有一个让lrqk都^ans的操作。

//
//  main.cpp
//  B主席树
//
//  Created by 陈冉飞 on 2019/8/9.
//  Copyright © 2019 陈冉飞. All rights reserved.
//

#include <iostream>
using namespace std;
#define maxn 100050

struct node{
    int l,r,sum;   //sum维护的是当前区间的最值
};
node t[maxn*30];
int a[maxn],root[maxn];  //a存放所有的数据   存放所有人节点的根节点,用于看那些节点属于一个支
int id,n,m;   //n节点,m为所有请求

void build(int l,int r,int &x,int y,int pos){
    t[++id] = t[y];
    t[id].sum ++;
    x = id;
    if (l == r) return;
    int mid = (l+r)>>1;
    if(pos <= mid) build(l, mid, t[x].l, t[y].l, pos);
    else build(mid+1, r, t[x].r, t[y].r, pos);
}

//第k大   x是节点的索引    用来返回是第几小
int query(int l,int r,int x,int y,int ql,int qr){
    int ans = 0;
    //当前搜索的空间
    if (ql <= l && r <= qr) return t[y].sum - t[x].sum;
    int mid = (l+r)>>1;
    if (ql<=mid) ans += query(l, mid, t[x].l, t[y].l, ql, qr);
    if (qr >= mid+1) ans += query(mid+1, r, t[x].r, t[y].r, ql, qr);
    return  ans;
}

int main(int argc, const char * argv[]) {
    int T,i,l,r,q,k,ans,tem;
    scanf("%d",&T);
    while (T--) {
        id = 0;
        scanf("%d%d",&n,&m);
        for (i = 1; i <= n; i++) scanf("%d",&a[i]);
        //增添树中元素
        for (i = 1; i <= n; i++) build(1, 1000000, root[i], root[i-1], a[i]);
        //查询所有的请求
        ans = 0;
        while (m--) {
            scanf("%d%d%d%d",&l,&r,&q,&k);
            //二分查找
            l ^= ans,r ^= ans,q ^= ans,k ^= ans;
            int ll = 0,rr = 1000000,mid;
            while(ll <= rr){
                mid = (ll+rr)>>1;
                tem = query(1, 1000000, root[l-1], root[r], max(q-mid,1), min(q+mid,1000000));
                //利用二分找第k大在哪里,当前位置
                if (tem>=k) ans = mid,rr = mid-1;  //大了就往左找
                else ll = mid+1;
            }
//            cout<<ans<<endl;
            printf("%d\n",ans);
        }
    }
    return 0;
}

模版总结:
都会开一个储存根节点的数组,然后遍历的时候遍历从a到b根节点所有的区间内的树形结构
查询语句:query(,root[l-1],root[r]);
查询函数:
往左偏就是传入的root[l-1]和root[r]对应的节点的左范围(如果是数组储存,就是L[root[l-1]],结构体t[root[l-1]].l 以及L[root[r]],t[root[r]])
往右偏就是传入的root[l-1]和root[r]对应的节点的右范围(R[root[l-1]] t[root[l-1]].r 以及 R[root[r]] t[root[r]].r),然后往后一直递归下去。

hdu6278
注意要先离散化一下,不然可以能爆掉数组

//
//  main.cpp
//  区间_至少有h个大于h
//
//  Created by 陈冉飞 on 2019/8/7.
//  Copyright © 2019 陈冉飞. All rights reserved.
//

#include <iostream>
using namespace std;
#include <cstring>
#include <algorithm>
#define cl(a,b) memset(a,b,sizeof(a))
#define maxn 100010

struct node{
    int l,r,val;
}tree[maxn*20];

int root[maxn],a[maxn],t[maxn];    //root标记根节点   a用来储存所有的原始数据  tem用来离散化中使用
int n,q,i,j,len,pos,num;      //len,pos是用来离散化   num用来标记所有节点的索引

//建一棵空树
int build(int l,int r){
    int tem = num++;
    //初始化一颗空树
    tree[tem].l = 0,tree[tem].r = 0,tree[tem].val = 0;
    if (l == r) return tem;
    int mid = (l+r)>>1;
    tree[tem].l = build(l,mid);
    tree[tem].r = build(mid+1, r);
    return tem;
}

//更新后来新进入的节点
int update(int rot,int pos,int l,int r){
    int tem = num++;
    //新进来一个节点首先原来的都复制过来,建了一颗新树
    tree[tem] = tree[rot];
    tree[tem].val++;
    if (l == r) return tem;
    int mid = (l+r)>>1;
    if (pos <= mid) tree[tem].l = update(tree[rot].l, pos, l, mid);
    else tree[tem].r = update(tree[rot].r, pos, mid+1, r);
    return tem;
}

//查询用的,返回一个位置来用来二分
int query(int lrot,int rrot,int k,int l,int r){
    if (l == r) return l;
    int mid = (l+r)>>1;      //与k位置进行比较
    //如果左区间满足,就往左边查询
    if (tree[tree[rrot].l].val-tree[tree[lrot].l].val >= k) return query(tree[lrot].l, tree[rrot].l, k, l, mid);
    else return query(tree[lrot].r, tree[rrot].r, k-tree[tree[rrot].l].val-tree[tree[lrot].l].val, mid+1, r);
}

int main(){
    int l,r,pl,pr,pm,res,ans;
    while (~scanf("%d%d",&n,&q)) {
        for (i = 1; i <= n; i++) {
            scanf("%d",&a[i]);
            t[i] = a[i];
        }
        //离散化
        sort(t+1,t+n+1);
        len = unique(t+1, t+n+1)-t-1;
        num = 0;
        root[0] = build(1, n);
        for (i = 1; i <= n; i++) {
            pos = lower_bound(t+1, t+len+1, a[i]) - t;
            root[i] = update(root[i-1], pos, 1, len);
        }
//        for (i = 0; i <= len; i++) cout<<i<<"  "<<t[i]<<endl;
        //查看所有的请求,二分
        while (q--) {
            scanf("%d%d",&l,&r);
            pl = 1,pr = r-l+1;
            while (pl<=pr) {
                pm = (pl+pr)>>1;
                res = query(root[l-1], root[r], r-l+1-pm+1, 1, len);
                if (t[res] >= pm) ans = pm,pl = pm+1;
                else pr = pm-1;
            }
            cout<<ans<<endl;
        }
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值