线段树讲义

线段树讲义

例题:在自然数,且所有的数不大于30000的范围内讨论一个问题:现在已知n条线段,把端点依次输入告诉你,然后有m个询问,每个询问输入一个点,要求这个点在多少条线段上出现过(0<m,n<30,000)。

 

最基本的解法当然就是读一个点,就把所有线段比一下,看看在不在线段中;

每次询问都要把n条线段查一次,那么m次询问,就要运算m*n次,复杂度就是O(m*n)

这道题m和n都是30000,那么计算量达到了10^9;而计算机1秒的计算量大约是10^8的数量级,所以这种方法无论怎么优化都是超时

 

 

   那么有没有什么算法可以完成这个任务呢?——没错,就是线段树。

 

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。

使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,因此有时需要离散化让空间压缩。所以,用线段树可以在O(m*logn)的时间内完成这道题目,大概是10^5的数量级,可以承受。

那么线段树到底怎么用呢?

 

 

线段树是建立在线段的基础上,每个结点都代表了一条线段[a,b]。长度为1的线段称为元线段。非元线段都有两个子结点,左结点代表的线段为[a,(a + b) / 2],右结点代表的线段为[((a + b) / 2)+1,b]。

下图就是一棵长度范围为[1,5]的线段树。

                                     

 

下面以查找区间内的最小值为例,开始介绍线段树。

-------------------------------------------------------------------------------------------------

 给出一个有n个元素的数组A[1..n],你的任务是设计一个数据结构,支持一下两种操作:

 ●  update(x,v): 把A[x]修改成v;

 ●  query(L,R): 计算min{A[L],A[L+1]…,A[R]}。

(数据范围当然不是暴力能够解决的。)

 

在查询时,我们从根节点开始自顶向下找到待查询线段的左边界和右边界,则“夹在中间”的所有叶子节点不重复不遗漏地覆盖了整个待查询线段。(如查询【2,5】)

 

[1 2 3 4 56 7 8]

/           \

[1 2 3 4]        [5 6 7 8]

/     \         /     \

[1 2]     [3 4]     [5 6]     [7 8]

/ \      /    \    /   \    /  \

1   2   3   4    5   6    7    8

 

    从图中不难发现,树的左右各有一条“主线”,虽有分叉,但每层最多只有两个结点继续向下延伸(整棵树的左右子树各一个)。如上图所示[2,5]=[2]+[3,4]+[5]。在后文中,凡是遇到这样的区间分解,就把分解的区间叫做边界区间,因为它们对应与分解过程的递归边界。

如何更新线段树呢?update(x,v)显然需要更新[x]对应的结点,然后还要更新他的所有祖先结点。

