线段树(从入门到进阶)


首先,讲解线段树之前,应该了解到线段树应该是一种工具,可以将一些对于区间(或者线段)的修改、维护,从O(N)的时间复杂度变成O(logN)。

一.线段树概念引入

线段树是一种二叉树,也就是对于一个线段,我们会用一个二叉树来表示。比如说一个长度为5的线段,我们可以表示成这样:
在这里插入图片描述
这代表着如果你要表示线段的和,那么最上面的根节点权值就等于两个儿子节点的权值之和。
那么就可以得到:节点i的权值 = i节点的左儿子权值 + i节点的右儿子权值
然后我们就可以根据这个结果来建树,设一个结构体数组tree,tree[i].l和tree[i].r就分别表示这个点代表线段的左右范围
二叉树的左右孩子节点编号等于当前节点编号 * 2和 * 2 + 1
那么我们就可以得到:tree[i].sum = tree[i << 1].sum + tree[i << 1 | 1].sum

inline ll ls(ll i) { return i << 1;}
inline ll rs(ll i) { return i << 1 | 1;}

inline void build(ll i, ll l, ll r)   //递归建树	
{
	if (l == r)  //这个节点是叶子节点
	{
		tree[i].sum = a[l];
		return;
	}

	ll mid = (l + r) >> 1;

	build(ls(i), l, mid);   //左孩子
	build(rs(i), mid + 1, r);   //右孩子
	push_up(i);  //将当前节点的sum更新
}

我们构建线段树的时候一般只有数据大小的4倍个节点。

二.简单线段树

1.区间查询,单点修改

首先,我们要用线段树来干什么呢?其实,线段树就如其名,是用来维护一个线段或区间的,例如你想求出1 ~ 1000的区间中,23 ~ 500这些元素的和,那么朴素的方法就是for循环遍历相加,这样当然可以解决,但是太慢了。
所以,我们要想出一种新的方法,线段树就可以很好的解决这样的问题。
好的,我们按刚才的那幅图为例,给它的叶子节点附上1,2,3,4,5的值
那么我们按照刚才建树的逻辑得出以下的图。
在这里插入图片描述
现在,我们来个数据来模拟一下区间查询。例如,我们现在要得出1 ~ 4区间的所有元素的权值之和(也就是区间查询)。
1。我们要得出1 ~ 4区间的和,我们先从根节点开始查询,发现它的左孩子1 ~ 3和它有关。
2. 我们发现它的左孩子1 ~ 3完全被它包含,那么我们直接返回1 ~ 3的值。并回到1 ~ 5区间.
3. 到了4 ~ 5区间,我们发现这个区间也和它有关,那么我们来到4 ~ 5区间。
4. 我们发现4 ~ 5区间的左孩子4 ~ 4这个区间和答案区间有关。那么我们把4 ~ 4这个区间的值返回到最开始。
5. 然后我们最开始要创建一个初始值为0的t变量
我们总结一下,线段树的查询方法:

1、如果这个区间被完全包括在目标区间里面,直接返回这个区间的值
2、如果这个区间的左儿子和目标区间有交集,那么搜索左儿子
3、如果这个区间的右儿子和目标区间有交集,那么搜索右儿子

写成代码就是这样:

ll query(ll q_x, ll q_y, ll l, ll r, ll i)
{
	ll t = 0;

	if (q_x <= l && r <= q_y) return tree[i].sum;

	ll mid = (l + r) >> 1;

	if (q_x <= mid) t += query(q_x, q_y, l, mid, ls(i));
	if (q_y > mid) t += query(q_x, q_y, mid + 1, r, rs(i));

	return t;
}

接下来,我们来讲解单点修改应该怎么实现。
首先,因为是只修改一个点的权值,那么只需要把区间的第dis位权值加上k。
那么,从根节点开始,看这个dis点是在左儿子还是右儿子,在哪就往哪跑就好,最后返回的时候再进行一下tree[i].sum = tree[i << 1].sum + tree[i << 1 | 1].sum

来张图帮助理解一下吧。黄色是代表去的路径,淡黄色是代表返回的路径。
在这里插入图片描述
变成代码就是这样:

inline void update(int xl, int k, int l, int r, int i)
{
    if(l == r)    //叶子节点就将要加的值给本来的sum加上
    {
        tree[i].sum += k;
        return;
    }

    int mid = q(l , r);

    if(xl <= mid)        //这个点在左边就往左儿子跑
    {
        update(xl, k, l, mid, ls(i));
    }
    else        //否则往右儿子跑
    {
        update(xl, k, mid + 1, r, rs(i));
    }

    push_up(i);    //将权值进行更新
    return;
}

