线段树及树状数组 学习笔记 持续学习更新中

先来简单回顾一下线段树基础:

线段树以二进制为载体(我个人觉得这算是位运算衍生出来的一种数据结构),主要思想还是二分(仔细想想到底是不是),

二叉树或者堆都手写过吧,好吧没写过也没关系。简单说,就是根节点为在数组中的下标为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;
}

还能普及一个时间戳优化空间的问题,每次操作前检查时间戳对不对,不对就清零,能有效解决多组数据每次清空数组的消耗。

线段树合并

主要针对权值线段树而言(我只见过这个)

[HNOI2012]永无乡 - 洛谷

一般通过并查集来维护集合各种关系

标记永久化的注意事项

标记永久化:如果能保证标记不溢出,可以省略标记下传这一步,直接在查询的时候添加沿途的影响即可。

对于一个父区间加上了永久化标记,假若给子区间上了两个与父区间不同的标记,这该怎么办?只能因题制宜了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值