线段树、主席树知识点

线段树空间

假设有 n n n 个点,那么会分配 ⌈ l o g 2 n ⌉ \lceil log_2n \rceil log2n 层空间的满二叉树来作为线段树的空间。
x x x 层最大的位置为 2 x + 1 − 1 2^{x+1}-1 2x+11。第 x + 1 x+1 x+1 层,最大的位置为 2 x + 2 − 1 2^{x+2}-1 2x+21
现在假设 2 x < n ≤ 2 x + 1 2^x < n \le 2^{x+1} 2x<n2x+1,那么 n 个节点的线段树会分配 x + 1 x + 1 x+1 层。它的最大位置 p 应该满足: 2 x + 1 − 1 < p ≤ 2 x + 2 − 1 2^{x+1}-1 <p \le 2^{x+2}-1 2x+11<p2x+21。因此在极端情况下,p 应该取到 n 的 4 倍大小,才能够保证满足最大空间

线段树要点

  • build 主要清空信息、最后回溯维护性质(max/min/sum)
  • update 区间更新使用lazy,往下走需pushDown,往回走pushUp
  • query 查询整棵树的信息就走到每一个叶节点。往下走需pushDown,没修改不需要回溯

动态开点

普通线段树都是直接开 4 倍空间,然后 l s ls ls 等于 r t × 2 rt \times 2 rt×2 r s rs rs 等于 r t × 2 + 1 rt\times 2 +1 rt×2+1。动态开点是用 l s ls ls r s rs rs 两个数组来记录 r t rt rt 的左右孩子。每次需要更新的时候才动态地分配空间。

  • 因此在查询的时候,会遇到 r t rt rt 为 0 的情况,也就是查询到某个点或者区间时不存在。此时就根据题意返回想要的数据即可
  • 对于主席树,也是动态开点的一种,不过我写主席树,一般会建一棵空树,然后再动态开点,更新每一个版本。这样就不会访问到空点的情况。

用途:对于值域为 inf ,但是操作很少的情况,可以使用动态开点,每次操作最多会访问 l o g 2 ( i n f ) log_2 (inf) log2(inf) 个节点,一共 q 次操作,时空复杂度为 q l o g 2 ( i n f ) q log_2 (inf) qlog2(inf)

注意点

  • update 回溯的时候会访问到空点需判断
  • query 的时候会访问到空点需判断

P3372 【模板】线段树 1

动态开点写法,只开了3倍空间

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=3e5+10,maxm=1e6+5;

int n,m,a[maxn];
ll st[maxn];
int lazy[maxn],ls[maxn],rs[maxn],no,root;

void pushUp(int rt)
{
    st[rt]=st[ls[rt]]+st[rs[rt]];
}
void pushDown(int rt,int L,int R)
{
    if(lazy[rt])
    {
        int mid=(L+R)>>1;
        st[ls[rt]]+=(mid-L+1)*lazy[rt];
        st[rs[rt]]+=(R-mid)*lazy[rt];
        lazy[ls[rt]]+=lazy[rt];
        lazy[rs[rt]]+=lazy[rt];
        lazy[rt]=0;
    }
}

void build(int &rt,int L,int R)
{
    if(!rt) rt=++no;
    if(L==R)
    {
        st[rt]=a[L];
        return;
    }
    int mid=(L+R)>>1;
    build(ls[rt],L,mid);
    build(rs[rt],mid+1,R);
    pushUp(rt);
}

void update(int &rt,int l,int r,int L,int R,int val)
{
    if(!rt) rt=++no;
    if(l<=L&&R<=r)
    {
        st[rt]+=(R-L+1)*val;
        lazy[rt]+=val;
        return;
    }
    pushDown(rt,L,R);
    int mid=(L+R)>>1;
    if(l<=mid) update(ls[rt],l,r,L,mid,val);
    if(r>mid) update(rs[rt],l,r,mid+1,R,val);
    pushUp(rt);
}
ll query(int rt,int l,int r,int L,int R)
{
	if(!rt) return 0;
    if(l<=L&&R<=r) return st[rt];
    pushDown(rt,L,R);
    ll ans=0;
    int mid=(L+R)>>1;
    if(l<=mid) ans+=query(ls[rt],l,r,L,mid);
    if(r>mid) ans+=query(rs[rt],l,r,mid+1,R);
    return ans;
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i) scanf("%d",&a[i]);
    build(root,1,n);
    int op,x,y,k;
    while(m--)
    {
        scanf("%d",&op);
        if(op==1)
        {
            scanf("%d%d%d",&x,&y,&k);
            update(root,x,y,1,n,k);
        }
        else if(op==2)
        {
            scanf("%d%d",&x,&y);
            printf("%lld\n",query(root,x,y,1,n));
        }
    }
    return 0;
}

