线段树例题

目录

线段树之基本概念

难点

EG

线段树1

线段树2

树状数组1

 敌兵布阵


 

线段树之基本概念

学习笔记:由浅入深的线段树入门 - LinearExpectation的文章 - 知乎 https://zhuanlan.zhihu.com/p/350443545

算法学习笔记(14): 线段树 - Pecco的文章 - 知乎

https://zhuanlan.zhihu.com/p/106118909

难点

线段树的难点主要体现在以下几个方面:

1. 线段树的建立:需要对原始数组进行递归划分,同时还需要考虑边界问题、节点区间和等细节,容易出错。

2. 线段树的查询:针对不同的问题,查询方式和返回结果都有所不同,需要根据具体情况进行分析。

3. 线段树的修改:修改一个节点的值需要对其祖先节点进行更新操作,需要考虑多个节点之间的关系,可能会影响到整个线段树的结构。

4. 线段树的空间优化:当数据量很大时,线段树的空间复杂度也会很高,需要进行一些空间优化,如使用动态开销等技巧。

总的来说,线段树需要综合考虑多个因素,需要具备较强的数学思维和编程能力,因此相对来说较为复杂和难以掌握。但是,一旦掌握了其核心思想和常见应用,就可以在许多问题中运用线段树解决。

EG

下面举一个求区间最大值的线段树例子。

假设有一个长度为 $n$ 的数组 $a$,需要支持以下两种操作:

1. 单点修改:将 $a_i$ 的值修改为 $v$;
2. 区间查询:找到 $\max\limits_{i\in[l,r]} a_i$ 的值。

首先构建一棵满二叉树,每个叶子节点代表一个原数组的元素。每个非叶子节点对应区间 $[l,r]$,左儿子对应区间 $[l,mid]$,右儿子对应区间 $[mid+1,r]$。其中 $mid=\lfloor\frac{l+r}{2}\rfloor$。

在每个节点维护一个最大值 $max$,表示该区间中最大的数值。单点修改操作可以通过从根节点开始访问,沿着要修改的位置向下,更新每个节点的最大值。区间查询操作可以通过递归遍历线段树,如果查询区间和当前节点区间没有交集,则返回一个足够小的值;否则,将查询区间分解成两个子区间,分别递归查询子节点,将结果合并返回。

下面是一个用于查询区间最大值的线段树实现:

const int MAXN = 1e5 + 5;
int a[MAXN];
struct node {
    int l, r, max;
} t[MAXN << 2];

void build(int p, int l, int r) {
    t[p].l = l, t[p].r = r;
    if (l == r) {
        t[p].max = a[l];
        return;
    }
    int mid = (l + r) >> 1;
    build(p << 1, l, mid);
    build(p << 1 | 1, mid + 1, r);
    t[p].max = max(t[p << 1].max, t[p << 1 | 1].max);
}//建树

void modify(int p, int x, int v) {
    if (t[p].l == t[p].r) {
        t[p].max = v;
        return;
    }
    int mid = (t[p].l + t[p].r) >> 1;
    if (x <= mid) modify(p << 1, x, v);
    else modify(p << 1 | 1, x, v);
    t[p].max = max(t[p << 1].max, t[p << 1 | 1].max);
}//节点值

int query(int p, int l, int r) {
    if (l > r) return INT_MIN;
    if (l <= t[p].l && t[p].r <= r) return t[p].max;
    int mid = (t[p].l + t[p].r) >> 1;
    int res = INT_MIN;
    if (l <= mid) res = max(res, query(p << 1, l, r));
    if (r > mid) res = max(res, query(p << 1 | 1, l, r));
    return res;
}//更新

以上代码实现了查询区间最大值、单点修改的功能,时间复杂度为 $\mathcal{O}(n\log n)$。

线段树1

Description

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

  1. 将某区间每一个数加上 k。
  2. 求出某区间每一个数的和。

Input

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

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

接下来 m 行每行包含 33 或 4 个整数,表示一个操作,具体如下:

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

Output

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

Sample 1

