数据结构:线段树

摘自《算法竞赛进阶指南》。

线段树是一种基于分治思想的二叉树结构,用于在区间上进行信息统计。

线段树的基本特征:
1.线段树的每个节点都代表一个区间。
2.线段树具有唯一的根节点,代表的区间是整个统计范围,如[1,N]。
3.线段树的每个叶节点都代表一个长度为1的元区间[x,x]。
4.对于每个内部节点[l,r],它的左子节点是[l,mid],右子节点是[mid+1,r],其中mid=(l+r)/2(向下取整)。

线段树的节点编号方法:“父子二倍”节点编号法
1.根节点编号为1。
2.编号为x的节点的左子节点的编号为x*2,右子节点编号为x*2+1。

注意:保存线段树的数组长度要不小于4N才能保证不会越界。(N为叶节点数)

线段树的基本操作(以维护区间最大值为例)

线段树的建树

  • 下面这段代码建立了一棵线段树并在每个节点上保存了对应区间的最大值。
struct SegmenTree{
    int l,r;
    int dat;
}t[N*4];//struct数组存储线段树 

void build(int p,int l,int r){
    t[p].l=l,t[p].r=r;//节点p代表区间[l,r]
    if(l==r){t[p].dat=a[l];return;}//叶节点
    int mid=(l+r)/2;//折半
    build(p*2,l,mid);//左子节点[l,mid],编号p*2
    build(p*2+1,mid+1,r);//右子节点[mid+1,r],编号p*2+1
    t[p].dat=max(t[p*2].dat,t[p*2+1].dat);//从下往上传递信息
} 

build(1,1,n);//调用入口

线段树的单点修改

  • 把a[x]的值修改为v,时间复杂度:O(log N)。

在线段树中,根节点(编号为1的节点)是执行各种指令的入口。我们需要从根节点出发,递归找到代表区间[x,x]的叶节点,然后从下往上更新[x,x]以及它的所有祖先节点上保存的信息。

void change(int p,int x,int v){
    if(t[p].l==t[p].r){t[p].dat=v;return;}//找到叶子结点
    int mid=(t[p].l+t[p].r)/2;
    if(x<=mid)change(p*2,x,v);//x属于左半区间
    else change(p*2+1,x,v);//x属于右半区间
    t[p].dat=max(t[p*2].dat,t[p*2+1].dat);//从下往上更新信息
} 

change(1,x,v);//调用入口 

线段树的区间查询

  • 查询序列a在区间[l,r]上的最大值。

从根节点开始,递归执行以下过程:
1.若[l,r]完全覆盖了当前节点代表的区间,则立即回溯,并且该节点的dat值为候选答案。
2.若左子节点与[l,r]有重叠部分,则递归访问左子节点。
3.若右子节点与[l,r]有重叠部分,则递归访问右子节点。

int ask(int p,int l,int r){
    if(l<=t[p].l&&r>=t[p].r)return t[p].dat;//完全包含
    int mid=(t[p].l+t[p].r)/2;
    int val=-(1<<30);//负无穷大
    if(l<=mid)val=max(val,ask(p*2,l,r));//左子节点有重叠
    if(r>mid)val=max(val,ask(p*2+1,l,r));//右子节点有重叠
    return val;
} 

cout<<ask(1,l,r)<<endl;//调用入口 

该查询过程会把询问区间[l,r]在线段树生分成O(log N)个节点,取它们的最大值作为答案。
原因:在每个节点[pl,p_r]上,设mid=(pl+pr)/2(向下取整),可能会出现以下几种情况:
1.l≤pl≤pr≤r,即完全覆盖了当前节点,直接返回。
2.pl≤l≤pr≤r,即只有l处于节点之中。
(1)l>mid,只会递归右子树。
(2)l≤mid,虽然递归两棵子树,但是右子节点会在递归后直接返回。
3.l≤pl≤r≤pr,即只有r处于节点之中,与情况2类似。
4.pl≤l≤r≤r,即l与r都处于节点之中。
(1)l,r都位于mid的一侧,只会递归一棵子树。
(2)l,r分别位于mid的两侧,递归左右两棵子树。

  • 只有情况4(2)会真正产生对左右两颗子树的递归。这种情况至多发生一次,之后在子节点上就会变成情况2或3。因此,上述查询过程的时间复杂度为O(2logN)=O(log N)。