2.区间修改,单点查询

区间修改和单点查询,我们的思路就变为:如果把这个区间加上k,相当于把这个区间涂上一个k的标记,然后单点查询的时候,就将这个点的父区间的值加上。

这里面给区间贴标记的方式与上面的区间查找类似,原则还是那三条,只不过第一条:如果这个区间被完全包括在目标区间里面,直接返回这个区间的值变为了如果这个区间如果这个区间被完全包括在目标区间里面,将这个区间标记k

三.区间+/-修改与区间查询

开始之前,那么难免发出疑问,区间修改和单点修改有什么不同吗?为什么要分开来介绍?
答案是没有错,它们两个虽然都是修改,但如果我们区间修改也按照刚才的思路来的话就会出现错误。
我们举个栗子:如果要给1 ~ 3区间的值加上2,按照单点修改的思路,我们发现到了1 ~ 3区间后就返回了,但我们应该是让这个区间每个元素都加上2呀。那么这样的思路肯定就没办法实现了。

那么既然出现了这个问题,我们就要想办法解决。
接下来要介绍的懒惰标记和push_down操作就是解决的办法。push_down实际上就是应用懒惰标记的操作。

那么我们就来看看懒惰标记是怎么解决这个问题的吧。
我们通过分析单点修改和区间修改,发现区间修改有可能会提前就停止递归,而导致了这个点的孩子节点本应同样改变的操作没有实现。这就是问题所在。
那么我们想想叶子节点都和它的父节点有关系,它的父节点又与它的父节点有关系。这样,我们就想既然有联系,我们就用个标记来记录这个修改值,然后,一层一层的传递下去。这样在我们得到答案区间时,它本身有了个懒惰标记,表明这个区间是改变过值的。
查询的时候就需要将这个表记往下传递。
通过刚才的操作,我们就可以解决这个问题,写成代码就是这样

inline void f(ll i, ll l, ll r, ll k)   //将儿子节点的值与懒惰标记进行更新
{
	tree[i].lazy = tree[i].lazy + k;
	tree[i].sum = tree[i].sum + k * (r - l + 1);
}

inline void push_down(ll i, ll l, ll r)   //将懒惰标记传递给儿子节点
{
	ll mid = (l + r) >> 1;

	f(ls(i), l, mid, tree[i].lazy);
	f(rs(i), mid + 1, r, tree[i].lazy);
	tree[i].lazy = 0;    //记得传递后要置零
}

inline void update(ll xl, ll xr, ll l, ll r, ll i, ll k)   //区间修改
{
	if (xl <= l && r <= xr)    //如果修改区间包含了这个节点区间那么直接更新
	{
		tree[i].sum += k * (r - l + 1);
		tree[i].lazy += k;
		return;
	}
	push_down(i, l, r);   //将懒惰标记传递给儿子节点

	ll mid = (l + r) >> 1;

	if (xl <= mid) update(xl, xr, l, mid, ls(i), k);
	if (xr > mid) update(xl, xr, mid + 1, r, rs(i), k);

	push_up(i);   //记得要进行向上传递信息
}

ll query(ll q_x, ll q_y, ll l, ll r, ll i)    //区间查询
{
	ll t = 0;

	if (q_x <= l && r <= q_y) return tree[i].sum;

	ll mid = (l + r) >> 1;

	push_down(i, l, r);  将懒惰标记传递给儿子节点
	if (q_x <= mid) t += query(q_x, q_y, l, mid, ls(i));
	if (q_y > mid) t += query(q_x, q_y, mid + 1, r, rs(i));

	return t;
}

四.区间乘除修改与查询

1.乘法线段树

如果这个线段树只有乘法,那么直接令tree[i].lazy = k,然后tree[i].sum = k就好了。但是,如果是又加又乘的,那就不一样了。
我们向下传递lazy时,需要考虑,是先加还是先乘。其实,解决办法很简单,我们只需要将lazy分成两个,一个是加法的plz一个是乘法的mlz。
mlz很简单处理,只需要在push_down时直接
父亲的就可以了,但是,加法就需要把原来的plz
父亲的mlz再加上父亲的plz。

接下来就是代码:

