线段树
线段树是一种维护区间的数据结构,与树状数组类似,其支持区间的修改与查询
树状数组优缺点:
优:时空复杂度好,码量小
缺:对于解决区间问题有局限性
线段树优缺点:
优:更加通用,可轻松解决用树状数组难以解决的问题
缺:时空复杂度较高,码量较大
线段树三大操作:
1.建树
我们的建树方式是:若p为父节点,p2为左子节点,p2+1为右子节点
定义结构体变量ST(Segment Tree)保存我们需要的当前节点,左右子节点以及当前节点所需要的信息,所需要维护的信息在建树时完成初始化
Code:以最大值为例进行建树
点击查看代码
void build(int p.int l,int r){
t[p].l=l;//保存左节点
t[p].r=r;//保存右节点
if(l==r){//l==r是区间中元素唯一情况,对应线段树中叶节点
t[p].dat=a[l];//元素唯一,直接保存
}
int mid=(l+r)/2;//二分建立左右节点,保存相应区间
build(p*2,l,mid);//p*2为左子节点,对应保存靠左侧的区间
build(p*2+1,mid+1,r)//p*2+1为右子节点,对应保存右区间,注意起点为mid+1防止重复
t[p].dat=max(t[p*2].dat,t[p*2+1].data)//两段区间对应的最大值即为该区间最大值,向上递归返回信息
}
单点修改,从整个区间递归查找,查找到后修改该区间的值,同时
不要忘记向上递归修改相关节点的值!
Code:仍以最大值为例
点击查看代码
void change(int p,int x,int v){
if(t[p].l==t[p].r){
t[p].dat=v;
return;
}
int mid=(t[p].l+t[p].r)/2;
if(x<=mid){
change(p*2,x,v);
}
else{
change(p*2+1,x,v);
}
t[p].dat=max(t[p*2].dat,t[p*2+1].dat);
}
1.区间最值
例题:
延绵的山峰
求区间最值(不修改)
题面:有一座延绵不断、跌宕起伏的山,最低处海拔为0,最高处海拔不超过8848米,从这座山的一端走到另一端的过程中,每走1米海拔就升高或降低1米。有Q个登山队计划在这座山的不同区段登山,当他们攀到各自区段的最高峰时,就会插上队旗。请你写一个程序找出他们插旗的高度。
自打的暴力非常弱
#include <bits/stdc++.h>
using namespace std;
const int p=1e6+10;
struct node{
int id;
int exp;
}a[p];
int q,n,x,y;
inline int read(){
int s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
return s*w;
}
inline void in(){
n=read();
for(register int i=0;i<=n;i++){
a[i].exp=read();
a[i].id=i;
}
q=read();
}
bool cmp(const node &x,const node &y){
return x.exp>y.exp;
}
void work(){
int ans;
sort(a,a+n,cmp);//这里离散化找最大值
for(register int i=1;i<=q;i++){
x=read();
y=read();
for(register int j=0;j<=n;j++){//最坏情况下就是qn,当然最好直接是个q。。。
if(x<=a[j].id&&y>=a[j].id){
printf("%d\n",a[j].exp);
break;
}
}
}
}
int main(){
in();
work();
return 0;
}
正解代码(线段树)
# include <bits/stdc++.h>
#define lson (x<<1)
#define rson (x<<1|1)
#define mid ((z+y)>>1)
#define up c[x].sum=max(c[lson].sum,c[rson].sum)
using namespace std;
inline int r(){
int x=0,flag=1;char c=getchar();
while(c>'9'||c<'0'){if(c=='-')flag=-1;c=getchar();}
while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*flag;
}
const int da=1e6+1;int a[da];int n , m;
struct C{
int z, y, sum;
}c[da*4];
int cha (int x,int z,int y){
if(c[x].z>y||c[x].y<z)return 0;
if(z<=c[x].z&&c[x].y<=y){return c[x].sum;}
int ret=max(cha(lson,z,y),cha(rson,z,y));
return ret;
}
void build(int x,int z,int y){
c[x].z=z,c[x].y=y;
if(z==y){c[x].sum=a[z];return ;}
build (lson,z,mid);build (rson,mid+1,y);
up;
}
int main (){
n=r();for(int i=0;i<=n;i++){a[i]=r();}build (1,0,n);
m=r();
for(int i=1;i<=m;i++){
int ja=r(),jb=r();
printf("%d\n",cha(1,ja,jb));
}
return 0;
}
2.区间求和
如果直接修改和会很麻烦,而且可能查询不到,于是,我们有了一个新的操作,叫
标记延迟下传
修改时先打上标记,然后在查询将大区间的标记打到小区间(只打一次),最后传到单点上,由于不是立即下传增大的值(即不是立即修改区间内元素的值),而是打成标记,
也叫标记延迟下传
还有个标记永久化蒟蒻不会,咕了
打个模板
加和
#include <bits/stdc++.h>//线段树维护区间求和
using namespace std;//参考了蓝书自打一遍
#define ll long long //不开long long见祖宗
const int p=1e5+10;//数据规模
int a[p+10];//存原始数据
struct st{//线段树结构体
int l;//记录左端点
int r;//记录右端点
ll p;//记录和
ll add;//记录下传标记,延迟下传
}t[4*p];//数据规模:线段树开4倍大小
void build(int n,int l,int r){//建树:n:节点编号,l:左端点,r:右端点
t[n].l=l;//存左端点
t[n].r=r;//存右端点
if(l==r){//存到单点,此时该编号节点对应叶节点
t[n].p=a[l];//保存相应信息
return ;//向上返回
}
int mid=(l+r)/2;//二分区间
build(n*2,l,mid);//向下:左区间,该节点编号乘2为左区间节点编号
build(n*2+1,mid+1,r);//同理,该编号*2+1为右编号
t[n].p=t[n*2].p+t[n*2+1].p;//左右区间信息加和为该区间和,保存该区间信息并向上返回
}
void sp(int n){//延迟标记
if(t[n].add){//有标记
t[n*2].p+=t[n].add*(t[n*2].r-t[n*2].l+1);//左区间和更新
t[n*2+1].p+=t[n].add*(t[n*2+1].r-t[n*2+1].l+1);//右区间和更新
t[n*2].add+=t[n].add;//左区间标记下传
t[n*2+1].add+=t[n].add;//右区间标记下传
t[n].add=0;//下传标记完成,清空标记即可
}
}
void change(int n,int x,int y,int z){//修改,n编号,x左端点,y右端点,z增幅
if(x<=t[n].l&&y>=t[n].r){//区间规模小于修改规模
t[n].p+=(ll)z*(t[n].r-t[n].l+1);//先把和加上
t[n].add+=z;//打上延迟标记
return ;//向上返回即可
}
sp(n);//及时下传标记,小区间更新要用
int mid=(t[n].l+t[n].r)/2;//二分当前节点区间
if(x<=mid){//左侧需要更新
change(n*2,x,y,z);//在左区间(即n*2节点代表区间)执行更新操作,该次更新左右端点和增幅
}
if(y>mid){//右侧需要更新
change(n*2+1,x,y,z);//同理,右侧区间更新
}
t[n].p=t[n*2].p+t[n*2+1].p;//维护新的区间和
}
ll ques(int n,int x,int y){//查询,n编号,x左端点,y右端点
if(x<=t[n].l&&y>=t[n].r){//区间被查询范围覆盖
return t[n].p;//直接返回区间信息即可
}
sp(n);//下传标记
int mid=(t[n].l+t[n].r)/2;//二分区间
ll ans=0;//定义非单点区间对应的返回值
if(x<=mid){//左侧被覆盖
ans+=ques(n*2,x,y);//查询左侧值并将左侧返回值加入返回值中
}
if(y>mid){//右侧被覆盖
ans+=ques(n*2+1,x,y);//同理,加入右侧值
}
return ans;//向上返回值
}
int main(){//主函数
int n,m;//定义题目变量
scanf("%d%d",&n,&m);//输入
for(int i=1;i<=n;i++){//依据区间规模进行循环,执行输入操作
scanf("%d",&a[i]);//输入区间信息
}
build(1,1,n);//从1到n执行建树操作
for(int i=1;i<=m;i++){//执行m条指令
int cmd,x,y,z;//定义指令信息
scanf("%d",&cmd);//输入指令类型
if(cmd==1){//修改
scanf("%d%d%d",&x,&y,&z);//输入修改信息
change(1,x,y,z);//修改
}
else{//查询
scanf("%d%d",&x,&y);//输入查询信息
ll ans=ques(1,x,y);//定义答案
printf("%lld\n",ans);//输出答案
}
}
return 0;//结束,你会了
}
附赠一份乘积与加和的维护
乘积与加和
#include <bits/stdc++.h>//线段树维护乘积与加和
using namespace std;
#define ll long long
const int maxn=1e5+10;
ll a[maxn],n,m,d;
struct st{
ll s;
ll m;
ll a;
ll l;
ll r;
}t[maxn*4+10];
ll read(){
ll x=0,y=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-'){
y=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+ch-'0';
ch=getchar();
}
return x*y;
}
void build(int p,int l,int r){//建树
t[p].l=l;
t[p].r=r;
t[p].m=1;
if(l==r){
t[p].s=a[l]%d;
return ;
}
ll mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
t[p].s=(t[p*2].s+t[p*2+1].s)%d;
}
/*重点说一下标记操作:
1.乘积与加和的标记分开维护,否则人为增加思考量
2.由四则运算法则我们知道,先维护乘积下传再维护加和下传,加和时不用考虑维护乘积
3.乘积初始化为1,加和初始化为0
4.模数时原有数据最后进行取模,加和标记要先取模
5.变更标记要取模
6.开long long
*/
void spread(ll p){
t[p*2].s=(ll)(t[p].m*t[p*2].s+((t[p*2].r-t[p*2].l+1)*t[p].a)%d)%d;
t[p*2+1].s=(ll)(t[p].m*t[p*2+1].s+((t[p*2+1].r-t[p*2+1].l+1)*t[p].a)%d)%d;
t[p*2].m=(ll)(t[p*2].m*t[p].m)%d;
t[p*2+1].m=(ll)(t[p*2+1].m*t[p].m)%d;
t[p*2].a=(ll)(t[p*2].a*t[p].m+t[p].a)%d;
t[p*2+1].a=(ll)(t[p*2+1].a*t[p].m+t[p].a)%d;
t[p].m=1;
t[p].a=0;
}
void add(ll p,ll l,ll r,ll k){
if(t[p].l>=l&&t[p].r<=r){
t[p].a=(t[p].a+k)%d;
t[p].s=(ll)(t[p].s+k*(t[p].r-t[p].l+1))%d;
return ;
}
spread(p);
t[p].s=(t[p*2].s+t[p*2+1].s)%d;
ll mid=(t[p].l+t[p].r)/2;
if(l<=mid){
add(p*2,l,r,k);
}
if(r>mid){
add(p*2+1,l,r,k);
}
t[p].s=(t[p*2].s+t[p*2+1].s)%d;
}
void mul(ll p,ll l,ll r,ll k){
if(t[p].l>=l&&t[p].r<=r){
t[p].a=(t[p].a*k)%d;
t[p].m=(t[p].m*k)%d;
t[p].s=(t[p].s*k)%d;
return ;
}
spread(p);
t[p].s=(t[p*2].s+t[p*2+1].s)%d;
ll mid=(t[p].l+t[p].r)/2;
if(l<=mid){
mul(p*2,l,r,k);
}
if(r>mid){
mul(p*2+1,l,r,k);
}
t[p].s=(t[p*2].s+t[p*2+1].s)%d;
}
ll ask(ll p,ll l,ll r){
if(t[p].l>=l&&t[p].r<=r){
return t[p].s;
}
spread(p);
ll ans=0,mid=(t[p].l+t[p].r)/2;
if(l<=mid){
ans=(ans+ask(p*2,l,r))%d;
}
if(r>mid){
ans=(ans+ask(p*2+1,l,r))%d;
}
return ans;
}
int main(){
cin>>n>>m>>d;
for(int i=1;i<=n;i++){
a[i]=read();
}
build(1,1,n);
for(int i=1;i<=m;i++){
int ty=read();
if(ty==1){
ll cn=read(),cm=read(),cw=read();
mul(1,cn,cm,cw);
}else if(ty==2){
ll cn=read(),cm=read(),cw=read();
add(1,cn,cm,cw);
}else {
ll cn=read(),cm=read();
cout<<ask(1,cn,cm)<<endl;
}
}
return 0;
}
扫描线
扫描线求面积并
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int o=2222222;
struct myline{
ll xa;
ll xb;
ll y;
bool b;
bool operator<(myline a)const{
return y<a.y;
}
}l[o];
struct node{
int l;
int r;
ll len;
ll cnt;
}t[o];
int lc,n,cnt;
ll a[o],xa,ya,xb,yb,ans;
void build(int p,int l,int r){//建树
t[p].l=l;
t[p].r=r;
if(l==r){
return ;
}
int mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
}
void pushup(int p){
if(t[p].cnt){
t[p].len=a[t[p].r+1]-a[t[p].l];
}
else{
t[p].len=t[p*2].len+t[p*2+1].len;
}
}
void change(int p,int l,int r,int v){
if(t[p].l>=l&&t[p].r<=r){
t[p].cnt+=v;
pushup(p);
return ;
}
int mid=(t[p].l+t[p].r)/2;
if(l<=mid){
change(p*2,l,r,v);
}
if(r>mid){
change(p*2+1,l,r,v);
}
pushup(p);
}
void in(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%lld%lld%lld%lld",&xa,&ya,&xb,&yb);
l[++lc].xa=xa;
l[lc].xb=xb;
l[lc].y=ya;
l[lc].b=0;
l[++lc].xa=xa;
l[lc].xb=xb;
l[lc].y=yb;
l[lc].b=1;
a[i]=xa;
a[i+n]=xb;
}
}
void pre(){
sort(a+1,a+2*n+1);
cnt=unique(a+1,a+2*n+1)-a-1;
sort(l+1,l+2*n+1);
build(1,1,cnt-1);
}
void work(){
for(int i=1;i<=2*n;i++){
myline nowline=l[i];
if(i>1){
ans+=t[1].len*(l[i].y-l[i-1].y);
}
if(nowline.b){
change(1,lower_bound(a+1,a+cnt+1,nowline.xa)-a,lower_bound(a+1,a+cnt+1,nowline.xb)-a-1,-1);
}
else{
change(1,lower_bound(a+1,a+cnt+1,nowline.xa)-a,lower_bound(a+1,a+cnt+1,nowline.xb)-a-1,1);
}
}
}
void out(){
cout<<ans;
}
int main(){
in();
pre();
work();
out();
return 0;
}
扫描线求周长并
#include <iostream>
#include <stdio.h>
#include <algorithm>
#define lson (x << 1)
#define rson (x << 1 | 1)
using namespace std;
const int MAXN = 2e4;
int n, X[MAXN << 1];
int x1, y1, x2, y2, pre = 0; /* 先初始化为 0 */
struct ScanLine {
int l, r, h, mark;
if(h == rhs.h)
return mark > rhs.mark;
return h < rhs.h;
// 注意!这里是后来被 hack 掉以后加上去的
// 在此感谢 @leprechaun_kdl 指出问题
// 如果出现了两条高度相同的扫描线,也就是两矩形相邻
// 那么需要先扫底边再扫顶边,否则就会多算这条边
// 这个对面积并无影响但对周长并有影响
// hack 数据:2 0 0 4 4 0 4 4 8 输出应为:24
} line[MAXN];
struct SegTree {
int l, r, sum, len, c;
// c表示区间线段条数
bool lc, rc;
// lc, rc分别表示左、右端点是否被覆盖
// 统计线段条数(tree[x].c)会用到
} tree[MAXN << 2];
void build_tree(int x, int l, int r) {
tree[x].l = l, tree[x].r = r;
tree[x].lc = tree[x].rc = false;
tree[x].sum = tree[x].len = 0;
tree[x].c = 0;
if(l == r)
return;
int mid = (l + r) >> 1;
build_tree(lson, l, mid);
build_tree(rson, mid + 1, r);
}
void pushup(int x) {
int l = tree[x].l, r = tree[x].r;
if(tree[x].sum) {
tree[x].len = X[r + 1] - X[l];
tree[x].lc = tree[x].rc = true;
tree[x].c = 1;
// 做好相应的标记
}
else {
tree[x].len = tree[lson].len + tree[rson].len;
tree[x].lc = tree[lson].lc, tree[x].rc = tree[rson].rc;
tree[x].c = tree[lson].c + tree[rson].c;
// 如果左儿子左端点被覆盖,那么自己的左端点也肯定被覆盖;右儿子同理
if(tree[lson].rc && tree[rson].lc)
tree[x].c -= 1;
// 如果做儿子右端点和右儿子左端点都被覆盖,
// 那么中间就是连续的一段,所以要 -= 1
}
}
void edit_tree(int x, int L, int R, int c) {
int l = tree[x].l, r = tree[x].r;
if(X[l] >= R || X[r + 1] <= L)
return;
if(L <= X[l] && X[r + 1] <= R) {
tree[x].sum += c;
pushup(x);
return;
}
edit_tree(lson, L, R, c);
edit_tree(rson, L, R, c);
pushup(x);
}
ScanLine make_line(int l, int r, int h, int mark) {
ScanLine res;
res.l = l, res.r = r,
res.h = h, res.mark = mark;
return res;
}
// POJ 不这样做就会CE,很难受
int main() {
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
scanf("%d %d %d %d", &x1, &y1, &x2, &y2);
line[i * 2 - 1] = make_line(x1, x2, y1, 1);
line[i * 2] = make_line(x1, x2, y2, -1);
X[i * 2 - 1] = x1, X[i * 2] = x2;
}
n <<= 1;
sort(line + 1, line + n + 1);
sort(X + 1, X + n + 1);
int tot = unique(X + 1, X + n + 1) - X - 1;
build_tree(1, 1, tot - 1);
int res = 0;
for(int i = 1; i < n; i++) {
edit_tree(1, line[i].l, line[i].r, line[i].mark);
res += abs(pre - tree[1].len);
pre = tree[1].len;
// 统计横边
res += 2 * tree[1].c * (line[i + 1].h - line[i].h);
// 统计纵边
}
res += line[n].r - line[n].l;
// 特判一下枚举不到的最后一条扫描线
printf("%d", res);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具