【例题1】:你能回答这些问题吗
【例题2】:区间最大公约数

延迟标记(遇到区间修改时)

我们在修改指令时,可以在l≤pl≤pl≤r的情况下立即返回,只不过在回溯之前向节点p增加标记,标识“该节点曾经被修改,但其子节点尚未被更新”。
在后续的指令中,需要从节点p向下递归,我们再检查p是否具有标记。若有标记,就根据标记信息更新p的两个子节点,同时为p的两个子节点增加标记,然后清除p的标记。
这样一来,每条修改的指令的时间复杂度从O(N)降到了O(log N)。

【例题】:一个简单的整数问题
主要代码如下:

#define 100000+10

struct N SegmentTree{
    int l,r;
    long long sum,add;
    #define l(x) tree[x].l
    #define r(x) tree[x].r
    #define add(x) tree[x].add
    #define sum(x) tree[x].sum
}tree[N*4];

int a[N],n,m;

void build(int p,int l,int r){
    l(p)=l,r(p)=r;
    if(l==r){sum(p)=a[l];return;}
    int mid=(l+r)/2;
    build(p*2,l,mid);
    build(p*2+1,mid+1,r);
    sum(p)=sum(p*2)+sum(p*2+1);
} 

void spread(int p){
    if(add(p)){//节点p有标记
        sum(p*2)+=add(p)*(r(p*2)-l(p*2)+1);//更新左子节点信息
        sum(p*2+1)+=add(p)*(r(p*2+1)-l(p*2+1)+1);//更新右子节点
        add(p*2)+=add(p);//给左子节点打延迟标记
        add(p*2+1)+=add(p);//给右子节点打延迟标记
        add(p)=0;//清除b的标记
    }
}

void change(int p,int l,int r,int d){
    if(l<=l(p)&&r>=r(p)){//完全覆盖
        sum(p)+=(long long)d*(r(p)-l(p)+1);//更新节点信息
        add(p)+=d;//给节点打延迟标记
        return;
    }
    spread(p);//下传延迟标记
    int mid=(l(p)+r(p))/2;
    if(l<=mid)change(p*2,l,r,d);
    if(r>mid)change(p*2+1,l,r,d);
    sum(p)=sum(p*2)+sum(p*2+1);
}

long long ask(int p,int l,int r){
    if(l<=l(p)&&r>=r(p))return sum(p);
    spread(p);//下传延迟标记
    int mid=(l(p)+r(p))/2;
    long long val=0;
    if(l<=mid)val+=ask(p*2,l,r);
    if(r>mid)val+=ask(p*2+1,l,r);
    return val;
}

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        cin>>a[i];
    build(1,1,n);
    while(m--){
        char op[2];int l,r,d;
        cin>>op>>l>>r;
        if(op[0]==‘C‘){
            cin>>d;
            change(1,l,r,d);
        }
        else cout<<ask(1,l,r);
    }
}

扫描线

【例题1】:亚特兰蒂斯
【例题2】:窗内的星星

原文地址:https://www.cnblogs.com/zhengchang/p/xianduanshu1.html

时间: 2024-10-11 07:27:19

数据结构:线段树的相关文章

HDU 4902 Nice boat(数据结构-线段树)

Nice boat Problem Description There is an old country and the king fell in love with a devil. The devil always asks the king to do some crazy things. Although the king used to be wise and beloved by his people. Now he is just like a boy in love and c

Chapter 3. 数据结构 线段树

Chapter 3. 数据结构 线段树 Sylvia's I.单点修改,区间查询. 模板: //单点修改 区间求和 //1操作 单点修改//2操作 区间求和 #include<cstdio> #include<iostream> using namespace std; #define MAXN 500005 int sum[MAXN<<2]; int n,m; void PushUp(int rt){//求和 sum[rt]=sum[rt<<1]+sum[