lazy

  • 使用 l a z y lazy lazy 的情况:维护区间异或、维护区间和、染色

update

  • 单点更新围绕一个点 p p p ,区间更新围绕 [ l , r ] [l,r] [l,r],并且可以加一个lazy
//单点更新
void update(int rt,int p,int L,int R,int val)
{
    if(L==R)
    {
        st[rt]=val;
        return;
    }
    int mid=(L+R)>>1;
    if(p<=mid) update(ls,p,L,mid,p);
    if(p>mid) update(rs,p,mid+1,R,p);
    pushUp(rt);
}
//区间更新:维护翻转操作
void update(int rt,int l,int r,int L,int R,int val)
{
    if(l<=L&&R<=r)
    {
        st[rt]=R-L+1-st[rt];
        lazy[rt]^=val;
        return;
    }
    pushDown(rt,L,R);
    int mid=(L+R)>>1;
    if(l<=mid) update(ls,l,r,L,mid,val);
    if(r>mid) update(rs,l,r,mid+1,R,val);
    pushUp(rt);
}

query

//单点查询:查询单点值
int query(int rt,int p,int L,int R)
{
    if(L==R) return st[rt];
    int mid=(L+R)>>1;
    if(p<=mid) return query(ls,p,L,mid);
    if(p>mid) return query(rs,p,mid+1,R);
}
//区间查询:查询区间和
int query(int rt,int l,int r,int L,int R)
{
    if(l<=L&&R<=r) return st[rt];
    pushDown(rt,L,R);
    int mid=(L+R)>>1;
    int ans=0;
    if(l<=mid) ans+=query(ls,l,r,L,mid);
    if(r>mid) ans+=query(rs,l,r,mid+1,R);
    return ans;
}

离散化

  • 线段树中维护的是段,一个叶节点表示一段的信息。需要注意是找准一段的左右坐标 [ l , r ]
  • 从维护一个变成维护一块,然后把给定的区间变成坐标即可:将块的左右两端 [ l , r ] [l,r] [l,r] ,然后记录为 l − 1 l-1 l1 r r r
  • 明确当前维护块的左右坐标: l i l_i li r i r_i ri 就没什么问题了
  • 自己习惯的设法:假设当前块为 L ,那么它的区间为 x [ L − 1 ] , x [ L ] x[L-1],x[L] x[L1],x[L]

总之:维护段,明确段的两个坐标
离散化操作

sort(allx.begin(),allx.end());
allx.resize(unique(allx.begin(),allx.end())-allx.begin());

对于主席树的理解

  • 首先按时间序,将序列中的每个数按权值更新到主席树上特定位置。然后主席树维护区间和。
  • 主席树是 n 颗权值线段树做前缀和的结果,自己在分析的时候,可以看成是独立的,具体在做题的时候,得看成是前缀和。
  • 可以查询到小于等于(大于等于) k 的个数、第 k 大、大于(小于) k 的第一个数
  • 主席树上查询:抓住两个关键点: 1、p 与 mid 的大小 2、左右子树是否存在值,存在值再往下搜
  • 通常遇见区间和可以直接累加

题型

1、染色:

  • 做法:可以使用 l a z y lazy lazy 标记(这里 st 就表示lazy)。把区间有多种颜色或者没有颜色标记为 -1,从左往右扫的时候,当遇到 st 不为 -1 ,说明这段颜色统一,可以直接统计后返回,而不用往下扫。
  • 统计区间上出现过哪些颜色?标记上次出现的颜色,然后用 v i s i t visit visit 标记一下出现过的颜色就好了。
  • 统计区间上每种颜色出现的段数?标记上次出现的颜色,当前和上次的颜色不同,直接让 m p [ x ] mp[x] mp[x] ++ 即可
int last,ans;
void query(int rt,int L,int R)
{
    if(st[rt]!=-1&&last!=st[rt])
    {
        if(!visit.count(st[rt]))
            ans++,visit[st[rt]]=1;
        last=st[rt];
        return;
    }
    if(L==R)
    {
        last=st[rt];
        return;
    }
    pushDown(rt);
    int mid=(L+R)>>1;
    query(ls,L,mid);
    query(rs,mid+1,R);
}

