码迷,mamicode.com
首页 > 其他好文 > 详细

线段树

时间:2020-05-01 18:31:57      阅读:51      评论:0      收藏:0      [点我收藏+]

标签:cst   直接   思想   scan   如何   分治   ace   turn   push   

线段树

线段树(Segment Tree)是一种基于分治思想的二叉树结构,在区间进行信息统计。比区间划分的树状数组通用

1.线段树的每个节点代表一个区间。

2.线段树有唯一的根节点,即代表整个区间的结点。

3.线段树的叶节点代表一个长度为 1 的元区间。

4.对于内部节点 \([l,r]\) (非叶节点) ,它的左子节点为 \([l,mid]\) , 右子节点为 \([mid+1,r]\) , \(\large mid= \lfloor{\frac{l+r}{2}\rfloor}\)

long long ls(long long rt) {return rt << 1;}//左孩子
long long rs(long long rt) {return rt << 1 | 1;}//右孩子
long long fa(long long rt) {return rt >> 1;}//父亲

这里左右儿子的分界一定要清晰、统一

线段树主要用于高效解决连续区间的动态查询问题(单点/区间修改,区间求最值/求和,区间查询...),由于二叉树的性质,能保证每个操作的复杂度为 \(O(log(n))\) .

存储

跟 二叉树,二叉堆 类似 ,可用数组存储,根据层数由上到下,由左到右依次编号:

1、根节点编号为 1
2、编号为 \(x\) 的节点的左子节点编号为 \(x*2\) , 右子节点编号为 \(x*2+1\)

3、编号为 \(x\) 的父节点编号为 \(x/2\)

这样我们就用 \(struct\) 数组来存储线段树,在里面存储区间 \([l,r]\) ,还有需要维护的信息。
\(N\) 个叶结点的满二叉树有 \(2N-1\) 个节点,所以 保存线段树的数组长度不小于 \(4N\) 才保证不会越界

struct tree{
    long long l,r,sum,tag;//l,r表示区间范围是[l,r],sum是要维护的信息,tag是懒标记
}t[size*4];//开四倍哦

建树 \(O(nlogn)\)

面对输入的数组,如何建成一棵树呢?

1、利用根节点(全区间点),不断进行折半,生成子树,产生子问题,递归

2、递归到叶子节点,区间长度为 1 ,确定区间信息,进行回溯 ,由下往上传递信息并处理 ,直到递归到根节点 那么树就建好了

void build(long long rt,long long l,long long r)
{
    t[rt].l = l , t[rt].r = r;//确定区间范围
    if(l == r)
    {
        t[rt].sum = a[l];//递归到叶节点,确定区间信息
        //do something
        return;          //回溯传递信息
    }
    long long mid = (l + r) >> 1; //折半
    build(ls(rt),l,mid);          //递归左子树
    build(rs(rt),mid+1,r);        //递归右子树
    t[rt].sum = t[ls(rt)].sum + t[rs(rt)].sum;  //进行信息处理
}
//build(1,1,n);//调用入口(从根节点开始)

单点修改 \(O(log n)\)

对区间内的某一点进行修改,变动的只是包含此点的区间。 1、从根节点出发,递归找到代表区间 \([x,x]\) 的叶节点 2、从下往上更新 \([x,x]\) 和此叶子节点的祖先结点

void change(long long rt,long long x,long long v)
{
    if(t[rt].l == t[rt].r) {t[p].sum = v; return;} //递归到叶节点,确定区间信息,传递信息   
    long long mid = (t[rt].l + t[rt].r) >> 1;      //折半
    if(x <= mid) change(ls(rt),x,v);               //递归左子树
    else change(rs(rt),x,v);                       //递归右子树
    t[rt].sum = t[ls(rt)].sum + t[rs(rt)].sum;     //更新信息
}

区间修改

显而易见我们要对区间内的所有节点进行修改,但是时间复杂度退化为 \(O(n)\) ,这是我们无法接受的。

Lazy_tag

