先来简单回顾一下线段树基础:
线段树以二进制为载体(我个人觉得这算是位运算衍生出来的一种数据结构),主要思想还是二分(仔细想想到底是不是),
二叉树或者堆都手写过吧,好吧没写过也没关系。简单说,就是根节点为在数组中的下标为N,左孩子是2N,右孩子是2N+1,
这样说来每个二叉树上地节点都能获得一个唯一的下标。
再说到线段树,其实也是通过二叉树实现,举一个简单的栗子,假设有15个数
由于线段树以二进制为基础,没有数值大小为15的位,但是我们有数值大小为16的位
所以我们可以通过简单的扩展将所需维护的区间扩大到16方便维护区间。我们有15个数,第16个
数我们可以设置为不影响维护目的区间的任意数。
while(nn<n)
nn*=2;
从代码看就很简单是吧哈哈哈
很容易得出一共有2N-1个节点
我们可以通过简单的递归实现线段树的初始化
时间复杂度为O(2N-1)
void build(int l,int r,int k)
{
if(l==r)
{
shu[k]=a[r];
return;
}
int mid=(l+r)>>1;
build(l,mid,k*2);build(mid+1,r,k*2+1);
shu[k]=shu[k*2]+shu[k*2+1];
}
首先,线段树能实现以下两个基本操作
1、查询某区间的最小值
2、在修改某些值后依然可以维护区间的最小值
当然不仅于此,线段树对于区间的操作(例如对某区间中的数值同时进行或增或减,同时还要求我们维护区间和或者是其他区间上的要求)
如果我们对于一些区间操作采取遍历的方式维护整个线段树,那就得不偿失体现不出线段树的真正优势了,for循环太out了
下面我们介绍一种简单的技巧:懒惰标记!!!
网传这是线段树的精华所在,我。。。保留意见。。。总感觉线段树应该还用更加巧妙地运用只是我没想到或者 没有学到罢了
继续说,
懒惰标记的出现的一点小问题为:为什么要在更新和sum里都要写标记的下推?刚开始写,只在sum里写了下推,理由理所当然,查询到的区间下推就行了,可是他就是错的的啊?
可是懒惰标记的操作不仅要考虑表层,还要考虑上层根据shu[k]=shu[k*2]+shu[k*2+1],由于回溯时会更新上层,所以要保证当前区间的值已经更新完毕
当我们需要对一个区间操作时,我们不需要立刻去做这个操作,我们可以把这个操作标记,等这个区间需要被查询的时候再去做这个操作,
不需要用到这个区间是就不需要操作也没有操作的意义。
开动脑筋如何标记区间,其实不难的。
懒惰标记该如何写:首先更新的时候
思路转换为具体操作还是需要注意一下细节的
下面贴一些代码简单展示一下这些细节(好吧其实是懒得说)
long long up(int x,int y,int k,int l,int r,int kk)
{
if(l>=x && r<=y)
{
lang[kk].l=l;lang[kk].r=r;lang[kk].lazy+=k;//其实没必要开结构体的,开个lazy[]就好
a[kk]+=((r-l+1)*k);//皮一下懒得改哈哈哈
return ((r-l+1)*k);
}
else if(l>y || r<x) return 0;
else
{
long long k1=up(x,y,k,l,(l+r)/2,kk*2);
long long k2=up(x,y,k,(l+r)/2+1,r,kk*2+1);
a[kk]+=k1+k2;
return (k1+k2);
}
}
//还是那句话我的代码可能也有也有不足之处,看看思路就好
void down(int k,int l,int r,int kk)
{
a[k]+=(r-l+1)*kk;
lang[k].lazy+=kk;
return;
}
long long quy(long long x,long long y,long long k,long long l,long long r)
{
if(l>r) return 0;
long long sum=0;
if(y<l || r<x) return 0;
else if(x<=l && r<=y)
{
return a[k];
}
else
{
if(lang[k].lazy)
{
down(k*2,l,(l+r)/2,lang[k].lazy);
down(k*2+1,(l+r)/2+1,r,lang[k].lazy);
lang[k].lazy=0;
}
sum+=quy(x,y,k*2,l,(l+r)/2);
sum+=quy(x,y,k*2+1,(l+r)/2+1,r);
}
return sum;
}
还是提一下吧,懒惰标记的区间上方还有需要操作一番的,例如我们需要标记【2,5】,我们也可以吧标记往下推,但是它的上面(例如【1,8】)不能不管吧,想想是吧???(查询往下推的时候并没有考虑顶层区间的更新,纯粹只是把标记往下推了,而已。
权值线段树
基于权值划分,记录每个值域的数的个数。
简单应用:用来查询全局第k大,前后驱等。权值线段树单纯的应用不错,其本身只是到权值的一种思维转变。(注意离散化,可以作为主席树的前置学习点)
主席树(可持久化线段树)
引入:静态查询区间第k大
普通线段树无法解决这样的问题,但是权值线段树可以。然而,考虑到区间问题,我们需要建立m颗权值线段树(查询的命令有m条),这过不了。
然而我们可以发现当我们每次对树进行单点修改时,最多只会修改到一条链而已,利用动态开点建树,这样在空间和时间上都能节省。
还有就是前缀和思想,我们需要调用某个区间时,我们可以调用这个区间的“区间树”,其实也不用得出整棵树,按照所需目的利用第i和第j棵树就好。
那么如果是在线的呢?
如果是单点修改,删一个减一个即可。
如果是区间修改。。。
树状数组
应用:在线查询前缀和。
如果我们用线段树做这个的话,也可以但是从状态数来看好像更少一点,树状数组好像更快一点,实现起来也很简单。
总结一点就是俗称的跳lowbit。。。
由于是维护前缀和,我们有时候还可以用差分思想来考虑区间问题
例如对某区间修改,然后单点查询,使用差分思想实现上就比单纯的线段树标记加查询简单些
然后树状数组可以实现查询全局的第k小,其实就是使用st表,但是这样的”拓展“的思想很多题目都能用。
// C++ Version
// 权值树状数组查询第k小
int kth(int k) {
int cnt = 0, ret = 0;
for (int i = log2(n); ~i; --i) { // i 与上文 depth 含义相同
ret += 1 << i; // 尝试扩展
if (ret >= n || cnt + t[ret] >= k) // 如果扩展失败
ret -= 1 << i;
else
cnt += t[ret]; // 扩展成功后 要更新之前求和的值
}
return ret + 1;
}
还能普及一个时间戳优化空间的问题,每次操作前检查时间戳对不对,不对就清零,能有效解决多组数据每次清空数组的消耗。
线段树合并
主要针对权值线段树而言(我只见过这个)
一般通过并查集来维护集合各种关系
标记永久化的注意事项
标记永久化:如果能保证标记不溢出,可以省略标记下传这一步,直接在查询的时候添加沿途的影响即可。
对于一个父区间加上了永久化标记,假若给子区间上了两个与父区间不同的标记,这该怎么办?只能因题制宜了