HDU 1394 Minimum Inversion Number (数据结构-线段树)

Minimum Inversion Number Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Total Submission(s): 9514    Accepted Submission(s): 5860 Problem Description The inversion number of a given number sequence a1, a2, ..., an

不可能数据结构(线段树+思维+找规律)

不可能数据结构 Description造一道数据结构题是一件很累的事情.即使是有了坚固的数据结构造题程式,出题人仍然不能够一劳永逸.问题大概出在,造题程式并不总能生成对的题.比如,造题程式无意产生了一道出题人认为不可做题,这便成了一道错题.嘛,出了错题,其实也是没有关系的.出题人可以拿着暴力程序跑一个星期来跑出所需要的测试数据.一个星期跑不出来就延期一个星期机考好了,至于算法和讲题嘛,也就看看现场ac代码现学现卖咯?可能对你们来说很不幸的现实是,这道题似乎是个错题.给你一个初始n个元素的序列,有

数据结构——线段树

线段树是一种基于分治思想的类似于二叉树的数据结构,一般用于数组的信息统计,相比于树状数组,线段树有着更广阔的应用空间,但是相对的其代码量长,且常数大 一. 首先我们来讲线段树的建树过程,请看下图: 这张图就是线段树的存储结构,我们从最长的区间开始依次分成两部分,每一部分都有一个需要维护的权,建树过程比较简单,代码如下: inline void build(int l,int r,int rt) //l表示当前的左端点,r表示右端点,rt是当前区间的编号 { if(l == r) //当左右端点相

[数据结构-线段树] poj 2528

在一面墙上贴海报,贴的顺序给出了,求最后能被看到的海报数量. 纯粹的线段树模拟题. 但数据范围给了10^7,超内存了. 实际上这里用了一个小技巧,虽然墙的宽度是很大的,但海报数量只有10000,所以这10^7个数中真正用到的数很少,这样的话就只需要把没用到的数给"删去",剩下来的数从小到大映射为新的数,这样空间复杂度就大大降低了. 比如题目给的样例: 1 4 2 6 8 10 3 4 7 10   用到的数有:1 2 3 4 6 7 8 10 可以把它们映射为: 1 2 3 4 6 7

数据结构---线段树

线段树 转载请注明出处,谢谢!http://blog.csdn.net/metalseed/article/details/8039326  持续更新中···   一:线段树基本概念 1:概述 线段树,类似区间树,是一个完全二叉树,它在各个节点保存一条线段(数组中的一段子数组),主要用于高效解决连续区间的动态查询问题,由于二叉结构的特性,它基本能保持每个操作的复杂度为O(lgN)! 性质:父亲的区间是[a,b],(c=(a+b)/2)左儿子的区间是[a,c],右儿子的区间是[c+1,b],线段树

模板 - 数据结构 - 线段树(单点修改)

这里是以区间最大值为例,要修改成其他的运算,注意修改每个函数的运算以及query中返回的无关值. 这里的区间最大值设置的最小元素为-1(在query中表示与当前区间不相交的区间的结果). 注意因为调用的方式传入l与r是(1,n),所以这个线段树(包括a)其实是从1开始计数的. 最后,小心爆MAXM. const int MAXM=200000; int a[MAXM+5],st[(MAXM<<2)+5]; void build(int o,int l,int r){ if(l==r) st[o

数据结构——线段树(C++)

源代码: #include<cstdio>int m,n,num(0),h[100001];struct treetype{ int left,right,lefts,rights,sum; //本代码中,应用为半开半闭区间.}i[200020]; //应用了完全二叉树的结点个数公式.void x1(int t1,int t2) //建树.{ int t=++num; //注意,在线段树中,始终遵循左小右大原则. i[t].left=t1; i[t].right=t2; if (t1!=t2-

高级数据结构-线段树

1.模板(以维护最小值为例) #include<iostream> #include<stdio.h> #define LEN 11 #define MAX 1<<30 using namespace std; int arr[LEN]={1,3,7,6,8,5,3,2,7,2,9}; int st[LEN*3]; //segment tree int n; void init(int len){ n=1; while(n<len) n*=2; //不断乘以2,知