什么是线段树

线段树(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;
}