Lazy_tag 又称作 懒标记,延迟标记,如果要修改的区间把某个结点完全覆盖掉,就在此节点打一个Lazy_tag, 表明该节点曾经被修改,但其子节点尚未进行更改,目的是减少区间修改的时间复杂度,等到查询到此节点时,再根据信息修改其子节点。

这样延迟了修改的时间,故也称为延迟标记。

void change(long long rt,long long l,long long r,long long x)
{
    if(l <= t[rt].l && r >= t[rt].r)//要修改的区间把此结点完全覆盖掉
    {
        t[rt].sum += (t[rt].r - t[rt].l + 1) * x;//将此节点修改
        t[rt].tag += x;                          //打上懒标记
        return;                                  //回溯(怠惰)
    }

    if(t[rt].tag) push_down(rt);                 //有懒标记有往下踹(修改子节点)

    if(l <= t[ls(rt)].r){                        //与左子结点有重叠
        change(ls(rt),l,r,x);                    //递归左子树
    }
    if(r >= t[rs(rt)].l){                        //与右子节点有重叠
        change(rs(rt),l,r,x);                    //递归右子树
    }

    t[rt].sum = t[ls(rt)].sum + t[rs(rt)].sum;   //处理信息
}
void push_down(long long rt)
{
    t[ls(rt)].sum += t[rt].tag * (t[ls(rt)].r - t[ls(rt)].l + 1);//信息处理
    t[rs(rt)].sum += t[rt].tag * (t[rs(rt)].r - t[rs(rt)].l + 1);

    t[ls(rt)].tag += t[rt].tag;
    t[rs(rt)].tag += t[rt].tag;//打上孩子的懒标记

    t[rt].tag = 0;
}

区间查询

1.完全覆盖,直接回溯

2.与左子节点区间有重叠,查他

3.与左子节点区间有重叠,查他

long long check(long long rt,long long l,long long r)
{
    if(l <= t[rt].l && r >= t[rt].r)  return t[rt].sum; //查询的区间包含整个节点的区间,省事,加
    if(t[rt].tag) push_down(rt);                        //有懒标记就往下踹
    long long res = 0;
    if(l <= t[ls(rt)].r){                               //与左子节点区间有重叠,查他
        res += check(ls(rt),l,r);
    }
    if(r >= t[rs(rt)].l){                               //与左子节点区间有重叠,查他
        res += check(rs(rt),l,r);
    }
    return res;
}

例题

p3372

题目描述

如题,已知一个数列,你需要进行下面两种操作:

  1. 将某区间每一个数加上 \(k\)
  2. 求出某区间每一个数的和。
输入格式

第一行包含两个整数 \(n, m\),分别表示该数列数字的个数和操作的总个数。

第二行包含 \(n\) 个用空格分隔的整数,其中第 \(i\) 个数字表示数列第 \(i\) 项的初始值。

接下来 \(m\) 行每行包含 3 或 4 个整数,表示一个操作,具体如下:

  1. 1 x y k:将区间 \([x,y]\)内每个数加上 \(k\)
  2. 2 x y:输出区间 \([x,y]\) 内每个数的和。
输出格式

输出包含若干行整数,即为所有操作 2 的结果。

AC代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
long long n,m,k,x,y;
long long a[100000];

struct tree{
    long long l,r,sum,tag;
}t[400000];


long long ls(long long rt){return rt << 1;}
long long rs(long long rt){return rt << 1 | 1;}

void build(long long rt,long long l,long long r){
    t[rt].l = l,t[rt].r = r;
    if(l == r){
        t[rt].sum = a[l];
        return;
    }
    long long mid = (l + r) >> 1;
    build(ls(rt),l,mid);
    build(rs(rt),mid+1,r);
    t[rt].sum = t[ls(rt)].sum + t[rs(rt)].sum;
}

void push_down(long long rt){
    t[ls(rt)].sum += t[rt].tag * (t[ls(rt)].r - t[ls(rt)].l + 1);
    t[rs(rt)].sum += t[rt].tag * (t[rs(rt)].r - t[rs(rt)].l + 1);

    t[ls(rt)].tag += t[rt].tag;
    t[rs(rt)].tag += t[rt].tag;

    t[rt].tag = 0;
}