InputcopyOutputcopy
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
#include<iostream>
#include<cstdio>
#define MAXN 1000001
#define ll long long
using namespace std;
unsigned ll n,m,a[MAXN],ans[MAXN<<2],tag[MAXN<<2];
ll ls(ll x){return x<<1;}
ll rs(ll x){return x<<1|1;}
inline void push_up(ll p){
    ans[p]=ans[ls(p)]+ans[rs(p)];
}
void build(ll p,ll l,ll r){
    tag[p]=0;
    if(l==r){ans[p]=a[l];return ;}
    ll mid=(l+r)>>1;
    build(ls(p),l,mid);
    build(rs(p),mid+1,r);
    push_up(p);
} 
void f(ll p,ll l,ll r,ll k){
    tag[p]=tag[p]+k;
    ans[p]=ans[p]+k*(r-l+1);
}
void push_down(ll p,ll l,ll r){
    ll mid=(l+r)>>1;
    f(ls(p),l,mid,tag[p]);
    f(rs(p),mid+1,r,tag[p]);
    tag[p]=0;
}
void update(ll nl,ll nr,ll l,ll r,ll p,ll k){
    if(nl<=l&&r<=nr)
    {
        ans[p]+=k*(r-l+1);
        tag[p]+=k;
        return ;
    }
    push_down(p,l,r);
    ll mid=(l+r)>>1;
    if(nl<=mid)update(nl,nr,l,mid,ls(p),k);
    if(nr>mid) update(nl,nr,mid+1,r,rs(p),k);
    push_up(p);
}
ll query(ll q_x,ll q_y,ll l,ll r,ll p){
    ll res=0;
    if(q_x<=l&&r<=q_y)return ans[p];
    ll mid=(l+r)>>1;
    push_down(p,l,r);
    if(q_x<=mid)res+=query(q_x,q_y,l,mid,ls(p));
    if(q_y>mid) res+=query(q_x,q_y,mid+1,r,rs(p));
    return res;
}
int main(){
    ll a1,b,c,d,e,f;
    scanf("%lld%lld",&n,&m);
    for(ll i=1;i<=n;i++)scanf("%lld",&a[i]);
    build(1,1,n);
    while(m--){
        scanf("%lld",&a1);
        switch(a1)
        {
            case 1:{
                scanf("%lld%lld%lld",&b,&c,&d);
                update(b,c,1,n,1,d);
                break;
            }
            case 2:{
                scanf("%lld%lld",&e,&f);
                printf("%lld\n",query(e,f,1,n,1));
                break;
            }
        }
    }
    return 0;
}

线段树2

Description

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

  • 将某区间每一个数乘上 x;
  • 将某区间每一个数加上 x;
  • 求出某区间每一个数的和。

Input

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

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

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

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

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

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

Output

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

Sample 1

InputcopyOutputcopy
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
#include <bits/stdc++.h>
#define MAXN 100010
#define ll long long
using namespace std;
int n,m,mod;
int a[MAXN];
struct Segment_Tree {
	ll sum,add,mul;
	int l,r;
}s[MAXN<<2];

void update(int pos) {
	s[pos].sum=(s[pos<<1].sum+s[pos<<1|1].sum)%mod;
    return;
}

void pushdown(int pos) { 
	s[pos<<1].sum=(s[pos<<1].sum*s[pos].mul+s[pos].add*(s[pos<<1].r-s[pos<<1].l+1))%mod;
	s[pos<<1|1].sum=(s[pos<<1|1].sum*s[pos].mul+s[pos].add*(s[pos<<1|1].r-s[pos<<1|1].l+1))%mod;
	
	s[pos<<1].mul=(s[pos<<1].mul*s[pos].mul)%mod;
	s[pos<<1|1].mul=(s[pos<<1|1].mul*s[pos].mul)%mod;
	
	s[pos<<1].add=(s[pos<<1].add*s[pos].mul+s[pos].add)%mod;
	s[pos<<1|1].add=(s[pos<<1|1].add*s[pos].mul+s[pos].add)%mod;
		
	s[pos].add=0,s[pos].mul=1;
	return; 
}
void build_tree(int pos,int l,int r) { 
	s[pos].l=l,s[pos].r=r,s[pos].mul=1;
	if (l==r)return void(s[pos].sum=a[l]%mod);
	int mid=(l+r)>>1;
	build_tree(pos<<1,l,mid);
	build_tree(pos<<1|1,mid+1,r);
	update(pos);
	return;
}
void ChangeMul(int pos, int x, int y, int k) { 
	if (x<=s[pos].l&&s[pos].r<=y){
		s[pos].add=(s[pos].add*k)%mod;
		s[pos].mul=(s[pos].mul*k)%mod;
		s[pos].sum=(s[pos].sum*k)%mod;
		return;
	}
	pushdown(pos);
	int mid=(s[pos].l+s[pos].r)>>1;
	if (x<=mid) ChangeMul(pos<<1,x,y,k);
	if (y>mid) ChangeMul(pos<<1|1,x,y,k);
	update(pos);
	return;
}
void ChangeAdd(int pos,int x,int y,int k) { 
	if (x<=s[pos].l&&s[pos].r<=y){
		s[pos].add=(s[pos].add+k)%mod;
		s[pos].sum=(s[pos].sum+k*(s[pos].r-s[pos].l+1))%mod;
		return;
	}
	pushdown(pos);
	int mid =(s[pos].l+s[pos].r)>>1;
	if (x<=mid) ChangeAdd(pos<<1,x,y,k);
	if (y>mid) ChangeAdd(pos<<1|1,x,y,k);
	update(pos);
	return;
}
ll AskRange(int pos, int x, int y){ 
	if (x<=s[pos].l&&s[pos].r<=y){
		return s[pos].sum;
	}
	pushdown(pos);
	ll val=0;
	int mid=(s[pos].l+s[pos].r)>>1;
	if (x<=mid) val=(val+AskRange(pos<<1,x,y)) % mod;
	if (y>mid) val=(val+AskRange(pos<<1|1,x,y)) % mod;
	return val;
}
int main() {
	scanf("%d%d%d",&n,&m,&mod);
	for (int i=1;i<=n;i++){
		scanf("%d",&a[i]);
	}
	build_tree(1,1,n);
	for (int i=1;i<=m;i++){
		int opt,x,y;
		scanf("%d%d%d",&opt,&x,&y);
		if (opt == 1){
			int k;
			scanf("%d",&k);
			ChangeMul(1,x,y,k);
		}
		if (opt==2) {
			int k;scanf("%d",&k);
			ChangeAdd(1,x,y,k);
		}
		if (opt==3) {
			printf("%lld\n",AskRange(1,x,y));
		}
	}
	return 0;
}

