【线段树】

【线段树】

Segment Tree

使用场景

只要满足区间可加性(大区间的信息能由它的两个子区间整理得到)
可以在 O(logN) 的时间复杂度内实现:
单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)

模版代码

【区间修改(加法),区间最值/和】

// 线段树模板类,支持泛型类型T
template<class T> 
struct Segt {
    // 线段树节点结构
    struct node {
        int l, r;       // 当前节点覆盖的区间 [l, r]
        T w;            // 区间和
        T rmq;          // 区间最值(最小/最大值)
        T lazy;         // 懒标记,用于区间更新优化
        bool has_lazy;  // 标记是否有未下推的懒标记
    };
    
    vector<T> w;         // 存储原始数据
    vector<node> t;      // 存储线段树节点

    // 构造函数
    Segt() {}
    Segt(int n) { init(n); }
    
    // 从向量初始化线段树
    Segt(vector<i64> in) {
        int n = in.size() - 1;
        w.resize(n + 1);
        for (int i = 1; i <= n; i++) {
            w[i] = in[i];
        }
        init(in.size() - 1);
    }
    
    // 宏定义:左孩子和右孩子的索引计算
    #define GL (k << 1)       // 左孩子:k*2
    #define GR (k << 1 | 1)   // 右孩子:k*2+1
    
    // 初始化线段树
    void init(int n) {
        w.resize(n + 1);
        t.resize(n * 4 + 1);  // 线段树通常需要4*n的空间
        
        // 递归构建线段树
        auto build = [&](auto self, int l, int r, int k = 1) {
            if (l == r) {  // 叶子节点,对应单个元素
                // 初始化叶子节点:区间和=元素值,最小值=元素值,无懒标记
                t[k] = {l, r, w[l], w[l], 0, false}; 
                return;
            }
            t[k] = {l, r, 0, 0, 0, false};  // 初始化非叶子节点
            int mid = (l + r) / 2;    // 计算中间点,划分左右区间
            self(self, l, mid, GL);   // 递归构建左子树
            self(self, mid + 1, r, GR); // 递归构建右子树
            pushup(k);  // 上传子节点信息到当前节点
        };
        build(build, 1, n);  // 从根节点(1)开始构建,覆盖区间[1, n]
    }
    
    // 带参数的下推函数:将懒标记应用到当前节点
    void pushdown(node &p, T lazy) { 
        p.w += (p.r - p.l + 1) * lazy;  // 更新区间和
        p.rmq += lazy;                  // 更新区间最值
        p.lazy += lazy;                 // 累加懒标记值
        p.has_lazy = true;              // 标记有未下推的懒标记
    }
    
    // 下推函数:将当前节点的懒标记传递给子节点
    void pushdown(int k) {
        if (!t[k].has_lazy) return;    // 没有懒标记则不需要下推
        
        // 将懒标记下推到左右孩子
        pushdown(t[GL], t[k].lazy);
        pushdown(t[GR], t[k].lazy);
        
        // 清除当前节点的懒标记
        t[k].lazy = 0;
        t[k].has_lazy = false;
    }
    
    // 上传函数:用子节点的信息更新当前节点
    void pushup(int k) {
        auto pushup = [&](node &p, node &l, node &r) { 
            p.w = l.w + r.w;             // 区间和 = 左子树和 + 右子树和
            p.rmq = min(l.rmq, r.rmq);   // 区间最值 = 左右子树最小值的较小者
        };
        pushup(t[k], t[GL], t[GR]);  // 更新当前节点
    }
    
    // 区间修改:将[l, r]区间内的每个元素加上val
    void modify(int l, int r, T val, int k = 1) { 
        // 如果当前节点的区间完全在目标区间内
        if (l <= t[k].l && t[k].r <= r) {
            pushdown(t[k], val);  // 应用修改到当前节点并更新懒标记
            return;
        }
        
        pushdown(k);  // 下推懒标记,确保子节点数据正确
        
        int mid = (t[k].l + t[k].r) / 2;  // 计算中间点
        if (l <= mid) modify(l, r, val, GL);  // 递归修改左子树
        if (mid < r) modify(l, r, val, GR);   // 递归修改右子树
        
        pushup(k);  // 更新当前节点信息
    }
    
