zoukankan      html  css  js  c++  java
  • 线段树

    众所周知,线段树这玩意又臭又长,但很容易理解。
    线段树,有许多构造方法,我写的其实就是一棵完全二叉树,用堆式存储(也就是根节点的下标是1,每个下标(=k)父节点的左儿子就是(k * 2),右儿子就是(k * 2 + 1))。每个父节点的值就是她的两个子结点的值的和,叶子节点就是输入的序列,也就是我们写暴力用的那个序列。如下图:

    P.S.:紫色是叶子节点,也就是初始序列。
    线段树一般有5个操作:建树、单点加、单点查询、区间加、区间查询(别问我区间乘怎么搞,我还在研究QwQ)。

    建树:

    我们要怎么把这个线段树建立起来呢?很简单,通过上面说的,只要从结点1开始递归,递归参数包括下标、控制的区间。其中控制的区间就是你这个点她所包括的范围。那么如果这个范围是1,就表示你已经到叶子结点了,就可以输入她的值了。而父节点则只要在递归完两个子树后将两个子节点的值相加即可。为了后面处理方便,我们定义数组(l[])(r[]),表示结点k所控制的范围。具体实现如下:

    inline void build(int ll,int rr,int k)//参数是:当前区间左边界、右边界,当前结点编号
    {
    	l[k]=ll,r[k]=rr;
    	if(ll==rr)
    	{
    		scanf("%lld",&w[k]);
    		return ;
    	}
    	int mid=(ll+rr)/2;
    	build(ll,mid,k*2);
    	build(mid+1,rr,k*2+1);
    	w[k]=w[k*2]+w[k*2+1];
    	return ;
    }
    

    单点加:

    单点加也很简单,看懂了建树,就会发现单点加其实也差不多,就是从根节点开始,一直递归到相应的叶子结点,然后更改值,最后一路返回上来更改父节点的值。这个思路和二分查找十分向像。

    inline void dadd(int k,int x,int y)//参数是:当前结点编号,要加的数的位置,要增加的值
    {
    	if(l[k]==r[k])
    	{
    		w[k]+=y;
    		return ;
    	}
        if(f[k]) down(k);
    	int mid=(l[k]+r[k])/2;
    	if(x<=mid) dadd(k*2,x,y);
    	else dadd(k*2+1,x,y);
    	w[k]=w[k*2]+w[k*2+1];
    	return ;
    }
    

    单点查询:

    在懂了单点加后,单点查询就更简单了,也是递归,也是二分的思路,而且还不用修改值。

    inline int dsum(int k,int x)//参数是:当前结点编号,要查询的位置
    {
    	if(l[k]==r[k]) return w[k];
    	if(f[k]) down(k);
    	int mid=(l[k]+r[k])/2;
    	if(x<=mid) return dsum(k*2,x);
    	else return dsum(k*2+1,x);
    }
    

    区间查询:

    诶,为什么不是区间加?因为区间加是重头戏,要放最后QwQ。区间查询和前面三个就大不相同了,但思路还是很好想的。想一下分块,处理一段区间,就是先把整的区间处理好,然后处理零碎的。线段树也一样,如果当前结点包含的范围都在查询区域内,那么就直接返回,否则,再考虑左右子节点,直到变成第一种情况。

    inline int qsum(int ll,int rr,int k)//参数是:要查询的左边界、右边界,当前结点的编号
    {
    	if(l[k]>=ll&&r[k]<=rr) return w[k];
    	if(f[k]) down(k);
    	int mid=(l[k]+r[k])/2;
    	int res=0;
    	if(ll<=mid) res+=qsum(ll,rr,k*2);
    	if(rr>mid) res+=qsum(ll,rr,k*2+1);
    	return res;
    }
    

    区间加:

    相信认证观察的同学已经发现了,前面除了建树,每个函数中都会出现if(f[k]) down(k);。而这个玩意的用处,就要在区间加中体现出来。区间加,思路和区间查询还是差不多的,不过这里我们要引入一个重点:懒惰标记。懒惰标记,顾名思义,要用的时候才动她,非常懒惰。懒惰标记分两种:下传型标记永久性标记。这里只讲下传型标记(因为永久性我不会)。当你区间加递归到一个结点k,满足k所包含的范围都在区间加的范围内,那么我们就不用再递归下去了。为什么?观察上面的代码,发现每次要访问下面的结点都要先经过上面的,那么我们在走到上面的结点时再把这些数传到下面即可,非常方便。于是,我们又要开一个数组(f[]),存储懒惰标记。而下传操作的具体实现如下,具体看注释:

    inline void down(int k)
    {
    	f[k*2]+=f[k];//把懒惰标记传下去,留给子子孙孙(划掉)下面的结点。
    	f[k*2+1]+=f[k];//同上
    	w[k*2]+=f[k]*(r[k*2]-l[k*2]+1);//根据结合律,就是这个玩意把数加上了QwQ。至于为什么要*f[k]……因为子节点本身也有可能之前就有懒标记啊。
    	w[k*2+1]+=f[k]*(r[k*2+1]-l[k*2+1]+1);//同上
    	f[k]=0;//清楚懒标记。
    	return ;
    }
    

    那么,搞定了这个,区间加也不是什么难事了。

    inline void qadd(int ll,int rr,int k,int x)//参数是:要加的区间的左边界、右边界,当前结点编号,要加的数。
    {
    	if(l[k]>=ll&&r[k]<=rr)
    	{
    		w[k]+=(r[k]-l[k]+1)*x;
    		f[k]+=x;
    		return ;
    	}
    	if(f[k]) down(k);
    	int mid=(l[k]+r[k])/2;
    	if(ll<=mid) qadd(ll,rr,k*2,x);
    	if(rr>mid) qadd(ll,rr,k*2+1,x);
    	w[k]=w[k*2]+w[k*2+1];//别忘了更新当前结点哦!
    	return ;
    }
    

    总结:

    如果有什么不懂的,可以尝试自己画个图模拟一下,很有用的。还有,数组要开四倍空间,至于为什么……自己画一个就知道了。
    下面是总的连起来的代码。

    struct tree
    {
    	int l[400005],r[400005];
    	int w[400005],f[400005];
    	inline void build(int ll,int rr,int k)
    	{
    		l[k]=ll,r[k]=rr;
    		if(ll==rr)
    		{
    			scanf("%lld",&w[k]);
    			return ;
    		}
    		int mid=(ll+rr)/2;
    		build(ll,mid,k*2);
    		build(mid+1,rr,k*2+1);
    		w[k]=w[k*2]+w[k*2+1];
    		return ;
    	}
    	inline void down(int k)
    	{
    		f[k*2]+=f[k];
    		f[k*2+1]+=f[k];
    		w[k*2]+=f[k]*(r[k*2]-l[k*2]+1);
    		w[k*2+1]+=f[k]*(r[k*2+1]-l[k*2+1]+1);
    		f[k]=0;
    		return ;
    	}
    	inline void dadd(int k,int x,int y)
    	{
    		if(l[k]==r[k])
    		{
    			w[k]+=y;
    			return ;
    		}
    		if(f[k]) down(k);
    		int mid=(l[k]+r[k])/2;
    		if(x<=mid) dadd(k*2,x,y);
    		else dadd(k*2+1,x,y);
    		w[k]=w[k*2]+w[k*2+1];
    		return ;
    	}
    	inline void qadd(int ll,int rr,int k,int x)
    	{
    		if(l[k]>=ll&&r[k]<=rr)
    		{
    			w[k]+=(r[k]-l[k]+1)*x;
    			f[k]+=x;
    			return ;
    		}
    		if(f[k]) down(k);
    		int mid=(l[k]+r[k])/2;
    		if(ll<=mid) qadd(ll,rr,k*2,x);
    		if(rr>mid) qadd(ll,rr,k*2+1,x);
    		w[k]=w[k*2]+w[k*2+1];
    		return ;
    	}
    	inline int dsum(int k,int x)
    	{
    		if(l[k]==r[k]) return w[k];
    		if(f[k]) down(k);
    		int mid=(l[k]+r[k])/2;
    		if(x<=mid) return dsum(k*2,x);
    		else return dsum(k*2+1,x);
    	}
    	inline int qsum(int ll,int rr,int k)
    	{
    		if(l[k]>=ll&&r[k]<=rr) return w[k];
    		if(f[k]) down(k);
    		int mid=(l[k]+r[k])/2;
    		int res=0;
    		if(ll<=mid) res+=qsum(ll,rr,k*2);
    		if(rr>mid) res+=qsum(ll,rr,k*2+1);
    		return res;
    	}
    }tr;
    

    对了,代码里我基本没有写注释,是因为我懒如果没有注释你也看得懂的话就说明你已经学会97%了,再自己手打一遍,就可以100%理解并学会了!

  • 相关阅读:
    Bootstrap 2.2.2 的新特性
    Apache POI 3.9 发布,性能显著提升
    SQL Relay 0.48 发布,数据库中继器
    ProjectForge 4.2.0 发布,项目管理系统
    红帽企业 Linux 发布 6.4 Beta 版本
    红薯 快速的 MySQL 本地和远程密码破解
    MariaDB 宣布成立基金会
    Percona XtraBackup 2.0.4 发布
    Rocks 6.1 发布,光盘机群解决方案
    精通Servlet研究,HttpServlet的实现追究
  • 原文地址:https://www.cnblogs.com/mk-oi/p/13555295.html
Copyright © 2011-2022 走看看