线段树解析

来源:互联网 发布:变声软件怎么实现 编辑:程序博客网 时间:2024/06/05 16:31

1.为什么要用线段树

例1:有M个数排成一列,初始值全为0,然后做N次操作,每次我们可以进行如下操作:
(1)将指定区间的每个数加上一个值;
(2)将指定区间的所有数置成一个值;
(3)询问一个区间上的最小值、最大值、所有数的和。

一般的模拟算法:
       用一张线性表表示整个数列,每次执行前两个操作的时候,将对应区间里的数值逐一进行修改,
执行第三个操作的时候,线性扫描询问区间,求出三个统计值,每次维护的时间复杂度O(m),整体的时间复杂度O(mn)。
readln(m,n);for i:=1 to n dobegin    read(t);    if t=1     //指定区间加上一个值    then begin                readln(left,right,x);                for j:=left to right do                   a[j]:=a[j]+x;            end;if t=2  //指定区间置成一个值then begin           readln(left,right,x);           for j:=left to right do              a[j]:=x;        end;if t=3    //询问一个区间的最小值、最大值、和then begin            readln(left,right);            min:=a[left];     max:=a[left];    sum:=a[left];            for j:=left+1 to right do            begin                if a[j]<min   then min:=a[j];                if a[j]>max  then max:=a[j];                sum:=sum+a[j];            end;            writeln(min,' ',max,' ',sum);        end;

M,N的规模
一般的模拟
M=100000,N=5000
2.06秒
M=100000,N=10000
4.14秒
M=100000,N=50000
21.32秒
M=100000,N=100000
43.36秒

       当m、n的值比较大时,这个算法就太低效了。
其低效的原因主要是我们在每一次操作中都是针对每个元素进行维护的,而这里我们进行的操作都是针对一个区间进行操作的。


       假如设计一种数据结构,能够直接维护所需处理的区间,那么就能更加有效地解决这个问题。


2.线段树的结构
定义1:长度为1的线段称为元线段。
定义2:一棵树被称为线段树,当且仅当这棵树满足如下条件:
    (1)该树是一棵二叉树;
    (2)树中的每一个结点都对应一条线段[a,b];
    (3)树中的结点是叶子结点,当且仅当它所代表的线段是元线段;
    (4)树中非叶子结点都有左右两棵子树,左子树树根对应线段[a,(a+b)/2],右子树树根对应线段[(a+b)/2,b]。


常用的是需要对数列进行处理,将区间[a,b]分解为[a,(a+b)/2],[(a+b)/2+1,b],当a=b时表示为一个叶子结点,表示数列中的一个数。

3.线段树的性质
性质1:长度范围为[1,L]的一棵线段树的深度不超过log2(L-1)+1;
性质2:线段树上的结点个数不超过2L个;
性质3:线段树把区间上的任意一条线段都分成不超过2log2L条线段。

4.线段树的存储方式
(1)链表实现:
       Node=^TNode;
       TNode=record
                       Left,Right:integer;
                       LeftChild,RightChild:Node;
                    end;
(2)数组模拟链表:
Left,Right,LeftChild,RightChild:array[1..n] of integer;
(3)堆的结构:
根节点为1,对于结点x,其左孩子为2x,右孩子为2x+1

5.线段树的构造
procedure build(cur:Node;l,r:integer);begin    cur^.Left:=l;    cur^.Right:=r;    if l=r    then begin                cur^.LeftChild:=nil;   cur^.RightChild:=nil;            end    else begin                new(cur^.LeftChild);                new(cur^.RightChild);                build(cur^.LeftChild,l,(l+r) div 2);                build(cur^.RightChild,(l+r) div 2+1,r);            end; end;
6.线段树的查询
线段树的查询procedure query(cur:Node;l,r:integer);var    LC,RC:Node;begin    LC:=cur^.LeftChild;  RC:=cur^.RightChild;    if (l<=cur^.Left) and (cur^.Right<=r)     then writeln(cur^.Left,‘ ’,cur^.Right);  //可以增加其他操作    else begin                if l<=(cur^.Left+cur^.Right) div 2 then query(LC,l,r);                if r>(cur^.Left+cur^.Right) div 2 then query(RC,l,r);           end;end;

7.线段树的维护
       如果想要让线段树发挥实际的作用,必须在每个结点上维护额外的域来记录更多的信息。
       首先看一个引例的弱化版,假设每次修改操作只能对其中某个元素进行修改。
       在每个结点上额外维护3个域:max、min、sum,分别表示子树上的最大值、最小值和所有元素的和。
8.对元素进行修改
procedure insert(cur:Node;x,num:integer);var  LC,RC:Node;begin    LC:=cur^.LeftChild;  RC:=cur^.RightChild;    if cur^.Left=cur^.Right  //对叶结点的处理    then begin                cur^.Min:=num;   cur^.Max:=num;  cur^.Sum:=num;            end   else begin               if x<=(cur^.Left+cur^.Right) div 2 then insert(LC,x,num);               if x>(cur^.Left+cur^.Right) div 2 then insert(RC,x,num);               cur^.Sum:=LC^.Sum+RC^.Sum;  //上推               if (LC^.Max>RC^.Max) then cur^.Max:=LC^.Max                                                     else cur^.Max:=RC^.Max;               if (LC^.Min<RC^.Min) then cur^.Min:=LC^.Min                                                   else cur^.Min:=RC^.Min;           end;end;
9.对区间进行查询
function querysum(cur:Node;l,r:integer):integer;var   LC,RC:Node;  ret:integer;begin    LC:=cur^.LeftChild;    RC:=cur^.RightChild;    ret:=0;    if (l<=cur^.Left) and (cur^.Right<=r)     then ret:=cur^.Sum    else begin               if l<=(cur^.Left+cur^.Right) div 2               then ret:=ret+querysum(LC,l,r);               if r>(cur^.Left+cur^.Right) div 2                then ret:=ret+querysum(RC,l,r);           end;    querysum:=ret;end;