    // 区间询问最值:查询[l, r]区间内的最小值
    T rmq(int l, int r, int k = 1) { 
        // 如果当前节点的区间完全在目标区间内,直接返回当前节点的最小值
        if (l <= t[k].l && t[k].r <= r) {
            return t[k].rmq;
        }
        
        pushdown(k);  // 下推懒标记,确保子节点数据正确
        
        int mid = (t[k].l + t[k].r) / 2;  // 计算中间点
        T ans = numeric_limits<T>::max();  // 初始化答案为最大值
        
        // 递归查询左子树
        if (l <= mid) ans = min(ans, rmq(l, r, GL));
        // 递归查询右子树
        if (mid < r) ans = min(ans, rmq(l, r, GR));
        
        return ans;  // 返回区间最小值
    }
    
    // 区间询问和:查询[l, r]区间内的元素和
    T ask(int l, int r, int k = 1) { 
        // 如果当前节点的区间完全在目标区间内,直接返回当前节点的区间和
        if (l <= t[k].l && t[k].r <= r) {
            return t[k].w;
        }
        
        pushdown(k);  // 下推懒标记,确保子节点数据正确
        
        int mid = (t[k].l + t[k].r) / 2;  // 计算中间点
        T ans = 0;  // 初始化答案为0
        
        // 递归查询左子树并累加结果
        if (l <= mid) ans += ask(l, r, GL);
        // 递归查询右子树并累加结果
        if (mid < r) ans += ask(l, r, GR);
        
        return ans;  // 返回区间和
    }
    
    // 调试函数:打印线段树的结构和节点信息
    void debug(int k = 1) {
        cout << "[" << t[k].l << ", " << t[k].r << "]: ";
        cout << "w = " << t[k].w << ", ";
        cout << "Min = " << t[k].rmq << ", ";
        cout << "lazy = " << t[k].lazy << ", ";
        cout << "has_lazy = " << t[k].has_lazy << endl;
        
        if (t[k].l == t[k].r) return;  // 叶子节点则停止递归
        
        debug(GL), debug(GR);  // 递归打印左右子树
    }
};

模版题

区间最值 https://www.luogu.com.cn/problem/P1816
区间修改+区间求和 https://www.luogu.com.cn/problem/P3372
https://www.luogu.com.cn/problem/P3373

线段树推导

五个基本要素:信息存储,建树,单点修改,区间查询,区间修改
image
image
->堆排序方式存点

存储信息

055e5453-c970-4b07-a519-eca372e4bf5f
数组要开4N

struct SegmentTree {
    int l, r;//每个区间左右端点
    int dat;//区间数据
	int tag;//懒标记
    //其他一些附加信息
}tree[4*MAX];

建树

从根节点“1”出发,向下递归建树,并把每个节点所代表的区间赋给它。
当到达了叶节点,便传值,再向上维护信息
【pushup操作】

void pushup(int x){//更新线段树 x 号节点
	tr[x]=tr[2*x]+tr[2*x+1];//tr 为线段树数组
}
//以区间和为例
void build(int x,int l,int r){
	if(l==r) tr[x]=a[l];
	else{
		int mid=(l+r)/2;
		build(2*x,l,mid);//更新左儿子
		build(2*x+1,mid+1,r);//更新右儿子
		pushup(x);
	}
}

单点修改

从根节点开始遍历,递归找到需要修改的叶子节点,然后修改,然后向上传递信息
如果 k 位于左儿子维护区间(<=mid),递归更新左儿子,右儿子同理
如果当前节点为叶子结点,更新该节点的值加上v
时间复杂度O(logN)

//第k个数增加v
void update(int x,int l,int r,int k,int v){
    if(l==r&&l==k){
        tr[x]+=v;
        return ;
    }
    if(k<=mid) update(2*x,l,mid,k,v);
    else update(2*x+1,mid+1,r,k,v);
    pushup(x);
}

区间查询

(不加懒标记)
1.若当前节点所表示的区间已经被询问区间所完全覆盖,则立即回溯,并传回该点的信息。
2.若当前节点的左儿子所表示的区间已经被询问区间所完全覆盖,就递归访问它的左儿子
3.若当前节点的右儿子所表示的区间已经被询问区间所完全覆盖,就递归访问它的右儿子

int query(int x,int l,int r,int from,int to){
	int ans=0;
	if(from<=l&&r<=to) return tr[x];
	if(from<=mid) ans+=query(2*x,l,mid,from,to);
	if(to>mid) ans+=query(2*x+1,mid+1,r,from,to);
	return ans;
}

