【超好懂的数据结构】线段树基础~(update:11/13)


title : 线段树基础
date : 2021-8-15
tags : ACM,数据结构


在这里插入图片描述

线段树

线段树基础

首先上个板子来复习一下线段树的基本写法。

//基础板 P3372 【模板】线段树 1
#include<bits/stdc++.h>
using namespace std;
int n,m,l,r,k,q; 
long long arr[100005],tree[270000],lazy[270000];

void build(int node,int l,int r){  //建树 
	if(l==r){
		tree[node]=arr[l];
		return;
	}
	int mid=(l+r)/2;
	build(node*2,l,mid); //左区间建树 
	build(node*2+1,mid+1,r); //右区间建树 
	tree[node]=tree[node*2]+tree[node*2+1]; //区间和 
}

void pushdown(int node,int start,int end){ //下传操作 
	int mid=(start+end)/2;
	if(lazy[node]){ 
		tree[node*2]+=lazy[node]*(mid-start+1); //更新区间和 
		tree[node*2+1]+=lazy[node]*(end-mid);
		lazy[node*2]+=lazy[node]; //懒标记下传 
		lazy[node*2+1]+=lazy[node];
	}
	lazy[node]=0;
}

void update(int node,int start,int end,int l,int r,int c){ //更新操作 
	if(l<=start&&end<=r){ //如果区间在更新范围内,直接标记返回 
		tree[node]+=(end-start+1)*c; //区间和加上Len倍的c 
		lazy[node]+=c; //打标记 
		return;
	}
	int mid=(start+end)/2;
	pushdown(node,start,end); //下传 
	if(l<=mid){update(node*2,start,mid,l,r,c);} //更新左区间 
	if(r>mid){update(node*2+1,mid+1,end,l,r,c);}//更新右区间 
	tree[node]=tree[node*2]+tree[node*2+1];  //pushup
}

long long query(int node,int start,int end,int l,int r){ //查询操作 
	int mid=(start+end)/2;
	if(l<=start&&end<=r) return tree[node]; //如果在查询范围内,直接返回 
	pushdown(node,start,end);
	long long sum=0;
	if(l<=mid) sum=query(node*2,start,mid,l,r); //查询左区间 
	if(r>mid) sum+=query(node*2+1,mid+1,end,l,r); //查询右区间 
	return sum;
}

int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&arr[i]);
	build(1,1,n);  
	while(m--){
		scanf("%d%d%d",&q,&l,&r);
		if(q==1){
			scanf("%d",&k);
			update(1,1,n,l,r,k); //区间修改 
		}else printf("%lld\n",query(1,1,n,l,r)); //区间查询 
	}
	return 0;
}
延迟标记

延迟标记,也叫lazy tag,是在区间中新增一个标记,在下一次访问该区间时,向左右区间下放(pushdown)节点的标记,以便完成区间修改+区间询问。

区间染色

例题:POJ 2528 Mayor’s posters

区间离散化

对于区间[1,5],[2,7],[7,100],[3,1e7],我们肯定不能直接对区间[1,1e7]进行修改,而是应该先进行排序并离散化,1->1,2->2,3->3,5->4,7->5,100->6,1e7->7,之后区间可以表示为[1,4],[2,5],[5,6],[3,7]。然而这样的表示实际上扩大了访问的区间,因此我们要在间隔大于1的两个元素中间再加数字,正确的表示方法如下:
x [ 1 ] = 1 , x [ 2 ] = 2 , x [ 3 ] = 3 , x [ 4 ] = 4 , x [ 5 ] = 5 , x [ 6 ] = 6 , x [ 7 ] = 7 , x [ 8 ] = 100 , x [ 9 ] = 101 , x [ 10 ] = 1 e 7 x[1]=1,x[2]=2,x[3]=3,x[4]=4,x[5]=5,x[6]=6,x[7]=7,x[8]=100,x[9]=101,x[10]=1e7 x[1]=1,x[2]=2,x[3]=3,x[4]=4,x[5]=5,x[6]=6,x[7]=7,x[8]=100,x[9]=101,x[10]=1e7
发现了新增的节点x[4],x[6]和[x8],这有什么用呢?

比如我要涂色[1,3]->颜色1,[5,7]->颜色2,加点前[1,3]->颜色1,[4,5]颜色2,可以发现最终两种颜色把[1,5]覆盖掉了,事实上中间还有一片(3,4)颜色未处理;我们增加节点后的效果是涂掉[1,3],[5,7],那么数颜色数量的时候就能答案就修正了。

//Mayor's posters
#include<iostream>
#include<vector>
#include<algorithm>
#define MID int mid=(p->l + p->r)>>1
using namespace std;
struct Post{ // 海报
    int l,r;
} pst[10100];
int nodenum,cnt,ans;
int t,n,hs[10000010];
vector<int>vt;

struct Node{
    int l,r;
    bool full; // 区间[l,r]是否被完全覆盖
    Node *ls, *rs; 
}tr[1000000];

