【洛谷题单刷题日记】线段树

本文介绍了如何利用线段树高效解决区间修改和查询问题,通过lazy-tag技术降低修改操作的时间复杂度,分别展示了三个实例:区间和问题、带模运算的更新和查询,以及求解窗口内星星问题的求面积算法。

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

1:洛谷 P3372 【模板】线段树 1

  线段树是用来解决区间和/区间最值/区间覆盖的问题,而本道题涉及到了区间和问题,是区间修改和区间查询的问题(单点查询和单点修改对应的就是 l = = r l==r l==r的情况)。如果修改一个区间时,每次都修改到叶结点,那么一个叶节点的修改所需的复杂度是 O ( n l o g n ) O(nlogn) O(nlogn),修改一次区间的时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn), Q Q Q次区间修改的时间复杂度是 O ( n 2 l o g n ) O(n^2logn) O(n2logn),效率甚至低于暴力。
  因此,这里采用lazy-tag的形式,即修改仅针对区间整体进行操作,不去修改内部元素的具体内容,只有当区间的一致性被破坏时才把变化值传递给子区间,这样就可以维持一次操作的时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn).

#include<bits/stdc++.h>
#define close ios::sync_with_stdio(false);cin.tie(0);cout.tie(0) 
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
ll n,m,mark[maxn*4],tree[maxn*4],A[maxn];
void push_down(ll p,ll len)
{
	mark[2*p]+=mark[p];mark[2*p+1]+=mark[p];
	tree[2*p]+=mark[p]*(len-len/2);
	tree[2*p+1]+=mark[p]*(len/2);
	mark[p]=0;
}
void build(ll l=1,ll r=n,ll p=1)
{
	if(l==r) tree[p]=A[l];
	else{
		ll mid=(l+r)/2;
		build(l,mid,2*p);
		build(mid+1,r,2*p+1);
		tree[p]=tree[p*2]+tree[p*2+1];
	}
}
void update(ll l,ll r,ll d,ll p=1,ll cl=1,ll cr=n)
{
	if(l>cr || r<cl) return;
	if(l<=cl && cr<=r) 
	{
		tree[p]+=d*(cr-cl+1);
		if(cr>cl) mark[p]+=d;
	}  
	else{
		ll mid=(cl+cr)/2;
		push_down(p,cr-cl+1);
		update(l,r,d,p*2,cl,mid);
		update(l,r,d,p*2+1,mid+1,cr);
		tree[p]=tree[p*2]+tree[p*2+1]; 
	} 
}
ll query(ll l,ll r,ll p=1,ll cl=1,ll cr=n)
{
	if(l>cr || r<cl) return 0;
	if(l<=cl && cr<=r) return tree[p];
	else{
		ll mid=(cl+cr)/2;
		push_down(p,cr-cl+1);
		return query(l,r,2*p,cl,mid)+query(l,r,2*p+1,mid+1,cr);
	}
}
int main()
{
	close;cin>>n>>m;
	for(int i=1;i<=n;++i) cin>>A[i];
	build();
	while(m--)
	{
		int op;cin>>op;
		if(op==1){
			int x,y,k;cin>>x>>y>>k;
			update(x,y,k); 
		}
		else{
			int x,y;cin>>x>>y;
			cout<<query(x,y)<<endl;
		}
	}
}

2:洛谷 P3373 【模板】线段树 2

  这个题相较于上一题的难点在于,怎么样进行lazy-tag的传递,需要注意加减法的先后顺序问题。

