线段树专题
线段树
由小区间更新大区间
局限性: 问题的答案必须是两个子区间的答案值运算可以得出
可以用于区间修改,区间查询最值, 最大子段和... 不能用于求解区间众数, 区间最长连续问题
-
上拉操作是线段树所有操作的基础,就是用左右儿子的信息更新自己的信息
-
分治建树
只有对给定好的数组计算的时候才需要buildTree, 有跟着整个数组扫一遍不需要buildTree
-
lazytag 是区间修改和查询保证时间复杂度的关键
用于表示某个节点o还有lz[o]的值尚未更新给子节点, 这个 值x节点个数=实际增加的权值
当lz[o]==0 说明当前节点的左右儿子都已经被更新了, 得到了正确的值
使用懒标记后必须配合下放操作, 若一个点的父亲链lz[o]都为0,则这个给节点的值准确
快速线段树(单点修改+区间最值)
注意n为2的幂次,下标从0开始,查询时左闭右开
struct Segt {
vector<int> w;
int n;
Segt(int n) : w(2 * n, (int)-2E9), n(n) {}
void modify(int pos, int val) {
for (w[pos += n] = val; pos > 1; pos /= 2) {
w[pos / 2] = max(w[pos], w[pos ^ 1]);
}
}
int ask(int l, int r) {
int res = -2E9;
for (l += n, r += n; l < r; l /= 2, r /= 2) {
if (l % 2) res = max(res, w[l++]);
if (r % 2) res = max(res, w[--r]);
}
return res;
}
};
int a[N];
void solve(){
Segt t(128);
int n;cin>>n;
rep(i,1,n)cin>>a[i];
rep(i,1,5){
t.modify(i-1,a[i]);//i-1
}
int l,r;cin>>l>>r;
cout<<t.ask(l-1,r);//l-1 r
}
基础线段树(区间加法和查询)
start, end, node 用来描述线段树, l r 用来描述查询区间
线段是确定的,因此 对于每个编号o(node),它的 start 和end 也是确定的, o==1时,s一定是1,e一定是n
const int N=2e5+5;
int n;
int a[N];
int t[N<<2],lz[N<<2];
#define ls (o<<1)
#define rs (o<<1|1)
void update(int s, int e, int o, int x){//把当前的懒标记更新到树上
t[o]+=x*(e-s+1);
lz[o]+=x;
}
void pushup(int o){
t[o]=t[ls] + t[rs];
}
void pushdown(int s, int e, int o){
if(lz[o]==0)return;
int mid=(s+e)>>1;//ls=o<<1, rs=o<<1|1;
update(s,mid,ls,lz[o]);//t[ls]+=lz[o]*(mid-s+1);lz[ls]+=lz[o];标记也要下放
update(mid+1,e,rs,lz[o]);//t[rs]+=lz[o]*(e-mid);lz[rs]+=lz[o];
lz[o]=0; //lazytag 下放完毕
}
void buildTree(int s=1, int e=n, int o=1){// start, end, node
if(s==e){
t[o]=a[s];return;//当t的某个节点所代表的s和e重合时
}
int mid=(s+e)>>1;
buildTree(s,mid,ls),buildTree(mid+1,e,rs);
pushup(o);
}
void add(int l, int r, int x, int s=1, int e=n, int o=1){// 给[l,r] 加上x, l,r,x在这个过程中保持不变
if(l<=s && e<=r){
// [s,e]区间是[l,r]的子区间时就可以偷懒了
update(s,e,o,x); //t[o]+=1ll*(e-s+1)*x;lz[o]+=x;
return;
}
pushdown(s,e,o); // [s,e]不是目标区间的子区间,o编号所代表的区间[s,e]的懒标记下放给两个儿子线段
int mid=s+e>>1;
if(mid>=l)add(l,r,x,s,mid,ls);
if(mid+1<=r)add(l,r,x,mid+1,e,rs);
pushup(o);
}
int query(int l, int r, int s=1, int e=n, int o=1){
if(l<=s && e<=r)return t[o];
int res=0;
int mid=s+e>>1;
pushdown(s,e,o);
if(mid>=l) res+=query(l,r,s,mid,ls);
if(mid+1<=r)res+=query(l,r,mid+1,e,rs);
return res;
}
void solve(){
int q;cin>>n>>q;
rep(i,1,n)cin>>a[i];
buildTree();//注意要建树!!!
while(q--){
int op;cin>>op;
if(op==1){
int l,r,x;cin>>l>>r>>x;
add(l,r,x);
}else{
int l,r;cin>>l>>r;
cout<<query(l,r)<<endl;
}
}
}
异或和线段树
void pushup(int o){
t[o]=t[o<<1]^t[o<<1|1];
}
void update(int s, int e, int o, int x){
t[o]^=((e-s+1)&1)?x:0; //区间长度都异或上奇数个x等于t[o]异或一个x,否则异或0等于没有异或
lz[o]^=x;
}
void buildTree(int s=1, int e=n, int o=1){// start, end, node
if(s==e){
t[o]=a[s];return;
}
int mid=(s+e)>>1;
buildTree(s,mid,o<<1),buildTree(mid+1,e,o<<1|1);
pushup(o);
}
void pushdown(int s, int e, int o){
if(lz[o]==0)return;
int mid=(s+e)>>1, ls=o<<1, rs=o<<1|1;
update(s,mid,ls,lz[o]);
update(mid+1,e,rs,lz[o]);
lz[o]=0;
}
void add(int l, int r, int x, int s=1, int e=n, int o=1){
if(l<=s && e<=r){
update(s,e,o,x);
return;
}
int mid=s+e>>1;
pushdown(s,e,o);
if(mid>=l)add(l,r,x,s,mid,o<<1);
if(mid+1<=r)add(l,r,x,mid+1,e,o<<1|1);
pushup(o);
}
int query(int l, int r, int s=1, int e=n, int o=1){
if(l<=s && e<=r){
return t[o];
}
int res=0;
int mid=s+e>>1;
pushdown(s,e,o);
// 上面是一样的
if(mid>=l)res^=query(l,r,s,mid,o<<1);
if(mid+1<=r)res^=query(l,r,mid+1,o<<1|1);
return res;
}
最值线段树
const int inf=4e18;
int n;
int a[N],lz[N<<2];
int tmax[N<<2],tmin[N<<2];
#define ls o<<1
#define rs o<<1|1
void pushup(int o){
tmax[o]=max(tmax[ls],tmax[rs]);
tmin[o]=min(tmin[ls],tmin[rs]);
}
void update(int s, int e, int o, int x){
tmax[o]+=x;tmin[o]+=x;
lz[o]+=x;//lz^=x;
}
void buildTree(int s=1, int e=n, int o=1){
if(s==e){
tmax[o]=tmin[o]=a[s];return;
}
//下面是一样的
int mid=(s+e)>>1;
buildTree(s,mid,ls),buildTree(mid+1,e,rs);
pushup(o);
}
void pushdown(int s, int e, int o){
if(lz[o]==0)return;
int mid=(s+e)>>1;
update(s,mid,ls,lz[o]);
update(mid+1,e,rs,lz[o]);
lz[o]=0;
}
void add(int l, int r, int x, int s=1, int e=n, int o=1){
if(l<=s && e<=r){
update(s,e,o,x);
return;
}
int mid=s+e>>1;
pushdown(s,e,o);
if(mid>=l)add(l,r,x,s,mid,ls);
if(mid+1<=r)add(l,r,x,mid+1,e,rs);
pushup(o);
}
int queryMax(int l, int r, int s=1, int e=n, int o=1){
if(l<=s && e<=r){
return tmax[o];
}
int res=-inf;
int mid=s+e>>1;
pushdown(s,e,o);
if(mid>=l)res=max(res,queryMax(l,r,s,mid,ls));
if(mid+1<=r)res=max(res,queryMax(l,r,mid+1,e,rs));
return res;
}
int queryMin(int l, int r, int s=1, int e=n, int o=1){
if(l<=s && e<=r){
return tmin[o];
}
int res=inf;
int mid=s+e>>1;
pushdown(s,e,o);
if(mid>=l)res=min(res,queryMin(l,r,s,mid,ls));
if(mid+1<=r)res=min(res,queryMin(l,r,mid+1,e,rs));
return res;
}
//solve()中,注意 buildtree();
线段树进阶(加法, 乘法, 赋值)
给定一个长度为n的数组,你可以执行以下操作共q次:
1 l r x: 将区间[l,r][l,r]的数字都加上x。
2 l r x: 将区间[l,r][l,r]的数字都乘上x。
3 l r x: 将区间[l,r][l,r]的数字都赋值为x。
4 l r: 求区间[l,r][l,r]的数字之和。
对于每次操作4,输出结果,结果对998244353取模。
#define ls o<<1
#define rs o<<1|1
int n,q;
int a[N],mul[N << 2],add[N << 2],t[N << 2];// mul 和 add 应该是懒标记
int mo(int x){return (x % p + p) % p;}
void update(int s,int e,int o,int k,int x){
t[o] = mo(mo(t[o] * k) + mo((e - s + 1) * x));
mul[o] = mo(mul[o] * k % p);
add[o] = mo(mo(add[o] * k) + x);
}
void pushdown(int s,int e,int o){
int mid = (s + e) >> 1;
update(s,mid,ls,mul[o],add[o]);
update(mid+1,e,rs,mul[o],add[o]);
mul[o] = 1;add[o] = 0;
}
void pushup(int o){
t[o] = mo(t[ls] + t[rs]);
}
void buildTree(int s = 1,int e = n,int o = 1){
mul[o] = 1;
if(s == e){
t[o]=a[s];
return;
}
int mid = (s + e) >> 1;
buildTree(s,mid,ls);
buildTree(mid+1,e,rs);
pushup(o);
}
// [l,r]内的所有数*k+x
void modify(int l,int r,int k,int x,int s = 1,int e = n,int o = 1){
if(s >= l && e <= r){
update(s, e, o, k, x);
return;
}
pushdown(s,e,o);
int mid = (s + e) >> 1;
if(mid >= l)modify(l,r,k,x,s,mid,ls);
if(mid+1<= r)modify(l,r,k,x,mid+1,e,rs);
pushup(o);
}
int query(int l,int r,int s = 1,int e = n,int o = 1){
if(s >= l && e <= r)return t[o];
pushdown(s,e,o);
int mid = (s + e) >> 1;
int ans = 0;
if(mid >= l)ans = mo(ans + query(l,r,s,mid,ls));
if(mid+1 <= r)ans = mo(ans + query(l,r,mid + 1,e,rs));
return ans;
}
void solve(){
cin>>n>>q;
rep(i,1,n)cin>>a[i];
buildTree();
while(q--){
int op;cin>>op;
if(op==1){
int l,r,x;cin >> l >> r >> x;
modify(l,r,1,x);
}
else if(op == 2){
int l,r,k;cin >> l >> r >> k;
modify(l,r,k,0);
}
else if(op == 3){
int l,r,x;cin >> l >> r >> x;
modify(l,r,0,x);
}
else if(op == 4){
int l,r;cin >> l >> r;
cout << query(l,r) << endl;
}
}
}
静态权值线段树(没有实现可持久化)
接下来的线段树都没有 lazy 和 buildTree, 而且都是手动插入建树
之前的线段树是直接维护一个数组, 现在是维护一个桶(权值).
- 插入一个元素
- 查询整个数组第k小(大)的元素值
- 查询元素大小在[l,r]之间的元素个数
#define ls o<<1
#define rs o<<1|1
int n=2e5,t[N<<2];//t是桶
void pushup(int o){
t[o]=t[ls]+t[rs];
}
void insert(int val,int s=1,int e=n,int o=1){
if(s==e)return t[o]++,void();
int mid =s+e>>1;
if(val<=mid)insert(val,s,mid,ls);
else insert(val,mid+1,e,rs);
pushup(o);
}
int queryCnt(int l,int r,int s=1,int e=n,int o=1){
if(l<=s&&e<=r)return t[o];
int mid=s+e>>1;
int res=0;
if(max(s,l)<=min(mid,r))res+=queryCnt(l,r,s,mid,ls);
if(max(mid+1,l)<=min(e,r))res+=queryCnt(l,r,mid+1,e,rs);
return res;
}
int queryVal(int k,int s=1,int e=n,int o=1){
if(s==e)return s;
int left_sum=t[ls];
int mid=s+e>>1;
if(k<=left_sum)return queryVal(k,s,mid,ls);
return queryVal(k-left_sum,mid+1,e,rs);
}
可持久化权值线段树(主席树)
静态的权值线段树只能维护整个区间的第k小(大)的数,和[l,r]上的值的个数, 由于桶很大, 插入的数范围大, 范围不是给定的
主席树问题一般给定一个长度为n的数组a,有q次询问,每次询问区间[l,r][l,r]中排名为k的元素值(即第k小的元素)。桶的目标都已知, 所以可以离散化桶
主席树类似前缀和思想,把几棵线段树相减, 从而得到符合这个区间的 一颗(计算)虚构出来的树, 这个树的性质符合 所询问区间的 静态权值线段树
因此,主席树可以求 区间 第k小(大)
- 离散化处理:将原始数组映射到紧凑的整数范围,便于主席树处理
- 建树过程:每个元素依次插入,构建版本1~n的持久化记录
- 查询操作:通过传入左右版本根节点,查询区间第k小值
- 结果转换:将查询结果的离散值映射回原始数值
const int N=2e5+5;
int n,q;
int a[N];
int rt[N],idx;
vector<int> v;
int bin(int x){
return lower_bound(all(v),x)-v.begin()+1;
}
struct node{
int ls,rs,val;
}t[N<<5];
void insert(int &o, int pre, int val, int s=1,int e=n){ //传入当前节点,且需要进行操作, pre节点只需要复制不需要操作, val是新插入的值
o=++idx;//分配节点,o是编号, 树的点编号在主席树中没有意义,线段树只由e,s控制
t[o]=t[pre];//复制上一个版本
t[o].val++;//修改自身权值
if(s==e)return ;//表明到了叶子节点s,e==要插入的值
int mid = s+e>>1;
if(val<=mid)insert(t[o].ls, t[pre].ls, val, s, mid);
else insert(t[o].rs, t[pre].rs, val, mid+1, e);
}
int queryVal(int lo,int ro, int k, int s=1,int e=n){
if(s==e)return s;// 返回的是v数组下标的索引
int left_sum=t[t[ro].ls].val-t[t[lo].ls].val;
int mid=s+e>>1;
if(k<=left_sum)return queryVal(t[lo].ls, t[ro].ls, k, s, mid);
return queryVal(t[lo].rs, t[ro].rs, k-left_sum, mid+1, e);
}
void solve(){
cin>>n>>q;
rep(i,1,n)cin>>a[i];
rep(i,1,n)v.push_back(a[i]);
sort(all(v));
v.erase(unique(all(v)),v.end());
rep(i,1,n)insert(rt[i],rt[i-1],bin(a[i]));// rt[i],里面的值是树的点的编号,但是用i来控制方便是第几棵树,
// bin(a[i])返回离散后数组v下标, 表示的是第几个数
while(q--){
int l,r,k;cin>>l>>r>>k;
cout<< v[queryVal(rt[l-1],rt[r],k)-1] <<endl;
}
}
a= 100,1,6,10, 2, 7,2,6
v= 1,2,6,7,10,100
bin: 6, 2, 3, 5, 2, 4, 2, 3 在第rt[i]棵树加入的时候(第i时刻),第k个位置的数的桶+1
主席数的变换运用
给定一个长度为n的数组a,有q次询问,每次询问区间[l,r][l,r]中不同的数字的个数。: 开一个last数组存上次出现的位置, [l,r]区间的不同数的个数f(l,r)=∑(l,r) ( last[ i ] ]<l )
a: 1 2 1 2 2
lst: 0 0 1 2 4
int a[N], rt[N], lst[N], idx, n ;
struct Node{
int ls, rs, val;
}t[N << 5];
vector<int> v;
int bin(int x){return lower_bound(all(v), x) - v.begin() + 1;}
void insert(int &o, int pre, int val, int s = 0, int e = n){
o = ++idx;
t[o]= t[pre];
t[o].val++;
if(s == e)return;
int mid = (s + e) >> 1;
if(val <= mid)insert(t[o].ls, t[pre].ls, val, s, mid);
else insert(t[o].rs, t[pre].rs, val, mid + 1, e);
}
int query(int lo, int ro, int l, int r, int s = 0, int e = n){
if(l <= s && e <= r)return t[ro].val - t[lo].val;
int mid = (s + e) >> 1, res = 0;//设置返回加和的个数
if(mid >= l)res += query(t[lo].ls, t[ro].ls, l, r, s, mid);
if(mid + 1 <= r)res += query(t[lo].rs, t[ro].rs, 1, r, mid + 1, e);
return res;
}
void solve(){
int q;cin >> n >> q;
rep(i,1,n)cin >> a[i];
rep(i,1,n)v.push_back(a[i]);
sort(all(v));
v.erase(unique(all(v)), v.end());//因为是描述的这第某个数上次出现的位置,所以去重
rep(i,1,n){
insert(rt[i], rt[i - 1], lst[bin(a[i])]);//只是多了一层映射,就把对所有桶的维护转变为lst数组的维护
lst[bin(a[i])] = i;//a[i]这个数,第几个桶,上次出现的位置(也就是现在),记录在lst[bin(a[i])]
}
while(q --){
int l, r;cin >> l >> r;
cout << query(rt[l - 1], rt[r], 0, l - 1) << '\n';
// 0 ~ l-1 是控制线段范围,相当于两个东西描述一个要找的性质,之前的性质是k罢了
}
}
自己定义结构体线段树解决 矩形面积并问题 (结合扫描线)
const int N=1e5+5;
struct event{
int l,r,y,type;
bool operator<(const event& tem)const{
return y<tem.y;
}
};
//以下是线段树部分,因为要动态维护整个区间所覆盖的长度(区间之和),所以想到线段树
struct node{
int len,cnt;
}t[N<<2+4];//之前这里没有+4就RE了,以后开树都加上一点
vector<event> ev;
vector<int> v;
void update(int s,int e,int p,int l,int r,int typ){
if(r<=s||e<=l)return ;
if(l<=s&&e<=r){
t[p].cnt+=typ;
}else{
int mid=e+s>>1;//注意不是l+r>>1
update(s,mid,p<<1,l,r,typ);
update(mid,e,p<<1|1,l,r,typ);//注意这里不能mid+1
//如果+1,[mid,mid+1]这一段长度就直接被忽略了,现在维护的是相邻的线段而不是离散的点
}
if(t[p].cnt>0){
t[p].len=v[e-1]-v[s-1];
}
// }else if(s==e){t[p].len=0;} //可以不写,反正递归到最后也是0
else{
t[p].len=t[p<<1].len+t[p<<1|1].len;
}
}
void solve(){
int n;cin>>n;
rep(i,1,n){
int x1,x2,y1,y2;cin>>x1>>y1>>x2>>y2;
ev.push_back({x1,x2,y1,1});
ev.push_back({x1,x2,y2,-1});
v.push_back(x1);
v.push_back(x2);
}
sort(all(v));
v.erase(unique(all(v)),v.end());
for(auto &e:ev){
e.l=lower_bound(all(v),e.l)-v.begin()+1;//因为树维护的本来就是第几个x之间的关系,至于具体的值再在v映射一步即可
e.r=lower_bound(all(v),e.r)-v.begin()+1;
}
sort(all(ev));
int ans=0;
int pre_y=ev[0].y;
for(auto &e:ev){
int dy=e.y-pre_y;
ans+=dy*t[1].len;
update(1,v.size(),1,e.l,e.r,e.type);//注意这里v.size()即可,不需要再+1
pre_y=e.y;
}
cout<<ans<<endl;
}
错误总结: ①线段树结点数目开小了 ② int mid=e+s>>1 ③ v.size()不用+1否则访问越界
④ update(mid,e,...),不用mid+1,这题比较特殊, 因为维护的每个点, 代表的是 连续的线段

浙公网安备 33010602011771号