void buildTree(Node *p, int l, int r){ //建树 
    p->l=l;
    p->r=r;
    p->full=0; //初始化节点 
    if(l==r)return; //如果是叶子,直接返回 
    nodenum++;
    p->ls=tr+nodenum; //建立左右子树 
    nodenum++;
    p->rs=tr+nodenum;
    MID;
    buildTree(p->ls,l,mid); //左区间建树 
    buildTree(p->rs,mid+1,r); //右区间建树 
}
bool check(Node *p,int l,int r){ //判断该区间是否有覆盖 
    if (p->full) return false; //已经被覆盖了,这份海报就看不到了 
    if (p->l==l&&p->r==r){
        p->full=1; //覆盖此区间 
        return true;
    }
    bool res;
    MID;
    if(r<=mid) res=check(p->ls,l,r); //全在左区间,找左子树 
    else if(l>mid) res=check(p->rs,l,r);
    else{
        bool b1=check(p->ls,l,mid);
        bool b2=check(p->rs,mid+1,r);
        res=b1||b2; //其中一点没覆盖即可 
    }
    if (p->ls->full&&p->rs->full){
    	p->full=1;//如果左右区间都被覆盖,那么这个区间也被覆盖了 
	}
    return res;
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
    cin>>t;
    while(t--){
    	cin>>n;
    	nodenum=0,cnt=0,ans=0; //清空数据 
        for(int i=0;i<n;i++) {
        	cin>>pst[i].l>>pst[i].r;
        	vt.push_back(pst[i].l); //把区间端点放入容器 
        	vt.push_back(pst[i].r);
        }
        sort(vt.begin(),vt.end());//排序 
        vt.erase(unique(vt.begin(),vt.end()),vt.end()); //去重 
        for(int i=0;i<vt.size();i++){
            hs[vt[i]]=cnt++; //记录元素所在数的节点编号 
            if(i<vt.size()-1){
                if(vt[i+1]-vt[i]>1) cnt++; //中间再插一个点 
            }
        } 
        buildTree(tr,0,cnt);//开始建树 
        for(int i=n-1;i>=0;i--){ //这里倒序,要从没被覆盖的开始数 
            if(check(tr,hs[pst[i].l],hs[pst[i].r])){
            	ans++; //可见海报增加 
			}
        }
        cout<<ans<<endl;
    }
    return 0;
}

区间第K大

由于篇幅问题这个问题另开一篇主席树的博文讲吧。

扫描线线段树

扫描线算法可用于解决多个矩形围成的周长和面积问题。

扫描线算法

用一根线对图像从下往上进行扫描,扫描到边的时候对答案进行计算。相当于给妙计分层,然后计算每一层的面积,最后汇总到答案当中。

矩阵面积并

给定多个矩形,边一定平行于x或y轴。求所有矩形的面积之和。
( 1 ) 每 个 矩 形 的 上 下 边 加 入 扫 描 线 并 标 记 , 下 边 标 记 为 1 , 上 边 标 记 为 − 1 ( 2 ) 让 扫 描 线 从 从 低 到 高 排 序 , 而 矩 形 的 左 右 边 从 小 到 大 排 序 ( 3 ) 考 虑 数 据 范 围 , 我 们 进 行 离 散 化 处 理 ( 3 ) 建 立 线 段 树 , 那 么 区 间 可 表 示 为 区 域 的 y 1 , y 2 , 以 及 长 度 。 ( 4 ) 遍 历 所 有 相 邻 左 右 边 , 每 次 通 过 线 段 树 询 问 出 高 度 , 然 后 面 积 相 加 。 (1)每个矩形的上下边加入扫描线并标记,下边标记为1,上边标记为-1\\ (2)让扫描线从从低到高排序,而矩形的左右边从小到大排序\\ (3)考虑数据范围,我们进行离散化处理\\ (3)建立线段树,那么区间可表示为区域的y1,y2,以及长度。\\ (4)遍历所有相邻左右边,每次通过线段树询问出高度,然后面积相加。\\ (1)线11(2)线(3)(3)线y1,y2(4)线
注意我们这里要维护的是线段长度而非点的值,所以我们要对线段树进行修改,即左孩子的右值=右孩子的左值,这样才能使维护不出现缝隙。

// luoguP5490 【模板】扫描线
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+7;

struct L{
	ll x,y1,y2,flag;
	L(ll X=0,ll Y1=0,ll Y2=0,ll T=0){
		x=X,y1=Y1,y2=Y2,flag=T;
	}
}line[N<<1];

struct node{
	int l,r;
	ll len;
	ll lazy;
}t[N<<2];

int n,cnt;
ll seg[N];
map<ll,int>val;

ll len(int p){
	//获得区域p的高度,即两扫描线相减 
	return seg[t[p].r+1]-seg[t[p].l]; 
}

void update(int p){
	if(t[p].lazy) t[p].len=len(p); //如果该区间被标记过 
	else if(t[p].l==t[p].r) t[p].len=0; //区间长度为0 
	else t[p].len=t[p<<1].len+t[p<<1|1].len; //两区间相加 
}

