线段树入门:
前几天开始接触线段树,其一些基本的操作还是很容易理解的,但是区间更新我着实理解了好一会(因该是本人太菜),今天有时间,所以总结一下。
这篇博客主要是讲一讲线段树的一些基本操作和我的一些理解。
首先我们需要知道线段树它是建立在线段的基础上的,树上的每个节点代表的是一条线段[a,b],并且树上每个点都可以维护该区间的某个性质,因此线段树在处理区间问题上是非常高效的。例如查找某个区间的最大值、最小值、求区间的和等,并且这几天发现线段树的题目都有个特点就是题目都会有多次查询,一般做这类问题我们可能会想到循环去找,一次查询的时间复杂度为O(n),可是当数据范围很大并且查询次数很多的时候循环肯定达不到题目时间要求,但是用线段树可以很好的解决这类问题,一次查询的时间复杂度仅为O(logN)。
先看看线段树长什么样子,本人手画,别嫌弃........
从图中看出,树上每个节点有几个值,改点维护的区间的左端点、右端点、维护的性质、根据题目不同还会添加其他值例如需要给节点标记时要加上标记值等。一个节点有这么多性质因此我们通常用结构体来储存。如下(看个人习惯):
const int Max_n=10000;
struct node
{
int l;//节点的左端点
int r;//节点的右端点
int val;//节点维护的该区间的某个性质
int tag;//给节点标记的变量
}Segtree[Max_n];//定义线段树
下面讲线段树最基本的建树,查询,单点更新,区间更新操作,用到比较多的是递归方法,直接看代码吧应该好懂:
建树操作:
void pushup(int root)
{
segtree[root].val=max(segtree[root<<1].val,segtree[root<<1|1].val);//val维护的为区间最大值
}
//从图中不难看出节点的编号是从上往下从从左往右,因此从最顶层1号节点开始建树
void buildtree(int root,int l,int r)//root为当前结点,l为当前区间的左端点,r为当前区间的右端点
{
segtree[root].l=l;//当前节点维护区间的左端点为l,下同
segtree[root].r=r;
if(l==r)//找到最底层对的节点
{
segtree[root].val=1;//把节点的初始值都置为1
return;//找到底层元素后返回上一层
}
int mid=(segtree[root].l+segtree[root].r)>>1;//mid为当前区间的中点。
//'>>1',该运算表示右移一位,在二进制状态下操作,相当于/2,'<<1',左移一位相当于*2
buildtree(root<<1,l,mid);//左边建树,不难发现当前节点的左儿子编号为父亲切点的两倍
buildtree(root<<1|1,mid+1,r);//右边建树
pushup(root);//更新父亲节点
}
查询操作:
int query(int root,int l,int r)//root为查询节点,[l,r]为查询区间
{
int ll=segtree[root].l;
int rr=segtree[root].r;
if(l<=ll&&rr<=r)//如果该节点的区间刚好在查询区间内,则返回该节点的val
return segtree[root].val;
int mid=(ll+rr)>>1;
int ans=-inf;//ans为查询结果,初始化为一个很大的负值
if(l<=mid)//该节点的左边可查询
ans=max(ans,query(root<<1,l,r));
if(r>mid)//右边可查询
ans=max(ans,query(root<<1|1,l,r));
return ans;
}
单点更新:
void update(int root,int r,int cur)//当前节点,要更新的节点,要更新的的值
{
int ll=segtree[root].l;
int rr=segtree[root].r;
if(ll==rr)//找到最底层的点则更新
{
segtree[root].val=cur;//将该点的值改为cur;
return;
}
int mid=(ll+rr)>>1;
if(r<=mid)//要更新的点在该区间的左边则去左边更新
update(root<<1,r,cur);
else //否则去右边更新
update(root<<1|1,r,cur);
pushup(root);
}
区间更新:
区间更新如过把这个区间的所有的点都单点更新的的话并不能体现线段树优势,所以线段树是直接将整个区间维护的值进行修改,区间更新需要用到延迟更新(应该就这个位置不好理解一点),我个人理解延迟更新就是当你找到要更新的区间后对该区间维护的值进行修改然后给该节点打上标记,然后就 返回,不再继续向下更新。当下一次查询操作如果发现当前节点被标记过则把该节点的左右儿子更新并打上标记,这样原本在上一次操作就因该跟新的点延迟到了需要用到它的时候再更新,巧妙啊!!!
void pushdown(int root)
{
if(segtree[root].tag)//如果这个点被标记过,则把他带到的左右儿子更新并打上标记
{
segtree[root<<1].val=max(segtree[root].tag,segtree[root<<1].val);//更新左儿子
segtree[root<<1|1].val=max(segtree[root].tag,segtree[root<<1|1].val);//更新右儿子
segtree[root<<1].tag=segtree[root<<1|1].tag=segtree[root].tag;//标记
segtree[root].tag=0;//把该节点的标记还原,否则下次遇到还会更新
return ;
}
}
void update(int root,int l,int r,int cur)//当前节点,更新的区间左右端点,更新值
{
int ll=segtree[root].l;
int rr=segtree[root].r;
if(l<=ll&&rr<r)//该节点的区间为要更新的区间,更新该节点并标记
{
segtree[root].val=max(cur,segtree[root].val);
segtree[root].tag=cur;
return;
}
pushdown(root);//如果该节点被标记过则需要先更新他的儿子
int mid=(ll+rr)>>1;
if(l<=mid)//左边可更新
update(root<<1,l,r,cur);
if(l>mid)//右边可更新
update(root<<1|1,l,r,cur);
pushup(root);
}
线段树的基本操作就就讲完了,我觉得只要仔细想想应该是很好懂的。
另外我们对线段树的操作一般都是从最顶端开始。
本人第一次写博客,如果有什么错误请大家及时指出,谢谢~~