树状数组1

Description

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

  • 将某一个数加上 x

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

Input

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

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

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

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

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

Output

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

Sample 1

InputcopyOutputcopy
5 5
1 5 4 2 3
1 1 3
2 2 5
1 3 -1
1 4 2
2 1 4
14
16
#include<iostream>
#include<cstdio>
#define MAXN 1000001
#define ll long long
using namespace std;
unsigned ll n,m,a[MAXN],ans[MAXN<<2],tag[MAXN<<2];
ll ls(ll x){return x<<1;}
ll rs(ll x){return x<<1|1;}
inline void push_up(ll p){
    ans[p]=ans[ls(p)]+ans[rs(p)];
}
void build(ll p,ll l,ll r){
    tag[p]=0;
    if(l==r){ans[p]=a[l];return ;}
    ll mid=(l+r)>>1;
    build(ls(p),l,mid);
    build(rs(p),mid+1,r);
    push_up(p);
} 
void f(ll p,ll l,ll r,ll k){
    tag[p]=tag[p]+k;
    ans[p]=ans[p]+k*(r-l+1);
}
void push_down(ll p,ll l,ll r){
    ll mid=(l+r)>>1;
    f(ls(p),l,mid,tag[p]);
    f(rs(p),mid+1,r,tag[p]);
    tag[p]=0;
}
void update(ll nl,ll nr,ll l,ll r,ll p,ll k){
    if(nl<=l&&r<=nr)
    {
        ans[p]+=k*(r-l+1);
        tag[p]+=k;
        return ;
    }
    push_down(p,l,r);
    ll mid=(l+r)>>1;
    if(nl<=mid)update(nl,nr,l,mid,ls(p),k);
    if(nr>mid) update(nl,nr,mid+1,r,rs(p),k);
    push_up(p);
}
ll query(ll q_x,ll q_y,ll l,ll r,ll p){
    ll res=0;
    if(q_x<=l&&r<=q_y)return ans[p];
    ll mid=(l+r)>>1;
    push_down(p,l,r);
    if(q_x<=mid)res+=query(q_x,q_y,l,mid,ls(p));
    if(q_y>mid) res+=query(q_x,q_y,mid+1,r,rs(p));
    return res;
}
int main(){
    ll a1,b,c,d,e,f;
    scanf("%lld%lld",&n,&m);
    for(ll i=1;i<=n;i++)scanf("%lld",&a[i]);
    build(1,1,n);
    while(m--){
        scanf("%lld",&a1);
        switch(a1)
        {
            case 1:{
                scanf("%lld%lld%lld",&b,&c,&d);
                update(b,c,1,n,1,d);
                break;
            }
            case 2:{
                scanf("%lld%lld",&e,&f);
                printf("%lld\n",query(e,f,1,n,1));
                break;
            }
        }
    }
    return 0;
}

 敌兵布阵