void build(int p,int l,int r){ 
	t[p].l=l; //建树时只需记录区间左右就可以了 
	t[p].r=r;
	if(l==r) return;
	int mid=l+r>>1;
	build(p<<1,l,mid); //对左边建树 
	build(p<<1|1,mid+1,r); //对右边建树 
}

void change(int p,int l,int r,int k){
	if(l<=t[p].l&&t[p].r<=r){ //如果区间在所求上下边中 
		t[p].lazy+=k; //打上标记 
		update(p); //更新区间长度 
		return;
	}
	int mid=t[p].l+t[p].r>>1;
	if(l<=mid) change(p<<1,l,r,k); //向下更新 
	if(r>mid) change(p<<1|1,l,r,k);  //向上更小 
	update(p); //最后要更新整个区间 
}

bool cmp(L a,L b){
	return a.x<b.x; //按照x坐标排序 
}

signed main(){
	ios::sync_with_stdio(0); //读入优化 
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++){
		ll x1,y1,x2,y2;
		cin>>x1>>y1>>x2>>y2;
		seg[++cnt]=y1; //加入扫描线 
		line[cnt]=L(x1,y1,y2,1); //加入竖线 
		seg[++cnt]=y2;
		line[cnt]=L(x2,y1,y2,-1);
	}
	sort(line+1,line+cnt+1,cmp); //对竖线排序 
	sort(seg+1,seg+1+cnt); //扫描线从低到高排序 
	int m=unique(seg+1,seg+1+cnt)-(seg+1); //去重 
	for(int i=1;i<=m;i++) val[seg[i]]=i; //离散化 
	build(1,1,m); //建树 
	ll ans=0; //记录答案 
	for(int i=1;i<cnt;i++){ //对于cnt-1条竖边 
		int x=val[line[i].y1],y=val[line[i].y2]-1; //区域的上下边 
		change(1,x,y,line[i].flag); //查询并更新区域的高 
		ans+=t[1].len*(line[i+1].x-line[i].x); //累加区域面积 
	}
	printf("%lld",ans); //输出答案 
	return 0;
}

区间最大子段和

GSS3 - Can you answer these queries III

#include<bits/stdc++.h>
//#define int long long
#define inf 0x7f7f7f7f
using namespace std;
const int N=1e5+7;
const int mod=1e9+7;
int n,m,idx=0,op,x,y;

#define MID int mid=l+r>>1;

struct Seg{
	int ls,rs,sum,mx;
}tr[N<<2];

void pushup(int p){
	tr[p].mx=max(tr[p<<1].mx,tr[p<<1|1].mx);
	tr[p].mx=max(tr[p].mx,tr[p<<1].rs+tr[p<<1|1].ls);
	tr[p].sum=tr[p<<1].sum+tr[p<<1|1].sum;
	tr[p].ls=max(tr[p<<1].ls,tr[p<<1].sum+tr[p<<1|1].ls);
	tr[p].rs=max(tr[p<<1|1].rs,tr[p<<1|1].sum+tr[p<<1].rs);	
}

void build(int p,int l,int r){
	if(l==r){
		cin>>tr[p].mx;
		tr[p].sum=tr[p].ls=tr[p].rs=tr[p].mx;
		return;
	}
	MID;
	build(p<<1,l,mid);
	build(p<<1|1,mid+1,r);
	pushup(p);
}

void update(int p,int l,int r,int pos,int val){
	if(l==r){
		tr[p].ls=tr[p].rs=tr[p].mx=tr[p].sum=val;
		return;	
	}
	MID;
	if(pos<=mid) update(p<<1,l,mid,pos,val);
	else update(p<<1|1,mid+1,r,pos,val);
	pushup(p);
}

Seg query(int p,int l,int r,int ql,int qr){ //查询最大字段和 
	if(ql<=l&&r<=qr) return tr[p];
	MID;
	if(qr<=mid) return query(p<<1,l,mid,ql,qr); 
	if(ql>mid) return query(p<<1|1,mid+1,r,ql,qr);
	Seg res,L=query(p<<1,l,mid,ql,mid),R=query(p<<1|1,mid+1,r,mid+1,qr);
	res.sum=L.sum+R.sum;
	res.ls=max(L.ls,L.sum+R.ls);
	res.rs=max(R.rs,R.sum+L.rs);
	res.mx=max(L.mx,R.mx);
	res.mx=max(res.mx,L.rs+R.ls);
	return res;
}

signed main(){
	scanf("%d",&n);
	build(1,1,n);
	scanf("%d",&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&op,&x,&y);
		if(op){
			if(x>y) x^=y^=x^=y;
			printf("%d\n",query(1,1,n,x,y).mx);
		}else{
			update(1,1,n,x,y);
		}
	}
	return 0;
}

动态开点

动态开点线段树可以避免离散化。

如果权值线段树的值域较大,离散化比较麻烦,可以用动态开点的技巧。

省略了建树的步骤,而是在具体操作中加入结点。

参考资料

https://oi-wiki.org/geometry/scanning/

https://mirasire.xyz/2019/11/17/SMX/

https://wmathor.com/index.php/archives/1176/

https://www.bilibili.com/video/BV1FU4y1t79g

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RWLinno

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值