void change(long long rt,long long l,long long r,long long x){
    if(l <= t[rt].l && r >= t[rt].r){
        t[rt].sum += (t[rt].r - t[rt].l + 1) * x;
        t[rt].tag += x;
        return;
    }

    if(t[rt].tag) push_down(rt);

    if(l <= t[ls(rt)].r){
        change(ls(rt),l,r,x);
    }
    if(r >= t[rs(rt)].l){
        change(rs(rt),l,r,x);
    }

    t[rt].sum = t[ls(rt)].sum + t[rs(rt)].sum;
}

long long check(long long rt,long long l,long long r){
    if(l <= t[rt].l && r >= t[rt].r){
        return t[rt].sum;
    }

    if(t[rt].tag) push_down(rt);

    long long res = 0;

    if(l <= t[ls(rt)].r){
        res += check(ls(rt),l,r);
    }
    if(r >= t[rs(rt)].l){
        res += check(rs(rt),l,r);
    }
    return res;
}

int main(){
    cin >> n >> m;
    for(long long i = 1;i <= n; i++) cin >> a[i];
    build(1,1,n);

    for(long long i = 1;i <= m; i++){
        cin >> k;
        if(k == 1){
            cin >> x >> y >> k;
            change(1,x,y,k);
        }else{
            cin >> x >> y;
            cout << check(1,x,y) << endl;
        }
    }
    return 0;
}

p3373

\(\large 题目描述\)

如题,已知一个数列,你需要进行下面三种操作:

  • 将某区间每一个数乘上 x
  • 将某区间每一个数加上 x
  • 求出某区间每一个数的和
\(\large输入格式\)

第一行包含三个整数 n,m,p, 分别表示该数列数字的个数、操作的总个数和模数。

第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。

接下来 m 行每行包含若干个整数,表示一个操作,具体如下:

操作 11: 格式:1 x y k 含义:将区间 \([x,y]\) 内每个数乘上 kk

操作 22: 格式:2 x y k 含义:将区间 \([x,y]\)内每个数加上 kk

操作 33: 格式:3 x y 含义:输出区间 \([x,y]\) 内每个数的和对 \(p\) 取模所得的结果

\(\large 输出格式\)

输出包含若干行整数,即为所有操作 3 的结果。

AC代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 1000000+50
using namespace std;
struct tree{
	long long l,r,sum,tag,tag_x;//l,r左右端点,sum为结点对应区间和,tag为加法标记,tag_x为乘法标记 
}t[N]; 

long long a[N];
long long m,n,p = 9223372036854775807,k; 
long long ls(long long rt){return rt<<1;}//左孩子 
long long rs(long long rt){return rt<<1|1;}//右孩子 

void build(long long rt,long long l,long long r){
	t[rt].tag_x = 1; t[rt].tag = 0;//初始化
	t[rt].l = l,t[rt].r = r;//建立一个结点,更新左右端点标记 
	if(l == r){ //如果到了叶子结点 
		t[rt].sum = a[l] % p; //不要忘记取模操作 
		return;
	}
	long long mid = (l + r) >> 1; //中间节点 
	build(ls(rt),l,mid);
	build(rs(rt),mid+1,r); //如果不是叶子结点,就分别建立左右孩子 
	t[rt].sum = (t[ls(rt)].sum + t[rs(rt)].sum) % p; // 更新sum 
}