C国的死对头A国这段时间正在进行军事演习,所以C国间谍头子Derek和他手下Tidy又开始忙乎了。A国在海岸线沿直线布置了N个工兵营地,Derek和Tidy的任务就是要监视这些工兵营地的活动情况。由于采取了某种先进的监测手段,所以每个工兵营地的人数C国都掌握的一清二楚,每个工兵营地的人数都有可能发生变动,可能增加或减少若干人手,但这些都逃不过C国的监视。
中央情报局要研究敌人究竟演习什么战术,所以Tidy要随时向Derek汇报某一段连续的工兵营地一共有多少人,例如Derek问:“Tidy,马上汇报第3个营地到第10个营地共有多少人!”Tidy就要马上开始计算这一段的总人数并汇报。但敌兵营地的人数经常变动,而Derek每次询问的段都不一样,所以Tidy不得不每次都一个一个营地的去数,很快就精疲力尽了,Derek对Tidy的计算速度越来越不满:"你个死肥仔,算得这么慢,我炒你鱿鱼!”Tidy想:“你自己来算算看,这可真是一项累人的工作!我恨不得你炒我鱿鱼呢!”无奈之下,Tidy只好打电话向计算机专家Windbreaker求救,Windbreaker说:“死肥仔,叫你平时做多点acm题和看多点算法书,现在尝到苦果了吧!”Tidy说:"我知错了。。。"但Windbreaker已经挂掉电话了。Tidy很苦恼,这么算他真的会崩溃的,聪明的读者,你能写个程序帮他完成这项工作吗?不过如果你的程序效率不够高的话,Tidy还是会受到Derek的责骂的.

Input

第一行一个整数T,表示有T组数据。
每组数据第一行一个正整数N(N<=50000),表示敌人有N个工兵营地,接下来有N个正整数,第i个正整数ai代表第i个工兵营地里开始时有ai个人(1<=ai<=50)。
接下来每行有一条命令,命令有4种形式:
(1) Add i j,i和j为正整数,表示第i个营地增加j个人(j不超过30)
(2)Sub i j ,i和j为正整数,表示第i个营地减少j个人(j不超过30);
(3)Query i j ,i和j为正整数,i<=j,表示询问第i到第j个营地的总人数;
(4)End 表示结束,这条命令在每组数据最后出现;
每组数据最多有40000条命令

Output

对第i组数据,首先输出“Case i:”和回车,
对于每个Query询问,输出一个整数并回车,表示询问的段中的总人数,这个数保持在int以内。

Sample

InputcopyOutputcopy
1 
10 
1 2 3 4 5 6 7 8 9 10 
Query 1 3 
Add 3 6 
uery 2 7 
Sub 10 2 
Add 6 3 
Query 3 10 
End 
Case 1: 6 33 59
#include <cstdio>
using namespace std;
#define ll long long
int T,n,a,b;
char s[15];
ll aa[50005];
struct node {
    ll l, r, sum;
} t[4 * 50005];

ll build(int l, int r, int u) {
    t[u].l = l, t[u].r = r;
    if (t[u].l == t[u].r) return t[u].sum = aa[l];
    return t[u].sum = build(l, (r + l) >> 1, u << 1) +
                      build(((r + l) >> 1) + 1, r, (u << 1) + 1);
}
void add(int i, int num) {
    int u = 1;
    while (true) {
        t[u].sum += num;
        if (t[u].l == t[u].r && t[u].l == i) return;
        ((t[u].l + t[u].r) >> 1) >= i ? u = u << 1 : u = (u << 1) + 1;
    }
}

void sub(int i, int num) {
    int u = 1;
    while (true) {
        t[u].sum -= num;
        if (t[u].l == t[u].r && t[u].l == i) return;
        ((t[u].l + t[u].r) >> 1) >= i ? u = u << 1 : u = (u << 1) + 1;
    }
}

ll query(int l, int r, int u) {
    if (l > r || t[u].l > r || t[u].r < l) return 0;
    if (t[u].l >= l && t[u].r <= r) return t[u].sum;
    return query(l, r, u << 1) + query(l, r, (u << 1) + 1);
}

int main() {
    scanf("%d", &T);
    int _case = 1;
    while (T--) {
        scanf("%d", &n);
        for (int i = 1; i <= n; i++) scanf("%lld", &aa[i]);
        printf("Case %d:\n", _case++);
        build(1, n, 1);
        while (true) {
            scanf("%s", s);
            if (s[0] == 'E') break;
            scanf("%d%d", &a, &b);
            if (s[0] == 'Q') printf("%lld\n", query(a, b, 1));
            if (s[0] == 'A') add(a, b);
            if (s[0] == 'S') sub(a, b);
        }
    }
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值