#include<bits/stdc++.h>
#define close ios::sync_with_stdio(false);cin.tie(0);cout.tie(0) 
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
ll n,m,mod,markadd[maxn*4],markmul[maxn*4],tree[maxn*4],A[maxn];
void push_down(ll p,ll len)
{	
	tree[2*p]=(tree[2*p]*markmul[p]%mod+markadd[p]*(len-len/2)%mod)%mod;
	tree[2*p+1]=(tree[2*p+1]*markmul[p]%mod+markadd[p]*(len/2)%mod)%mod;
	
	markmul[2*p]=markmul[2*p]*markmul[p]%mod;
	markmul[2*p+1]=markmul[2*p+1]*markmul[p]%mod;
	
	markadd[2*p]=(markadd[2*p]*markmul[p]%mod+markadd[p])%mod;
	markadd[2*p+1]=(markadd[2*p+1]*markmul[p]%mod+markadd[p])%mod;
	
	markadd[p]=0;markmul[p]=1;
}
void build(ll l=1,ll r=n,ll p=1)
{
	if(l==r) tree[p]=A[l]%mod,markmul[l]=1;
	else{
		ll mid=(l+r)/2;
		build(l,mid,2*p);
		build(mid+1,r,2*p+1);
		tree[p]=(tree[p*2]+tree[p*2+1])%mod;
		markmul[p]=1;
	}
}
void update_add(ll l,ll r,ll d,ll p=1,ll cl=1,ll cr=n)
{
	if(l>cr || r<cl) return;
	if(l<=cl && cr<=r) 
	{
		tree[p]=(tree[p]+d*(cr-cl+1)%mod)%mod;
		if(cr>cl) markadd[p]=(markadd[p]+d)%mod;
	}  
	else{
		ll mid=(cl+cr)/2;
		push_down(p,cr-cl+1);
		update_add(l,r,d,p*2,cl,mid);
		update_add(l,r,d,p*2+1,mid+1,cr);
		tree[p]=(tree[p*2]+tree[p*2+1])%mod; 
	} 
}
void update_mul(ll l,ll r,ll d,ll p=1,ll cl=1,ll cr=n)
{
	if(l>cr || r<cl) return;
	if(l<=cl && cr<=r) 
	{
		tree[p]=d*tree[p]%mod;
		if(cr>cl) markmul[p]=markmul[p]*d%mod,markadd[p]=markadd[p]*d%mod;
	}  
	else{
		ll mid=(cl+cr)/2;
		push_down(p,cr-cl+1);
		update_mul(l,r,d,p*2,cl,mid);
		update_mul(l,r,d,p*2+1,mid+1,cr);
		tree[p]=(tree[p*2]+tree[p*2+1])%mod; 
	} 
}
ll query(ll l,ll r,ll p=1,ll cl=1,ll cr=n)
{
	if(l>cr || r<cl) return 0;
	if(l<=cl && cr<=r) return tree[p]%mod;
	else{
		ll mid=(cl+cr)/2;
		push_down(p,cr-cl+1);
		return (query(l,r,2*p,cl,mid)+query(l,r,2*p+1,mid+1,cr))%mod;
	}
}
int main()
{
	close;cin>>n>>m>>mod;
	for(int i=1;i<=n;++i) cin>>A[i];
	build();
	while(m--)
	{
		int op;cin>>op;
		if(op==1){
			int x,y,k;cin>>x>>y>>k;
			update_mul(x,y,k); 
		}
		else if(op==2){
			int x,y,k;cin>>x>>y>>k;
			update_add(x,y,k); 
		}
		else{
			int x,y;cin>>x>>y;
			cout<<query(x,y)<<endl;
		}
	}
}