inline void pushdown(ll i,ll l, ll r){//注意这种级别的数据一定要开long long
    ll k1=tree[i].mlz,k2=tree[i].plz;

	ll mid = (l + r) >> 1;

    tree[ls(i)].sum=(tree[ls(i)].sum*k1+k2*(mid - l+1))%p;
    tree[rs(i)].sum=(tree[rs(i)].sum*k1+k2*(r - mid))%p;
    tree[ls(i)].mlz=(tree[ls(i)].mlz*k1)%p;
    tree[rs(i)].mlz=(tree[rs(i)].mlz*k1)%p;
    tree[ls(i)].plz=(tree[ls(i)].plz*k1+k2)%p;
    tree[rs(i)].plz=(tree[rs(i)].plz*k1+k2)%p;
    tree[i].plz=0;
    tree[i].mlz=1;
    return ;
}

2.根号线段树

首先,我们要知道c++的除法是向下取整,很明显,(a+b)/k!=a/k+b/k(在向下取整的情况下),而根号,很明显根号(a)+根号(b)!=根号(a+b)那么怎么办?

第一个想法就是暴力,对于每个要改动的区间l~r,把里面的每个点都单独除,但这样就会把时间复杂度卡得比大暴力都慢(因为多个常数),所以怎么优化?

我们对于每个区间,维护她的最大值和最小值,然后每次修改时,如果这个区间的最大值根号和最小值的根号一样,说明这个区间整体根号不会产生误差,就直接修改(除法同理)
其中,lazytage把除法当成减法,记录的是这个区间里每个元素减去的值。

代码是这样的:

inline void Sqrt(int i, int lx, int yr, int l, int r){
    if(l >= lx && r<=yr && (tree[i].minn - (long long)sqrt(tree[i].minn)) == (tree[i].maxx - (long long)sqrt(tree[i].maxx))){//如果这个区间的最大值最小值一样
        long long u = tree[i].minn - (long long)sqrt(tree[i].minn);//计算区间中每个元素需要减去的
        tree[i].lz += u;
        tree[i].sum -= (tree[i].r-tree[i].l+1)*u;
        tree[i].minn -= u;
        tree[i].maxx -= u;
            //cout<<"i"<<i<<" "<<tree[i].sum<<endl;
        return ;
    }
    if(r < lx || l > yr)  return ;
    push_down(i);

	ll mid = (l + r) >> 1;

    if(mid >= l)  Sqrt(ls(i), lx, yr, l, mid);
    if(mid + 1 <= r)  Sqrt(rs(i), lx, yr, mid + 1, r);
    tree[i].sum = tree[ls(i)].sum+tree[rs(i)].sum;
    tree[i].minn = min(tree[ls(i)].minn,tree[rs(i)].minn);//维护最大值和最小值
    tree[i].maxx = max(tree[ls(i)].maxx,tree[rs(i)].maxx);
    //cout<<"i"<<i<<" "<<tree[i].sum<<endl;
    return ;
}

然后pushdown没什么变化,就是要记得tree[i].minn、tree[i].maxx也要记得-lazytage。

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
线段树是一种用来解决区间查询问题的数据结构。在CSND的线段树入门指南中,介绍了线段树的基本原理和实现方法,并且提供了进阶内容来扩展应用。 线段树的基本原理是将待查询的区间划分为若干个较小的子区间,并将每个子区间的信息预处理保存在树节点中。通过在树上的查询和更新操作,可以有效地解决区间最值、区间修改、区间合并等问题。 在入门阶段,CSND的指南首先介绍了线段树的基本结构和构建方法。通过递归思想和分治策略,可以将一个区间划分为两个子区间,并依次构建子区间的线段树,最终构建出整个区间的线段树。通过优化构建过程,如使用线性时间复杂度的构建方法,可以提高线段树的构建效率。 在进阶阶段,CSND的指南介绍了线段树的应用扩展。例如,可以使用线段树解决静态区间最值查询问题,即在一个不可修改的区间中快速计算最大或最小值。另外,还可以使用线段树解决动态区间修改问题,即可以在区间内进行元素的插入、删除、更新等操作,并支持快速的查询操作。 此外,CSND的指南还介绍了线段树的一些常见优化技巧,如懒惰标记、矩阵树状数组等。这些优化方法可以进一步提高线段树的查询和更新效率,适用于一些特殊的应用场景。 总的来说,通过CSND的线段树入门进阶指南,我们可以全面了解线段树的基本原理和常见应用,并学会使用线段树解决各种区间查询问题。这对于算法竞赛、数据结构设计等领域都具有重要的实用价值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值