10.对区间修改(统一增加一个数)
procedure update(cur:Node;l,r,delta:integer);var   LC,RC:Node;   //需要给每个区间增加一个变量deltabegin    LC:=cur^.LeftChild;   RC:=cur^.RightChild;    if (l<=cur^.Left) and (cur^.Right<=r)     then cur^.Delta:=cur^.Delta+delta //改变该区间的delta,注意sum    else begin                if l<=(cur^.Left+cur^.Right) div 2                 then update(LC,l,r,delta);                if r>(cur^.Left+cur^.Right) div 2                 then update(RC,l,r,delta);                cur^.Sum:=LC^.Sum+LC^.Delta*(LC^.Right-LC^.Left+1);                cur^.Sum:=cur^.Sum+RC^.Sum+   //上推                                  RC^.Delta*(RC^.Right-RC^.Left+1);            end;end;

11.对区间的查询
function querysum(cur:Node;l,r:integer):integer;var  LC,RC:Node;  ret:integer;begin    LC:=cur^.LeftChild;    RC:=cur^.RightChild;   ret:=0;    if (l<=cur^.Left) and (cur^.Right<=r)     then ret:=ret+cur^.Sum+(cur^.Right-cur^.Left+1)*cur^.Delta    else begin  //将该区间的delta下传 给左右区间,同时修改sum的值                cur^.sum:=cur^.sum+(cur^.right-cur^.left+1)*cur^.delta;                LC^.Delta:=LC^.Delta+cur^.Delta;                RC^.Delta:=RC^.Delta+cur^.Delta;                cur^.Delta:=0;                if l<=(cur^.Left+cur^.Right) div 2                then ret:=ret+querysum(LC,l,r);                if r>(cur^.Left+cur^.Right) div 2                 then ret:=ret+querysum(RC,l,r);            end;    querysum:=ret;end;

12.对区间的修改(设置为同一个数)
procedure update(cur:Node;l,r,num:integer);var  LC,RC:Node;begin    LC:=cur^.LeftChild;  RC:=cur^.RightChild;    if cur^.En   //如果该区间已经是同一个值,将值下传给左右区间    then begin                cur^.sum:=(cur^.right-cur^.left+1)*cur^.data;                if LC<>nil then begin LC^.Data:=cur^.Data; LC^.En:=true; end;                if RC<>nil then begin RC^.Data:=cur^.Data; RC^.En:=true; end;                cur^.En:=false;            end;    if (l<=cur^.Left) and (cur^.Right<=r)     then begin   cur^.En:=true;   cur^.Data:=num;   end    else begin                if l<=(cur^.Left+cur^.Right) div 2 then update(LC,l,r,num);                if r>(cur^.Left+cur^.Right) div 2 then update(RC,l,r,num);                cur^.Sum:=calcsum(LC)+calcsum(RC);           end;end;

13.对区间查询
function querysum(cur:Node;l,r:integer):integer;var  LC,RC:Node;  ret:integer;begin    LC:=cur^.LeftChild;  RC:=cur^.RightChild;  ret:=0;    if (l<=cur^.Left) and (cur^.Right<=r)    then ret:=ret+calcsum(cur)    else begin                if cur^.En  //如果该区间是同一个数,将值下传给左右区间                then begin                            LC^.Data:=cur^.Data; LC^.En:=true;                            RC^.Data:=cur^.Data; RC^.En:=true;                            cur^.En:=false;                        end;                if l<=(cur^.Left+cur^.Right) div 2                then ret:=ret+querysum(LC,l,r);                if r>(cur^.Left+cur^.Right) div 2                then ret:=ret+querysum(RC,l,r);            end;    querysum:=ret;end;

14.线段树的维护方法
线段树上的参数通常有两种维护方法:
(1)一类参数表达了结点的性质,通常具有树型的递推性质,可以从下向进行递计算;(如sum,max,min)
(2)一类参数表达了子树的性质,维护的时候可以先打上标记,在需要进一步访问其子结点的时候从上向递。(如delta,en)
线段树上维护或者询问过程的一般过程:
对于当前区间[l,r]
if 达到某种边界条件(如叶结点或被整个区间完全包含)then
    对维护或是询问作相应处理
else
    将第二类标记传递下去(注意,在查询过程中也要处理)
    根据区间的关系,对两个孩子结点递归处理
    利用递推关系,根据孩子结点的情况维护第一类信息
M,N的规模
一般的模拟
线段树
M=100000,N=5000
2.06秒
0. 57秒
M=100000,N=10000
4.14秒
0. 61秒
M=100000,N=50000
21.32秒
1.21秒
M=100000,N=100000
43.36秒
2.03秒








0 0
原创粉丝点击