2、给定一个序列 n ,然后在给定区间内,查找第一个小于 i 的数
这里有三种方法:线段树可以做边更新边查询的操作有点在线的意思,而主席树就可以直接全部更新完之后查询。

  • 滑动窗口:这里比较特殊, a i a_i ai 是序列上的一个数,可以用set 维护一个长度为 k 的区间,然后搜出左边 k 个中的小于 a i a_i ai 最大值,右边 k 个中的小于 a i a_i ai 最大值,最后两边取最大值即可
  • 线段树:先将小于自己的数先更新到线段树上,然后就查询这个区间上的最大值,就找到了。边更新边寻找答案。对自己答案有贡献的做更新,有影响的先不更新。
  • 主席树:在主席树上区间 [ l , r ] [l,r] [l,r] 表现为时间序,小于 i i i 表现为 i 在主席树上的位置 p 左边第一个有数的位置,即从位置 p -1 找到 1 。

主席树上查找 p p p 左边第一个数,即小于 p 的第一个数

int query(int pre,int now,int p,int L,int R)
{
    if(st[now]-st[pre]==0) return 0;
    if(L==R) return L<=p-1?L:0;
    int mid=(L+R)>>1;
    if(p-1<=mid||st[rs[now]]-st[rs[pre]]==0)
        return query(ls[pre],ls[now],p,L,mid);
    int res=query(rs[pre],rs[now],p,mid+1,R);
    if(res!=0) return res;
    return query(ls[pre],ls[now],p,L,mid);
}

3、在区间上找一个最小的 ≥ k \ge k k的数,与上面类似
抓住两个关键点: p 与 mid 的大小,左右子树是否存在值,存在值再往下搜

int query(int pre,int now,int p,int L,int R)
{
    if(st[now]-st[pre]==0) return inf;
    if(L==R) return L>=p?L:inf;
    int mid=(L+R)>>1;
    if(p>=mid+1||st[ls[now]]-st[ls[pre]]==0)
        return query(rs[pre],rs[now],p,mid+1,R);
    int res=query(ls[pre],ls[now],p,L,mid);
    if(res!=inf) return res;
    return query(rs[pre],rs[now],p,mid+1,R);
}
int Query(int pre,int now,int p,int L,int R)
{
    if(L==R)
        return L;
    int mid=(L+R)>>1;
    int cnt1=ST[ls[now]]-ST[ls[pre]],cnt2=ST[rs[now]]-ST[rs[pre]];
    int ans=INF;
    if(p<=mid&&cnt1)
        ans=Query(ls[pre],ls[now],p,L,mid);
    if(ans!=INF)
        return ans;
    if(cnt2)
        ans=Query(rs[pre],rs[now],p,mid+1,R);
    return ans;
}

4、在区间上查找所有 ≥ k \ge k k 的和,k 的权值为 p
所有 ≥ p \ge p p 的都可以直接累加

//cnt是区间个数,sum是区间和
pll query(int pre,int now,int p,int L,int R)
{
	if(L==R) return {st[now].cnt-st[pre].cnt,st[now].sum-st[pre].sum};
	int mid=(L+R)>>1;
	if(p<=mid)
	{
		pll ans=query(ls[pre],ls[now],p,L,mid);
		ans.fi+=st[rs[now]].cnt-st[rs[pre]].cnt;
		ans.se+=st[rs[now]].sum-st[rs[pre]].sum;
		return ans;
	}
	else return query(rs[pre],rs[now],p,mid+1,R);
}

其实还有更简单的写法:直接用区间查询,查询区间 [ p , t o t ] [p,tot] [p,tot] 的和即可

pll query2(int pre,int now,int l,int r,int L,int R)
{
	if(l<=L&&R<=r)
		return {st[now].cnt-st[pre].cnt,st[now].sum-st[pre].sum};
	int mid=(L+R)>>1;
	pll ans1={0,0},ans2={0,0};
	if(l<=mid)
		ans1=query2(ls[pre],ls[now],l,r,L,mid);
	if(r>mid)
		ans2=query2(rs[pre],rs[now],l,r,mid+1,R);
	return {ans1.fi+ans2.fi,ans1.se+ans2.se};
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值