懒标记

【延迟更新】
当区间修改把一个区间修改完之后,如果要往下一直递归修改,时间复杂度会爆掉
在这个节点打一个标记v:现在这个节点的子树(除该节点本身)维护的值比实际的值少v
下一次需要用到这个节点的子节点的维护值时,把两个子节点的维护值更新,并且将该节点的懒标记清空
将两个子节点的懒标记加上原来父节点的懒标记值(这两个子节点的四个子节点现在比她们的父节点又少了v)

区间修改

【pushdown操作】

void pushdown(int x,int l,int r){
	if(l==r) return;
	//加上原来父节点的懒标记值
	tr[2*x].sum+=(mid-l+1)*tr[x].tag;
	tr[2*x+1].sum+=(r-mid)*tr[x].tag;
	//将懒标记下放
	tr[2*x].tag+=tr[x].tag; //注意这里需要 + 而不是直接赋值
	tr[2*x+1].tag+=tr[x].tag;
	//去掉父节点的懒标记
	tr[x].tag=0;
}

【区间更新】

//将[from,to]区间加上v
void update(int x,int l,int r,int from,int to,int v){
	if(from<=l&&r<=to){
		tr[x].sum+=(r-l+1)*v;//区间长度*更新值
		tr[x].tag+=v;
		return ;
	}
	pushdown(x,l,r);//更新懒标记与子节点的值
	if(from<=mid) update(2*x,l,mid,from,to,v);
	if(to>mid) update(2*x+1,mid+1,r,from,to,v);
	pushup(x);
}

此时的 【区间查询】 也需要用到懒标记

int query(int x,int l,int r,int from,int to){
	int ans=0;
	if(from<=l&&r<=to) return tr[x];
	//先往下传懒标记
	pushdown(x,l,r);
	if(from<=mid) ans+=query(2*x,l,mid,from,to);
	if(to>mid) ans+=query(2*x+1,mid+1,r,from,to);
	return ans;
}

板子题

最大数

https://www.acwing.com/problem/content/1277/
注意本题强制在线
且要开ll

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef pair<int,int> PII;
typedef long long ll;
ll abss(ll a){return a>0?a:-a;}
ll max_(ll a,ll b){return a>b?a:b;}
ll min_(ll a,ll b){return a<b?a:b;}
bool cmpll(ll a,ll b){return a>b;}
const int N=200010;
int m,p;
struct Node{
      int l,r;
      int v;//区间[l,r]的最大值
}tr[N*4];
//pushup基本操作
void pushup(int u){
      tr[u].v=max(tr[u<<1].v,tr[u<<1|1].v);
}
//建树
void build(int u,int l,int r){
      tr[u]={l,r};
      if(l==r) return;
      int mid=(l+r)>>1;
      build(u<<1,l,mid);
      build(u<<1|1,mid+1,r);
}
//查询
int query(int u,int l,int r){
      if(tr[u].l>=l && tr[u].r<=r) return tr[u].v;//树中节点,已经被完全包含在[l,r]中
      int mid=tr[u].l+tr[u].r>>1;
      int v=0;
      if(l<=mid) v=query(u<<1,l,r);
                 //求最大值
      if(r>mid) v=max(v,query(u<<1|1,l,r));
      return v;
}
//修改
void modify(int u,int x,int v){
      if(tr[u].l==tr[u].r) tr[u].v=v;
      else{
            int mid=tr[u].l+tr[u].r>>1;
            if(x<=mid) modify(u<<1,x,v);
            else modify(u<<1|1,x,v);
            pushup(u);
      }
}
ll n=0,last=0;
signed main(){
      ios::sync_with_stdio(0);
      cin.tie(0);
      cout.tie(0);
      cin>>m>>p;
      build(1,1,m);
      ll x;//注意要开ll:2e9会爆
      string op;
      while(m--){
            cin>>op>>x;
            if(op=="Q"){
                  last=query(1,n-x+1,n);
                  cout<<last<<endl;
            }
            else if(op=="A"){
                  modify(1,n+1,(last+x)%p);
                  n++;
            }
      }
      return 0;
}

参考:
https://www.cnblogs.com/silentEAG/p/10808978.html
https://oi-wiki.org/ds/seg/
https://www.luogu.com.cn/article/p8hyxnl7

posted @ 2025-03-08 20:00  White_ink  阅读(12)  评论(0)    收藏  举报