什么是线段树
线段树(Segment Tree)是一种二叉树数据结构,主要用于高效处理区间查询和区间更新操作。它将一个线性区间递归地划分成若干个小区间,并将每个区间的信息(如区间和、最大值、最小值等)存储在树节点中,从而在对数时间复杂度内完成区间操作。
线段树的典型应用场景包括:
区间求和、求最值
区间修改(单点修改、区间加减、区间赋值等)
区间统计(满足某种条件的元素个数等)
线段树的基本结构
线段树是一棵完全二叉树,对于区间 [1, n]:
根节点表示整个区间 [1, n]
每个非叶子节点 [l, r] 有两个子节点:
左子节点:[l, mid],其中 mid = (l + r) / 2
右子节点:[mid+1, r]
叶子节点表示长度为1的区间 [i, i]
存储方式:
通常使用数组存储线段树(堆式存储):
根节点下标为1
节点 i 的左子节点下标为 2*i
节点 i 的右子节点下标为 2*i+1
对于长度为 n 的区间,线段树需要约 4*n 的空间。
线段树的基本操作
建树(Build)
从下往上递归构建,叶子节点存储原始数据,非叶子节点存储合并后的信息。
时间复杂度:O(n)
区间查询(Query)
查询区间 [ql, qr] 的信息:
如果当前节点区间完全包含在查询区间内,直接返回节点信息
否则,递归查询左右子节点,合并结果
时间复杂度:O(log n)
单点更新(Point Update)
更新某个点的值:
递归找到对应的叶子节点
更新叶子节点值
回溯更新所有祖先节点
时间复杂度:O(log n)
区间更新(Range Update)
更新整个区间的值,如果直接递归到叶子节点,时间复杂度为O(n log n),不可接受。因此引入懒标记(Lazy Tag)技术。
懒标记(Lazy Propagation)
懒标记的核心思想:延迟更新。当需要更新一个区间时,只更新当前节点,并将更新信息存储在懒标记中,等到以后需要访问子节点时,再将懒标记下传。
懒标记操作步骤:
更新当前节点值
如果当前节点区间完全包含在更新区间内,设置懒标记并返回
否则,先下传懒标记到子节点
递归更新左右子节点
合并子节点信息更新当前节点
线段树的变体
动态开点线段树
传统线段树需要预分配4*n空间,而动态开点线段树只在需要时创建节点,适用于:
区间范围很大(如[1, 1e9])
空间受限的情况
权值线段树
将值域作为区间,用于统计数值分布情况,可以解决:
查询第k大/小的值
查询某个值的排名
求前驱/后继
可持久化线段树(主席树)
保存历史版本,用于解决:
区间第k大问题
可回滚操作
二维线段树
处理二维平面上的区间查询和更新
线段树的优缺点
优点:
区间操作效率高:查询和更新都是O(log n)
灵活性强:可以维护多种区间信息
扩展性好:支持多种懒标记操作
缺点:
空间消耗大:需要约4*n空间
代码实现相对复杂
对于某些简单问题可能"杀鸡用牛刀"
实现技巧与注意事项
节点设计:根据问题需求设计节点存储的信息
合并函数:正确定义如何合并左右子区间的信息
懒标记设计:不同类型的操作需要不同的懒标记
边界处理:注意查询区间的边界情况
溢出问题:注意数据范围,使用合适的数据类型
下面通过4道经典例题来深入理解线段树的应用。
洛谷 P3373 【模板】线段树 2
题目描述
已知一个数列,需要对其进行三种操作:
将某区间每个数乘上x
将某区间每个数加上x
求出某区间所有数的和
解题思路
这道题需要同时处理两种区间更新操作:加法和乘法。关键在于懒标记的设计和下传顺序。
关键点:
需要维护两个懒标记:加法标记add和乘法标记mul
乘法优先于加法:先乘后加
更新时注意取模操作
代码实现
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int n , m , p;
struct segmentTree
{
int l , r;
ll sum , add , mul;
}t[N * 4];
ll a[N];
void pushup(int k)
{
t[k].sum = (t[k * 2].sum + t[k * 2 + 1].sum) % p;
}
void build(int k , int l , int r)
{
t[k].l = l , t[k].r = r , t[k].mul = 1;
if(l == r)
{
t[k].sum = a[l] % p;
return;
}
int mid = l + r >> 1;
build(k * 2 , l , mid);
build(k * 2 + 1 , mid + 1 , r);
pushup(k);
}
void change(int k , int u , int v)
{
t[k].sum = ((ll)t[k].sum * v + (ll)u * (t[k].r - t[k].l + 1)) % p;
t[k].add = ((ll)t[k].add * v + u) % p;
t[k].mul = (ll)t[k].mul * v % p;
}
void pushdown(int k)
{
change(k * 2 , t[k].add , t[k].mul);
change(k * 2 + 1 , t[k].add , t[k].mul);
t[k].add = 0;
t[k].mul = 1;
}
void update(int k , int x , int y , int u , int v)
{
if(t[k].l >= x && t[k].r <= y)
{
change(k , u , v);
return;
}
pushdown(k);
int mid = t[k].l + t[k].r >> 1;
if(x <= mid)
{
update(k * 2 , x , y , u , v);
}
if(y > mid)
{
update(k * 2 + 1 , x , y , u , v);
}
pushup(k);
}
int query(int k , int x , int y)
{
if(t[k].l >= x && t[k].r <= y)
{
return t[k].sum;
}
pushdown(k);
int mid = t[k].l + t[k].r >> 1;
int res = 0;
if(x <= mid)
{
res = query(k * 2 , x , y);
}
if(y > mid)
{
res += query(k * 2 + 1 , x , y);
}
return res % p;
}
int main()
{
cin >> n >> m >> p;
for(int i = 1;i <= n;i++)
{
cin >> a[i];
}
build(1 , 1 , n);
while(m--)
{
int op , x , y , k;
cin >> op >> x >> y;
if(op == 1)
{
cin >> k;
update(1 , x , y , 0 , k);
}
else if(op == 2)
{
cin >> k;
update(1 , x , y , k , 1);
}
else
{
cout << query(1 , x , y) << endl;
}
}
return 0;
}
洛谷 P2471 [SCOI2007] 降雨量
题目描述
有n年的降雨量数据,判断给定的两个年份之间的降雨量关系。
解题思路
本题需要查询区间最大值,判断"X年是自Y年以来降雨量最多的"这一说法是否正确。
关键点:
使用线段树维护区间最大值
年份可能不连续,需要离散化处理
需要处理各种边界情况
代码
/*
y>=x无解,false
y和x均未知,maybe
有一个未知,判断另一个与区间的大小关系maybe或false
y和x均已知但中间有未知的,按题意判断maybe或false
包括y和x的整个区间都已知,按题意判断true或false
*/
#include <iostream>
#include <map>
#define True {printf("true\n");continue;}
#define False {printf("false\n");continue;}
#define Maybe {printf("maybe\n");continue;}
#define Code using
#define by namespace
#define jhc std
Code by jhc;
const int N = 5e4 + 10;
int n , m , y[N] , r[N] , i , L , R;
map<int , int> mp;
int st[20][N] , log2[N] , mi[20];
void build_st()
{
log2[1] = 0;
for(int i = 2;i <= n;i++)
{
log2[i] = log2[i / 2] + 1;
}
mi[0] = 1;
for(int i = 1;i < 20;i++)
{
mi[i] = mi[i - 1] * 2;
}
for(int i = 1;i <= 16;i++)
{
for(int j = 1;j <= n;j++)
{
st[i][j] = max(st[i - 1][j] , st[i - 1][j + mi[i - 1]]);
}
}
}
inline int ask(int l , int r)
{
if(l > r) return 0;
return max(st[log2[r - l + 1]][l] , st[log2[r - l + 1]][r - mi[log2[r - l + 1]] + 1]);
}
int fl(int x)
{
int L = 0 , R = n + 1 , mid;
while(L < R)
{
mid=(L + R + 1) >> 1;
if(y[mid] < x)
{
L = mid;
}
else
{
R = mid - 1;
}
}
return L;
}
char Getchar()
{
return getchar();
static char buff[1000000],*p,*end=p;
if(p==end)
end=buff+fread(p=buff,1,1000000,stdin);
return *(p++);
}
template<typename T>void read(T &x)
{
static char rc;static int flag;
x=0;rc=Getchar();flag=1;
while(!isdigit(rc))
flag=(rc=='-'?-1:1),rc=Getchar();
while(isdigit(rc))
x=x*10+rc-'0',rc=Getchar();
x*=flag;
}
int main()
{
read(n);
for(i = 1;i <= n;i++)
{
read(y[i]);
read(r[i]);
mp[y[i]] = i;
st[0][i] = r[i];
}
read(m);
build_st();
for(i = 1;i <= m;i++)
{
read(L);
read(R);
if(mp.find(R) == mp.end())
{
if(mp.find(L) == mp.end())
{
Maybe;
}
if(ask(mp[L] + 1 , fl(R)) < r[mp[L]])
{
Maybe;
}
False;
}
else if(mp.find(L) != mp.end())
{
if(r[mp[L]] < r[mp[R]])
{
False;
}
if(ask(mp[L] + 1 , mp[R] - 1) >= r[mp[R]])
{
False;
}
if(R-L == mp[R] - mp[L])
{
True;
}
Maybe;
}
else
{
if(ask(fl(L) + 1 , mp[R] - 1) >= r[mp[R]])
{
False;
}
Maybe;
}
}
return 0;
}
//代码提供人:唐晓玥
洛谷 P4513 小白逛公园
题目描述
给定一个数列,进行两种操作:
修改某个位置的值
查询某个区间的最大子段和
解题思路
这是线段树维护区间信息的经典问题。每个节点需要维护四个信息:
区间和 sum
区间最大子段和 max_sum
区间最大前缀和 max_prefix
区间最大后缀和 max_suffix
合并两个区间时:
sum = left.sum + right.sum
max_prefix = max(left.max_prefix, left.sum + right.max_prefix)
max_suffix = max(right.max_suffix, right.sum + left.max_suffix)
max_sum = max(left.max_sum, right.max_sum, left.max_suffix + right.max_prefix)
代码实现
#include<bits/stdc++.h>
#define Code using
#define by namespace
#define jhc std
Code by jhc;
#define int long long
#define lc (t<<1)
#define rc ((t<<1)|1)
int in()
{
int k=0,f=1;
char c=getchar();
while(c<'0'||c>'9')
{
if(c=='-')f=-1;
c=getchar();
}
while(c>='0'&&c<='9')k=k*10+c-'0',c=getchar();
return k*f;
}
void out(int x)
{
if(x<0)putchar('-'),x=-x;
if(x<10)putchar(x+'0');
else out(x/10),putchar(x%10+'0');
}
const int N=5e5+10;
int a[N];
struct nod
{
int l,r;
int sum,ms,ls,rs;
}T[N<<2];
int ty,qx,qy,qk;
int sum,mxs,rms,lms;
void up(int t)
{
T[t].sum=T[lc].sum+T[rc].sum;
T[t].ls=max(T[lc].ls,T[rc].ls+T[lc].sum);
T[t].rs=max(T[rc].rs,T[lc].rs+T[rc].sum);
T[t].ms=max(max(T[lc].ms,T[rc].ms),T[lc].rs+T[rc].ls);
}
void buildt(int t,int x,int y)
{
T[t].l=x,T[t].r=y;
if(x<y)
{
int mid=(x+y)>>1;
buildt(lc,x,mid),buildt(rc,mid+1,y);
up(t);
}
else T[t].sum=T[t].ls=T[t].rs=T[t].ms=a[x];
}
void add(int t)
{
if(T[t].l==T[t].r)
{
T[t].sum=T[t].ls=T[t].rs=T[t].ms=qk;
return;
}
int mid=(T[t].l+T[t].r)>>1;
if(qx<=mid)add(lc);
else add(rc);
up(t);
}
void ask(int t)
{
if(qx<=T[t].l&&T[t].r<=qy)
{
mxs=max(max(mxs,T[t].ms),rms+T[t].ls);
rms=max(T[t].rs,T[t].sum+rms);
lms=max(lms,sum+T[t].ls);
sum+=T[t].sum;
return;
}
int mid=(T[t].l+T[t].r)>>1;
if(qx<=mid)ask(lc);
if(qy>mid)ask(rc);
}
signed main()
{
int n=in(),q=in();
for(int i=1;i<=n;i++)a[i]=in();
buildt(1,1,n);
while(q--)
{
ty=in(),qx=in();
if(ty==2)qk=in(),add(1);
else
{
qy=in();
if(qx>qy)swap(qx,qy);
mxs=-1e9,sum=lms=rms=0;
ask(1);
out(mxs),putchar('\n');
}
}
return 0;
}
洛谷 P3224 [HNOI2012] 永无乡
题目描述
有n个岛屿,每个岛屿有一个重要度排名。初始时有一些桥连接岛屿(形成多个连通块)。支持两种操作:
在x和y之间建一座桥
询问与x连通的所有岛屿中,重要度排名第k小的岛屿编号
解题思路
这道题需要结合并查集和线段树(权值线段树):
用并查集维护连通性
对每个连通块维护一棵权值线段树
合并连通块时合并对应的线段树
关键点:
使用动态开点线段树避免MLE
线段树合并时注意内存管理
查询第k小使用权值线段树的特性
代码
#include <iostream>
#include <algorithm>
#define Code using
#define by namespace
#define jhc std
Code by jhc;
const int MAXN = 1e5 + 5;
int n , m , q;
int rank_to_island[MAXN];
int island_to_rank[MAXN];
int root[MAXN];
int seg_root[MAXN];
int find(int x)
{
return root[x] == x ? x : root[x] = find(root[x]);
}
struct SegNode
{
int lc , rc;
int sum;
}seg_tree[MAXN * 20];
int seg_cnt = 0;
void seg_update(int &p , int l , int r , int pos)
{
if(!p)
{
p = ++seg_cnt;
}
seg_tree[p].sum++;
if(l == r)
{
return;
}
int mid = (l + r) / 2;
if(pos <= mid)
{
seg_update(seg_tree[p].lc , l , mid , pos);
}
else
{
seg_update(seg_tree[p].rc , mid + 1 , r , pos);
}
}
int seg_merge(int p , int q , int l , int r)
{
if(!p || !q)
{
return p | q;
}
if(l == r)
{
seg_tree[p].sum += seg_tree[q].sum;
return p;
}
int mid = (l + r) / 2;
seg_tree[p].lc = seg_merge(seg_tree[p].lc , seg_tree[q].lc , l , mid);
seg_tree[p].rc = seg_merge(seg_tree[p].rc , seg_tree[q].rc , mid + 1 , r);
seg_tree[p].sum = seg_tree[seg_tree[p].lc].sum + seg_tree[seg_tree[p].rc].sum;
return p;
}
int seg_query(int p , int l , int r , int k)
{
if(l == r)
{
return l;
}
int mid = (l + r) / 2;
int left_sum = seg_tree[seg_tree[p].lc].sum;
if(k <= left_sum)
{
return seg_query(seg_tree[p].lc , l , mid , k);
}
else
{
return seg_query(seg_tree[p].rc , mid + 1 , r , k - left_sum);
}
}
int main()
{
cin >> n >> m;
for(int i = 1;i <= n;i++)
{
root[i] = i;
int r;
cin >> r;
rank_to_island[r] = i;
island_to_rank[i] = r;
}
for(int i = 1;i <= m;i++)
{
int x , y;
cin >> x >> y;
int fx = find(x) , fy = find(y);
if(fx != fy)
{
root[fx] = fy;
}
}
for(int i = 1;i <= n;i++)
{
int fi = find(i);
seg_update(seg_root[fi] , 1 , n , island_to_rank[i]);
}
cin >> q;
while(q--)
{
char op;
int x , y;
cin >> op >> x >> y;
if(op == 'B')
{
int fx = find(x) , fy = find(y);
if(fx != fy)
{
root[fx] = fy;
seg_root[fy] = seg_merge(seg_root[fy] , seg_root[fx] , 1 , n);
}
}
else
{
int fx = find(x);
if(seg_tree[seg_root[fx]].sum < y)
{
cout << "-1\n";
}
else
{
int rank = seg_query(seg_root[fx] , 1 , n , y);
cout << rank_to_island[rank] << "\n";
}
}
}
return 0;
}
浙公网安备 33010602011771号