线段树
这篇文章就是线段树模板和各类用法(懒标记,扫描线,可持久化,树套树)的大杂烩。个人不用树状数组所以不谈。
朴素的线段树模板
宏定义:
#define mid ((l+r)>>1)
#define Lson root<<1,l,mid
#define Rson root<<1|1,mid+1,r
建立线段树
void build(int root,int l,int r)//当前节点编号,左区间,右区间
{
if (l==r)//递归到了叶子节点
{
sum[root]=a[i];//叶子节点的内容因我们
//维护的内容而异
return ;
}
build(Lson);
build(Rson);
pushup(root);
}
pushup操作将我们计算出来的左子树和右子树的结论更新到根节点中。更新操作也是因我们要维护的区间内容而异。
这里的代码维护了一个区间和。
void pushup(int root)
{
sum[root]=sum[root<<1]+sum[root<<1 | 1];//左右儿子节点和
}
单点修改操作:
void change(int root,int l,int r,int x,int v)//x点权值修改成v
{
if (l==r)//目标位置
{
sum[root]=v;//修改操作
return ;
}
if (x<=mid)//左儿子上
change(Lson,x,v);
else//右儿子身上
change(Rson,x,v);
pushup(root);
}
区间查询操作:
int Query_sum(int root,int l,int r,int ql,int qr)//当前节点是rt,当前区间[l,r],目标区间[ql,qr]
{
int ans=0;
if (ql<=l && r<=qr) //目标区间包括了当前区间[l,r]
return sum[root];//直接返回当前区间
if (ql<=mid) //目标区间有部分在当前区间的左边
ans+=Query_sum(Lson,ql,qr);
if (qr>mid)//目标区间有部分在当前区间的右边
ans+=Query_sum(Rson,ql,qr);
Push_up(root);
return ans;
}
区间修改与线段树懒标记
懒标记的实质:推迟信息更新
这边以最小值的区间修改为例
只要是update和query操作,都需要加入pushdown
在update中的pushdown并不能做到底,找到update的所有区间就停止了,因此query中还需要pushdown一次保证每个查询区间覆盖的值都加上了懒标记的效果。
如果一道题目包含加减乘除等各种操作,pushdown的时候还要记录是哪种操作延迟进行。
struct TreeNode
{
int val;
int mark;//延迟标记
}Tree[1e5+10];//定义线段树
void build(int root,int l, int r)
{
Tree[root].mark = 0;//----设置标延迟记域
if(l == r)//叶子节点
segTree[root].val = arr[istart];
else
{
build(LSon);//递归构造左子树
build(RSon);//递归构造右子树
//根据左右子树根节点的值,更新当前根节点的值
Tree[root].val = min(Tree[root<<1|1].val, segTree[root<<1].val);
}
}
void pushDown(int root)
{
if(Tree[root].mark != 0)
{
//设置左右孩子节点的标志域,因为孩子节点可能被多次延迟标记又没有向下传递
//所以是 “+=”
Tree[root<<1|1].mark += Tree[root].mark;
Tree[root<<1].mark += Tree[root].mark;
//根据标志域设置孩子节点的值。因为我们是求区间最小值,因此当区间内每个元
//素加上一个值时,区间的最小值也加上这个值
Tree[root<<1|1].val += Tree[root].mark;
Tree[root<<1].val += Tree[root].mark;
//传递后,当前节点标记域清空
Tree[root].mark = 0;
}
}
int query(int root, int l, int r, int ql, int qr)
{
//查询区间和当前节点区间没有交集
if(ql > l || qr > r)
return 0x3f3f3f3f3f3f3f;//因为求的是最小值,如果没有交集自然不用计入
//最小值,于是用inf代替
//当前节点区间包含在查询区间内
if(ql <= l && qr >= r)
return Tree[root].val;
//分别从左右子树查询,返回两者查询结果的较小值
pushDown(root); //延迟标志域向下传递
//这个时候就是我们要查询返回值的时候了,必须pushdown更新
return min(query(LSon, ql, qr),
query(RSon, ql, qr));
}
void update(int root, int l, int r, int ul, int ur, int addVal)
{
//分多少类和query是一样的
//更新区间和当前节点区间没有交集
if(ul > l || ur > r)
return ;
//当前节点区间包含在更新区间内
if(ul <= l && ur >= r)
{
Tree[root].mark += addVal;
Tree[root].val += addVal;
return ;
}
pushDown(root); //延迟标记向下传递
//更新左右孩子节点
update(LSon, ul, ur, addVal);
update(RSon, ul, ur, addVal);
//根据左右子树的值回溯更新当前节点的值
Tree[root].val = min(Tree[root<<1|1].val, Tree[root<<1].val);
}
解题中重要的问题
- 怎么判断能否用线段树?只要通过左右区间的属性能得到整个区间的属性就行。(操作为:修改+(不一定是区间,整体1到n也可以的)查询)
- 线段树应该存储哪些属性?结果不可直接求出的话,求要求的结果需要用到什么属性就存什么。
主席树
补充习题部分
Crane
- 向量是可以叠加的
- 维护当前区间从头指向尾的向量v。一开始初始化为每一段的长度即可。
- 还要维护左子树到右子树转了多少角度w
通过v和w就可以用左右子树的向量解出整个区间的向量了,最后查询1到n。
pushup的时候先把左子树的向量旋转y角再和右子树的向量相加。
向量的旋转公式:
冒泡排序的最大次数
这是个求逆序数的问题。逆序数还可以用归并排序计算。
对于每个j,要求求出i<j且a[j]>a[i]的i的个数。
for(int j=0;j<n;j++)
{
ans+=j-query(1,a[j]);//j-a[j]之前的数的个数
add(a[j],1);//j正式变为“之前的数”
}
Acwing1275 最大数
m个操作,说明最多有m个数。建立有m个数的线段树,由于求最大值,没有值的地方初始化为0即可。
如果是A操作,只要把n+1位置改为添加的数。
懒标记+扫描线 Acwing 247 亚特兰蒂斯