下面给出这两个过程的代码(C版的,但pascal应该看得懂)o是当前结点编号,L,R是当前结点的左右端点。查询时,全局变量ql,qr代表查询区间的左右端点,修改时p,v分别代表修改点位置和修改后数值。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int ql,qr; 
int query( int o, int L, int R){   
   int M=L+(R-L)/2 , ans=INF;     
                                    
   if (ql<=L && R<=qr) return minv[o];      
   if (ql<=M) ans=min(ans,query(o*2,L,M));   
   if (M<qr) ans=min(ans,query(o*2+1,M+1,R)); 
   return ans;        
int p,v; 
void update( int o, int L, int R){      
   int M=L+(R-L)/2; 
   if (L==R) minv[o]=v; 
   else
     if (p<=M) update(o*2,L,M); else update(o*2+1,M+1,R); 
     minv[o]=min(minv[o*2],minv[o*2+1]); 
  
}

 

现在大家对线段树应该有了初步的理解,但是仅仅有点的修改和最小值的查询这两个操作,能做的事还是很少的,我们再来看一下下面的两个操作:

  • add(L,R,v): 把A[L..R]的值全部增加v;

  • query(L,R): 计算子序列A[L..R]的元素和、最小值、最大值。

这里要维护sum,min,max3个值,而且对应的add是修改区间,而不是前面讲过的点修改。点修改只会影响log(n)个结点,但是区间修改在最坏情况下会修改所有结点,那样就和朴素算法没有区别了,应该怎么办呢?

前面讲区间查询时有一个结论:任意区间都能分解成不超过2h个不相交区间的并(h是最大层的编号)。像上个图中[2,5]被分解成[2,2],[3,4],[5,5],所以只要对这三个区间进行add操作就可以了。用sumv[o],minv[o],maxv[o]表示在o结点对应的区间中的和、最大值、最小值,代码如下:

 

1
2
3
4
5
6
7
8
9
10
11
//维护结点o,对应区间为[L,R] 
void maintain( int o, int L, int R){ 
   int lc=o*2,rc=o*2+1; 
   if (R>L){  //维护到叶子结点什么的就挂了 
     sumv[o]=sumv[lc]+sumv[rc]; 
     minv[o]=min(minv[lc],minv[rc]); 
     maxv[o]=max(maxv[lc],maxv[rc]); 
  
   minv[o]+=addv[o]; maxv[o]+=addv[o];  
sumv[o]+=addv[o]*(R-L+1); 
}

 

如何在执行add时用上述代码维护线段树呢?只要在add递归返回之前维护对应的结点好了。y1,y2为add的左右区间,代码如下:

 

1
2
3
4
5
6
7
8
9
10
11
void update( int o, int L, int R){ 
   int lc=o*2,rc=o*2+1; 
   if (y1<=L && y2>=R){ 
     addv[o]+=v; 
   } else
     int M=L+(R-L)/2; 
     if (y1<=M) update(lc,L,M); 
     if (y2>M)  update(rc,M+1,R); 
  
   maintain(o,L,R); 
}

 

构造好了这棵线段树以后,应该如何查询呢?基本的思路和上面的查询是一样的,也是把查询区间递归分解成若干个不相交的子区间,把各个区间的结果加以合并,但是每个结点的结果不能直接用,因为祖先上的add操作是会影响下面所有结点的。我们用add代表当前区间所有祖先结点的add之和,代码如下:

 

1
2
3
4
5
6
7
8
9
10
11
12
int _min,_max,_sum; //保存最小值、最大值、和的全局变量 
void query( int o, int L, int R, int add){ 
   if (y1<=L && y2>=R){ 
     _sum+=sumv[o]+add*(R-L+1); 
     _min=min(_min,minv[o]+add); 
     _max=max(_max,maxv[o]+add); 
   } else
     int M=L+(R-L)/2; 
     if (y1<=M) query(o*2,L,M,add+addv[o]); 
     if (Y2>M)  query(o*2+1,M+1,R,add+addv[o]); 
  
}

 

怎么样,是不是比点修改复杂多了?还有更复杂的。

-----------------------------------------------------------------

   快速序列操作II:给出一个有n个元素的数组A[1..n],你的任务是设计一个数据结构,支持以下两种操作:

  • set(L,R,v):把A[L..R]的值全部修改为v(v>=0);

  • query(L,R):计算自序列A[L..R]的元素和、最小值、最大值。

有了上面两题的基础,应该不难想到把set操作也进行类似的分解,但有一个新问题,

即add的操作时间顺序不改变结果,但set会。比如先执行add(1,4,1)再执行add(2,3,2)和交换顺序等价,但是先执行set(1,4,1)和先执行set(2,3,2)是不等价的,怎么办呢?

完整的解决方案有些复杂,但大体思路是清晰的:在执行新的set操作时,把原来

set“推”到下面的两棵子树中去,新的set遇到完全覆盖某个区间的情况时,把旧的set覆盖掉就可以了。

 

1
2
3
4
5
6
7
8
9
10
11
12
void update( int o, int L, int R){ 
   int lc=o*2,rc=o*2+1; 
   if (y1<=L && y2>=R){ 
     setv[o]=v; 
   } else
     pushdown(o); 
     int M=L+(R-L)/2; 
     if (y1<=M) update(lc,L,M); else maintain(lc,L,M); 
     if (y2>M)  update(rc,M+1,R); else maintain(rc,M+1,R); 
  
   maintain(o,L,R); 
}

 

这里和add不同的是,多了2个maintain语句。因为set时把标记下推了,因此子树也会受到影响,所以无论如何应该更新它。

接下来是pushdown()函数

 

1
2
3
4
5
6
7
void pushdown( int o){ 
   int lc=o*2,rc=o*2+1; 
   if (setv[o]>=0){    //该结点有标记的话 
     setv[lc]=setv[rc]=setv[o]; 
     setv[o]=-1;     //清除标记 
  
}

 

因为pushdown函数是这样实现的,所以会发生子结点的set与父结点的set冲突的情况(考虑一下先执行set(1,3,2),再执行set(1,8,1)会怎样)。所以我们在查询时以祖先的set值为准即可,代码如下:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void query( int o, int L, int R){ 
   if (setv[o]>=0){ 
     _sum+=setv[o]*(min(R,y2)-max(L,y1)+1); 
     _min=min(_min,setv[o]); 
     _max=max(_max,setv[o]); 
   } else if (y1<=L && y2>=R){ 
     _sum+=sumv[o]; 
     _min= min(_min,minv[o]); 
     _max= max(_max,maxv[o]); 
} else
   int M=L+(R-L)/2; 
   if (y1<=M) query(o*2,L,M); 
   if (y2> M) query(o*2,M+1,R); 
  
}

---------------------------------------------------------------

 

以上就是线段树的所有基本操作,但是仅仅掌握这些还是不能做什么题目的,像下面这题:

 

FastMatrix Operations,UVa 11992

                           操作                               备注

1  x1 y1 x2 y2 v  子矩阵(x1,y1,x2,y2)所有元素增加v(v>0)

2  x1 y1 x2 y2     子矩阵(x1,y1,x2,y2)所有元素设为v(v>0)

3  x1 y1 x2 y2      查询(x1,y1,x2,y2)的元素和、最小值、最大值

   子矩阵(x1,x2,y1,y2)是指满足x1≤x≤x2,y1≤y≤y2的所有元素(x,y)。

   第一行为r,c,m(1<m<20,000),r为行数,c为列数,m是操作个数,接下来是r行c列的矩阵和m个操作。矩阵不超过20行,元素综合不超过10^6。

   之前都只是单纯的add或set,而这题需要同时用到2个操作。直接把代码生搬硬套肯定是不行的,需要进行一些改进,使程序能支持所有的操作。并且这题是矩阵,不是上面一直讲的线段。这些问题都是值得考虑的。

注意:比赛的题目不可能出得这么简单,以上的所有的一切都只是基础,光有基础是远远不够的,每个好的题目都有特色的地方,要真正地掌握线段树,还是需要很多时间的努力的。

 

接下来推荐几道线段树的练习题:

 

● wikioi 1080-1082(线段树练习I II III):比较基础性的题目。

● wikioi 1217(借教室 noip2012):挺简单,但当时我不会线段树= =。

● tyvj 2042(线段问题):基础题。

更多的题目可以在各种OJ的分类中找到,这里以基础题为主,就不列出来了。

 

代码综合。

------------------------------------------------------------------------------------------------

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
int ql,qr,v; //查询的左端点和右端点,修改参数
int _min,_max,_sum; //保存最小值、最大值、和的全局变量 
        
void update( int o, int L, int R); //将[ql,qr]增加v
void query( int o, int L, int R, int add); //查询[ql,qr],保存相关值至全局变量
void maintain( int o, int L, int R); //维护结点o,对应区间为[L,R] 
void set_update( int o, int L, int R); //设置[ql,qr]为v
void pushdown( int o); //下推set标记
void set_query( int o, int L, int R); //set版的查询
        
        
void update( int o, int L, int R){ 
   int lc=o*2,rc=o*2+1; 
   if (y1<=L && y2>=R){ 
     addv[o]+=v; 
   } else
     int M=L+(R-L)/2; 
     if (y1<=M) update(lc,L,M); 
     if (y2>M)  update(rc,M+1,R); 
  
   maintain(o,L,R); 
}
        
void query( int o, int L, int R, int add){ 
   if (y1<=L && y2>=R){ 
     _sum+=sumv[o]+add*(R-L+1); 
     _min=min(_min,minv[o]+add); 
     _max=max(_max,maxv[o]+add); 
   } else
     int M=L+(R-L)/2; 
     if (y1<=M) query(o*2,L,M,add+addv[o]); 
     if (Y2>M)  query(o*2+1,M+1,R,add+addv[o]); 
  
}
        
        
void maintain( int o, int L, int R){ 
   int lc=o*2,rc=o*2+1; 
   if (R>L){  //维护到叶子结点什么的就挂了 
     sumv[o]=sumv[lc]+sumv[rc]; 
     minv[o]=min(minv[lc],minv[rc]); 
     maxv[o]=max(maxv[lc],maxv[rc]); 
  
   minv[o]+=addv[o]; maxv[o]+=addv[o];  
   sumv[o]+=addv[o]*(R-L+1); 
}
        
void set_update( int o, int L, int R){ 
   int lc=o*2,rc=o*2+1; 
   if (y1<=L && y2>=R){ 
     setv[o]=v; 
   } else
     pushdown(o); 
     int M=L+(R-L)/2; 
     if (y1<=M) set_update(lc,L,M); else maintain(lc,L,M); 
     if (y2>M)  set_update(rc,M+1,R); else maintain(rc,M+1,R); 
  
   maintain(o,L,R); 
}
        
void pushdown( int o){ 
   int lc=o*2,rc=o*2+1; 
   if (setv[o]>=0){    //该结点有标记的话 
     setv[lc]=setv[rc]=setv[o]; 
     setv[o]=-1;     //清除标记 
  
}
        
void set_query( int o, int L, int R){ 
   if (setv[o]>=0){ 
     _sum+=setv[o]*(min(R,y2)-max(L,y1)+1); 
     _min=min(_min,setv[o]); 
     _max=max(_max,setv[o]); 
   } else if (y1<=L && y2>=R){ 
     _sum+=sumv[o]; 
     _min= min(_min,minv[o]); 
     _max= max(_max,maxv[o]); 
} else
   int M=L+(R-L)/2; 
   if (y1<=M) set_query(o*2,L,M); 
   if (y2> M) set_query(o*2,M+1,R); 
  
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值