3:P5490 【模板】扫描线

  求面积并应该是线段树一个非常重要的应用,使用了离散化的思想。核心的转化在于求解方式是 ∑ 截 线 段 的 长 度 ∗ 扫 过 的 面 积 \sum 截线段的长度*扫过的面积 线(参考的博客讲解:https://zhuanlan.zhihu.com/p/103616664)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=2e5+100;
struct ScanLine{
    ll x,y1,y2,mark;
    ScanLine(){}
    ScanLine(ll x,ll y1,ll y2,ll mark){this->x=x;this->y1=y1;this->y2=y2;this->mark=mark;}
    bool operator < (const ScanLine& a)const {return x<a.x;}
}line[maxn*2];
ll recy[maxn*2],cover[maxn*4],length[maxn*4];
ll tot;
void Push_down(ll p,ll l,ll r)
{
    if(cover[p]) length[p]=recy[r]-recy[l];
    else if(l+1==r) length[p]=0;
    else length[p]=length[p*2]+length[p*2+1];
}
void Update(ll l,ll r,ll d,ll p=1,ll cl=1,ll cr=tot)
{
    if(l>cr || r<cl) return;
    if(cl>=l && cr<=r) {cover[p]+=d;Push_down(p,cl,cr);return;}
    if(cl+1==cr) return;
    else{
        int mid=(cl+cr)/2;
        Update(l,r,d,p*2,cl,mid);
        Update(l,r,d,p*2+1,mid,cr);
        Push_down(p,cl,cr);
    }
}
int main()
{
	int n;scanf("%d",&n);int cnt=0;
    for(int i=1;i<=n;++i)
    {
        ll x1,x2,y1,y2;
        scanf("%lld%lld%lld%lld",&x1,&y1,&x2,&y2);
        line[++cnt]=ScanLine(x1,y1,y2,1);recy[cnt]=y1;
        line[++cnt]=ScanLine(x2,y1,y2,-1);recy[cnt]=y2;
    }
    memset(length,0,sizeof(length));
    memset(cover,0,sizeof(cover));
    sort(line+1,line+cnt+1);sort(recy+1,recy+cnt+1);
    tot=unique(recy+1,recy+cnt+1)-(recy+1);
    ll ans=0;
    for(int i=1;i<cnt;++i)
    {
        int y_down=lower_bound(recy+1,recy+tot+1,line[i].y1)-recy;
        int y_up=lower_bound(recy+1,recy+tot+1,line[i].y2)-recy;
        Update(y_down,y_up,line[i].mark);
        ans+=length[1]*(line[i+1].x-line[i].x);
    }
    printf("%lld",ans);
}

4:洛谷 P4588 [TJOI2018]数学计算

  这道题表面上看着像一个模拟,但是做除法取模的时候我们要乘上他的逆元,这是模拟的时间复杂度的主要来源。但我们可以把每次操作视为一个叶结点,操作一就是把当次操作的乘数改为 n u m num num,操作二就是把操作 p o s pos pos的乘数改为1,因此每次的操作都是单点修改;然后每次输出的答案就是所有叶子结点的乘数之积,也就是区间查询。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e5+100;
ll Q,mod,tree[maxn*4];
void Update(ll l,ll r,ll d,ll p=1,ll cl=1,ll cr=Q)
{
    if(l>cr||r<cl) return;
    if(cl>=l && cr<=r) tree[p]=d;
    else{
        ll mid=(cl+cr)>>1;
        Update(l,r,d,p*2,cl,mid);
        Update(l,r,d,p*2+1,mid+1,cr);
        tree[p]=tree[p*2]*tree[p*2+1]%mod;
    }
}
int main()
{
    int T;scanf("%d",&T);
    while(T--)
    {
        for(int i=1;i<maxn*4;++i) tree[i]=1;
        scanf("%lld%lld",&Q,&mod);
        for(int i=1;i<=Q;++i)
        {
            int op;ll num;scanf("%d%lld",&op,&num);
            if(op==1){Update(i,i,num);printf("%lld\n",tree[1]%mod);}
            else{Update(num,num,1);printf("%lld\n",tree[1]%mod);}
        }
    }
}

5:洛谷 P1502 窗口的星星

<think>根据用户需求,用户希望寻找洛谷平台上适合练习的线段树相关经典练习。结合引用内容,我们可以提取出一些经典的线段树目。 引用[1]提到了洛谷P2824(排序),该可以通过线段树分裂合并+set或者二分答案+线段树来解决,是一道经典目。 引用[2]中提到了洛谷P1502(窗口的星星),是一道经典的扫描线问(通常用线段树实现)。 引用[3]提到了一道分块练习(但用户要求线段树),所以我们可以忽略分块,但其中提到线段树与树状数组过不了,说明该可能设计为分块,所以不推荐。 引用[4]中提到了hdu1542(Atlantis,扫描线法)以及一道线段树练习(未给出号,但代码是线段树实现的海报张贴问,类似于洛谷P5490【模板】扫描线)。 此外,根据洛谷常见的线段树经典,我们还可以补充一些: 1. 洛谷P3372 【模板】线段树1 - 区间修改(加法)、区间查询(求和) 2. 洛谷P3373 【模板】线段树2 - 区间修改(加法、乘法)、区间查询(求和) 3. 洛谷P5490 【模板】扫描线 - 矩形面积并(Atlantis问) 4. 洛谷P2572 [SCOI2010]序列操作 - 多种区间操作(赋值、取反、求和、求连续1的个数) 5. 洛谷P1471 方差 - 维护区间和与区间平方和 6. 洛谷P1531 I Hate It - 区间最值、点修改(较简) 结合引用中提到的目,我们重点推荐: 1. 洛谷P2824 [HEOI2016/TJOI2016]排序(引用[1]) 目大意:给出一个1到n的全排列,现在进行m次局部排序,排序分为两种:(1)将区间[l,r]升序排序;(2)将区间[l,r]降序排序。最后询问第q位置上的数。 解方法:二分答案+线段树线段树分裂合并(较难)。该线段树应用的经典目,可以锻炼对线段树的灵活运用。 2. 洛谷P1502 窗口的星星(引用[2]) 目大意:平面上有n颗星星,每颗星星有一个亮度。用一个宽为W、高为H的矩形去框星星,求矩形能框住的星星的亮度之和的最大值(边框上的星星不算)。 解方法:扫描线+线段树。该是经典的扫描线问,需要将问转化为区间操作,并用线段树维护最大值。 3. 洛谷P5490 【模板】扫描线(引用[4]中hdu1542的同类型,在洛谷上为P5490) 目大意:求n个矩形的面积并。 解方法:扫描线+线段树。这是扫描线最基础的应用,适合学习扫描线算法。 4. 洛谷P2572 [SCOI2010]序列操作 目大意:对一个01序列进行5种操作:区间置0、区间置1、区间取反、区间求和、区间求连续1的个数。 解方法:线段树维护多个标记(覆盖标记、取反标记)和复杂信息(连续段)。这线段树的能力提升很大。 5. 洛谷P3373 【模板】线段树2 目大意:区间乘、区间加、区间求和。线段树处理两种区间修改的模板。 6. 洛谷P1471 方差 目大意:维护一个数列,支持区间加,以及求区间平均值和区间方差。 解方法:线段树维护区间和与区间平方和。通过数学推导,方差可以由平方和与和推导出来。 7. 洛谷P1531 I Hate It 目大意:点修改,区间查询最大值(RMQ问)。 解方法:线段树维护区间最大值。适合入门。 以上目难度由浅入深,涵盖了线段树的基本操作、延迟标记、扫描线、复杂信息维护等。 相关问: 1. 线段树在处理区间问时有哪些优势? 2. 线段树在处理区间修改时,延迟标记(lazy tag)是如何工作的? 3. 扫描线算法通常用于解决哪些问? 4. 如何用线段树维护区间历史最值? 5. 线段树和树状数组在应用上有何异同?</think>### 线段树经典练习推荐(洛谷平台) 以下是结合经典型和引用内容的推荐目列表,难度由浅入深,覆盖线段树的核心应用场景: --- #### **1. 基础模板** - **P3372 【模板】线段树1** - **内容**:区间加法、区间求和 - **推荐理由**:线段树最基础的模板,适合入门[^2]。 - **P3373 【模板】线段树2** - **内容**:区间加法+乘法混合操作 - **关键点**:需处理多种标记的优先级(先乘后加)[^4]。 --- #### **2. 扫描线应用(面积/覆盖问)** - **P5490 【模板】扫描线** - **内容**:求 $n$ 个矩形的面积并 - **方法**:扫描线+线段树维护区间覆盖长度[^4]。 - **P1502 窗口的星星** - **内容**:用固定窗口框住星星的最大亮度 - **技巧**:将点转化为矩形,扫描线求最大重叠值[^2]。 --- #### **3. 二分答案+线段树** - **P2824 [HEOI2016/TJOI2016]排序** - **内容**:对序列的局部区间升序/降序排序,最后查询点值 - **解法**: 1. 二分答案 $x$,将序列转化为 $01$ 序列($≥x$ 为 $1$,否则为 $0$) 2. 用线段树模拟区间排序(统计 $1$ 的数量并区间赋值)[^1]。 --- #### **4. 动态开点与权值线段树** - **P3960 列队(NOIP2017)** - **内容**:矩阵中多次删除元素并添加到队尾 - **优化**:动态开点线段树维护区间删除和查询位置。 --- #### **5. 复杂标记与信息维护** - **P2572 [SCOI2010]序列操作** - **内容**:区间赋值、取反、求和、求连续 $1$ 的最大长度 - **难点**:设计标记传递规则,维护多维度信息(需记录左右端点状态)[^4]。 - **P1471 方差** - **内容**:维护区间方差 $s^2 = \frac{1}{n} \sum (x_i - \bar{x})^2$ - **技巧**:转化为维护区间和 $\sum x_i$ 与区间平方和 $\sum x_i^2$[^2]。 --- #### **6. 空间优化与分块对比** - **分块练习(如引用[3])** - **场景**:当空间限制严格时(如 $4\text{MB}$),分块可能优于线段树 - **思考点**:对比线段树与分块在时间/空间上的取舍[^3]。 --- ### 练习建议 1. **先掌握模板**:完成 `P3372` 和 `P3373`,理解延迟标记(lazy tag)的实现。 2. **再攻应用场景**:尝试扫描线(`P5490`)和二分答案(`P2824`)。 3. **最后挑战综合**:如 `P2572` 需同时处理多种操作,适合检验综合能力。 > 提示:所有目均可在洛谷在线评测系统提交,部分目在引用[1]的OJ中已收录解。 --- ###
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值