原理:
线段树比树状数组要灵活许多,只要能满足“区间加法”的题,都能用线段树来做。但是对于没有修改的情况,区间和可以用树状数组,Max/Min可以用ST表。
线段树的本质是做区间分解,各个子区间的Sum/Max/Min合成大区间的,例如【2,12】=【2】+【3,4】+【5,7】+【8,10】+【11,12】.
可以证明的是,当n>=3时,[1, n]的任意子区间[l, r] 分解的子区间不超过 2log(n-1)
这样的话,不管是查询还是修改,只需要查询/修改 log(len) 次,而不用 len 次(len是区间长度)。
实现:
建树:可以不用单点修改n次,可以递归到叶子节点,再赋值
单点修改:找到那个点,改掉就可以了
区间修改:不能递归到叶子节点,而是遇到包含在修改范围内的子区间就打上Lazy标记。Update有两种,一种是Add,是个改变量,另一种是Set,是设置为,两者在PushDownh和区间赋值时略有不同
应用:
1. leetcode1526. 形成目标数组的子数组最少增加次数:
dfs(i, j) ,再在线段树上查询[i, j]的最小值
2. leetcode1157. 子数组中占绝大多数的元素:
线段树维护绝对众数,只是区间合并时与普通的不同,采用抵消法
3. leetcode699. 掉落的方块:
维护区间的最大值,由于数值范围过大,需要将所有出现的点进行离散化
4. leetcode1521. 找到最接近目标值的函数值:
二分时,查询[right, mid]的区间与
5. leetcdeo715. Range 模块
区间修改+动态开点,由于数值范围很大,但是又不能离散化(离散化只保留相对顺序,而这里必须修改那么长的区间)
总的来说,就是,要么直接给出左右端点,要么就是dfs/二分/滑动窗口等得到的左右端点,要么范围太大进行离散化/动态开点。
模板:
1. 区间修改(Add)+最小值
struct SegTree { #define maxn 100010 //元素总个数 #define ls l,m,rt<<1 #define rs m+1,r,rt<<1|1 #define INF 0x3f3f3f3f long long Sum[maxn<<2],Add[maxn<<2], Max[maxn<<2], Min[maxn<<2];//Sum求和,Add为懒惰标记, Max区间最大值 // int A[maxn],n;//存原数组数据下标[1,n] vector<int>A; void init(vector<int>& _A){ A = _A; for(int i = 1;i < maxn;i++) Min[i] = INF; } //PushUp函数更新节点信息 ,这里是求和 void PushUp(int rt){ Sum[rt]=Sum[rt<<1]+Sum[rt<<1|1]; Max[rt] = max(Max[rt<<1], Max[rt<<1|1]); // 标记不需要向上维护x Min[rt] = min(Min[rt<<1], Min[rt<<1|1]); // } //Build函数建树 void Build(int l,int r,int rt){ //l,r表示当前节点区间,rt表示当前节点编号 // cout << "build: " << l << " " << r << endl; if(l==r) {//若到达叶节点 Sum[rt]=A[l-1];//储存数组值 Max[rt]=Min[rt]=A[l-1]; return; } int m=(l+r)>>1; //左右递归 Build(l,m,rt<<1); Build(m+1,r,rt<<1|1); //更新信息 PushUp(rt); } void Update(int L,int R,int C,int l,int r,int rt){//L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号 // cout << "Update: " << l << " " << r << endl; if(L <= l && r <= R){//如果本区间完全在操作区间[L,R]以内 Sum[rt] +=C*(r-l+1);//更新数字和,向上保持正确 Add[rt] +=C;//增加Add标记,表示本区间的Sum正确,子区间的Sum仍需要根据Add的值来调整 Max[rt] = Max[rt]+C; Min[rt] = Min[rt]+C; return ; } int m=(l+r)>>1; PushDown(rt,m-l+1,r-m);//下推标记 //这里判断左右子树跟[L,R]有无交集,有交集才递归 if(L <= m) Update(L,R,C,l,m,rt<<1); if(R > m) Update(L,R,C,m+1,r,rt<<1|1); PushUp(rt);//更新本节点信息 } void PushDown(int rt,int ln,int rn){ //ln,rn为左子树,右子树的数字数量。 // cout << "rt: " << rt << endl; if(Add[rt]){ //下推标记 Add[rt<<1]+=Add[rt]; Add[rt<<1|1]+=Add[rt]; //修改子节点的Sum使之与对应的Add相对应 Sum[rt<<1]+=Add[rt]*ln; Sum[rt<<1|1]+=Add[rt]*rn; Max[rt<<1] = Max[rt<<1]+Add[rt]; Max[rt<<1|1] = Max[rt<<1|1]+Add[rt]; Min[rt<<1] = Min[rt<<1]+Add[rt]; Min[rt<<1|1] = Min[rt<<1|1]+Add[rt]; //清除本节点标记 Add[rt]=0; } } int Query(int L,int R,int l,int r,int rt){//L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号 if(L <= l && r <= R){ //在区间内,直接返回 return Min[rt]; } int m=(l+r)>>1; //下推标记,否则Sum可能不正确 PushDown(rt,m-l+1,r-m); //累计答案 int ANS=INF; if(L <= m) ANS=min(ANS, Query(L,R,l,m,rt<<1)); if(R > m) ANS=min(ANS, Query(L,R,m+1,r,rt<<1|1)); return ANS; } }segTree;