void push_down(long long rt){
    t[ls(rt)].tag_x = (t[ls(rt)].tag_x * t[rt].tag_x) % p;
    t[rs(rt)].tag_x = (t[rs(rt)].tag_x * t[rt].tag_x) % p;//乘法懒标记更新后取模 

    t[ls(rt)].tag = (t[ls(rt)].tag * t[rt].tag_x) % p;
    t[rs(rt)].tag = (t[rs(rt)].tag * t[rt].tag_x) % p;//加法懒标记更新 

    t[ls(rt)].sum = (t[ls(rt)].sum * t[rt].tag_x) % p;
    t[rs(rt)].sum = (t[rs(rt)].sum * t[rt].tag_x) % p;//sum结点对应区间和更新

    t[rt].tag_x = 1; //父亲的标记已经下传,就归零(因为是乘法,所以要调到1) 
		
    t[ls(rt)].tag = (t[ls(rt)].tag + t[rt].tag) % p;
    t[rs(rt)].tag = (t[rs(rt)].tag + t[rt].tag) % p;//加法懒标记更新 
	
    t[ls(rt)].sum += (t[ls(rt)].r - t[ls(rt)].l + 1) * t[rt].tag;
    t[rs(rt)].sum += (t[rs(rt)].r - t[rs(rt)].l + 1) * t[rt].tag;//sum结点对应区间和更新
    
    t[rt].tag = 0;//父亲的标记已经下传,就归零 
}

void change(long long rt,long long x,long long y,long long z){
	if(x <= t[rt].l && y >= t[rt].r){
		t[rt].tag = (t[rt].tag + z) % p;
		t[rt].sum = (t[rt].sum + (t[rt].r - t[rt].l + 1) * z) % p; //如果修改区间覆盖了这个节点的区间,就更新 
		return;
	}
    if(t[rt].tag || t[rt].tag_x != 1) push_down(rt);//访问孩子结点的时候一定先把懒标记 传下去 
	long long mid = (t[rt].l + t[rt].r) >> 1;
	if(x <= mid){
		change(ls(rt),x,y,z);
	}
	if(y > mid){
		change(rs(rt),x,y,z);
	}
	//分别往左右儿子传 
	
	t[rt].sum = (t[ls(rt)].sum + t[rs(rt)].sum) % p; //维护 
}

void change_x(long long rt,long long x,long long y,long long z){
	 if(x <= t[rt].l && y >= t[rt].r){
	 	t[rt].tag_x = (t[rt].tag_x * z) % p;
	 	t[rt].sum = (t[rt].sum * z) % p;
	 	t[rt].tag = (t[rt].tag * z) % p;//如果修改区间覆盖了这个节点的区间,就更新 
	 	return;
	 }
	 if(t[rt].tag || t[rt].tag_x != 1) push_down(rt);//访问孩子结点的时候一定先把懒标记 传下去 
	 long long mid = (t[rt].l + t[rt].r) >> 1;
	 if(x <= mid){
	 	change_x(ls(rt),x,y,z);
	 }
	 if(y > mid){
	 	change_x(rs(rt),x,y,z);
	 }
	 //分别往左右儿子传 
	 t[rt].sum = (t[ls(rt)].sum + t[rs(rt)].sum) % p; //维护
}

long long getsum(long long rt,long long x,long long y){
	long long res = 0;
	if(x <= t[rt].l && y >= t[rt].r){
		return t[rt].sum % p;
	}
    if(t[rt].tag || t[rt].tag_x != 1) push_down(rt);
	long long mid = (t[rt].r + t[rt].l) >> 1;
	if(x <= mid){
		res += getsum(ls(rt),x,y);
	}
	if(y > mid){
		res += getsum(rs(rt),x,y);
	}
	return res % p;
}

int main(){
	long long i,j,x,y,z;
	scanf("%lld%lld%lld",&n,&m,&p); 
	for(i = 1;i <= n; i++)
		 scanf("%lld",&a[i]);
	build(1,1,n);
	for(i = 1;i <= m; i++)
    {
		scanf("%lld",&k);
		if(k == 1)
        {
			scanf("%lld%lld%lld",&x,&y,&z);
			change_x(1,x,y,z);
		}
        else if(k == 2)
        {
			scanf("%lld%lld%lld",&x,&y,&z);
			change(1,x,y,z);
		}
        else if(k == 3)
        {
			scanf("%lld%lld",&x,&y);
			printf("%lld\n",getsum(1,x,y));
		}
     }
	return 0;
} 

线段树

标签:cst   直接   思想   scan   如何   分治   ace   turn   push   

原文地址:https://www.cnblogs.com/wzy1744315462/p/12814235.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!