Professional Documents
Culture Documents
A tree of segments is a data structure that allows one to effectively (ie, asymptotically
), implement the following operations: finding the sum / minimum of array elements
in a given interval ( , where and go to the input of the algorithm), in addition, it is
possible to change the elements of the array: both changing the value of one element, and
changing the elements on the whole subsegment of the array (that is, it is allowed to assign to
all elements any value, or add to any element of the array a number).
In general, the tree of segments is a very flexible structure, and the number of problems solved
by it is theoretically unlimited. In addition to the above types of operations with tree segments, it
is also possible and much more complex operations (see section "Advanced versions of the tree
segments"). In particular, the tree of segments can be easily generalized to large dimensions:
for example, to solve the problem of finding the sum / minimum in some sub-rectangle of the
Building
The process of constructing a tree of segments by a given array can be done effectively as
follows, from the bottom up: first write the values of the elements in the corresponding
leaves of the tree, then on the basis of them we calculate the values for the vertices of the
previous level as the sum of the values in the two leaves, then we calculate the values for one
more level in a similar way, etc. It is convenient to describe this operation recursively: we start
the construction procedure from the root of the tree of segments, and the construction
procedure, if called not from the sheet, calls itself from each of the two sons and summarizes
the calculated values, and if it was called from the sheet, it simply writes to the value of this
element of the array.
The asymptotics of constructing a tree of segments is, therefore, .
Implementation
The main realization moment is how to store the tree of segments in memory. For the sake of
simplicity, we will not store the tree explicitly, but use this trick: let's say that the root of the tree
has a number , his sons - numbers and , their sons - numbers with by , and so on.
It is easy to understand the correctness of the following formula: if the vertex has number ,
then let her left son - it's the top with the number , and the right one - with the number
.
This technique greatly simplifies the programming of the tree of segments, - now we do not
need to store the structure of the tree of segments in memory, but only to create an array for the
sums on each segment of the tree of segments.
It should be noted only that the size of this array with such a numbering should be set not ,
a . The point is that such a numbering does not work ideally in the case when is not a
power of two - then there are missing numbers that do not correspond to any vertices of the tree
(in fact, the numbering behaves like if rounded up to the nearest degree of deuces). This
does not create any complications in the implementation, but leads to the fact that the size of
the array should be increased to .
So, we store the tree of segments simply as an array , the size is four times the size
input data:
int n, t [ 4 * MAXN ] ;
Procedure for constructing a tree of segments by a given array looks like this: it is a
recursive function, it is passed to the array itself , number current top of the tree, and the
border and a segment corresponding to the current vertex of the tree. From the main
program, call this function with parameters , , .
void build ( int a [ ] , int v, int tl, int tr ) {
if ( tl == tr )
t [ v ] = a [ tl ] ;
else {
int tm = ( tl + tr ) / 2 ;
build ( a, v * 2 , tl, tm ) ;
build ( a, v * 2 + 1 , tm + 1 , tr ) ;
t [ v ] = t [ v * 2 ] + t [ v * 2 + 1 ] ;
}
}
Next, the function for querying a sum is also a recursive function, in which the information
about the current top of the tree (i.e., the number , , , which in the main program
should be passed values , , respectively), and besides this there are also borders
and the current request. In order to simplify the code, this function always does two recursive
calls, even if in fact one is needed - just a superfluous recursive call will be sent a request,
which , which is easily cut off by an additional check at the very beginning of the
function.
int sum ( int v, int tl, int tr, int l, int r ) {
if ( l > r )
return 0 ;
if ( l == tl && r == tr )
return t [ v ] ;
int tm = ( tl + tr ) / 2 ;
return sum ( v * 2 , tl, tm , l, min ( r, tm ) )
+ sum ( v * 2 + 1 , tm + 1 , tr, max ( l, tm + 1 ) , r ) ;
}
Finally, the modification request . It is also transmitted information about the current top of the
tree of segments, and additionally indicates the index of the changing element, as well as its
new value.
void update ( int v, int tl, int tr, int pos, int new_val ) {
if ( tl == tr )
t [ v ] = new_val ;
else {
int tm = ( tl + tr ) / 2 ;
if ( pos <= tm )
update ( v * 2 , tl, tm , pos, new_val ) ;
else
update ( v * 2 + 1 , tm + 1 , tr, pos, new_val ) ;
t [ v ] = t [ v * 2 ] + t [ v * 2 + 1 ] ;
}
}
It is worth noting that the function It is easy to make non-recursive, since the
recursion in it is a tail recursion. branching never happens: one call can spawn only one
recursive call. With non-recursive implementation, the speed of work can grow several times.
From other optimizations it is worth mentioning that multiplication and division by two is
replaced by bit operations - it also slightly improves the performance of the tree segments.
Search for the minimum / maximum and the number of times it occurs
The task is similar to the previous one, only now in addition to the maximum it is also required to
return the number of its occurrences. This problem arises in a natural way, for example, when
solving with the help of a tree of segments of such a problem: to find the number of the longest
growing subsequences in a given array.
To solve this problem, at each vertex of the tree of segments we will store a pair of numbers: in
addition to the maximum, the number of its occurrences on the corresponding segment. Then,
when building a tree, we just need to get two pairs for the current vertex by two such pairs,
obtained from the sons of the current vertex.
The combination of two such pairs into one should be singled out as a separate function, since
this operation will have to be performed both in the modification request and in the search for
the maximum.
pair < int , int > t [ 4 * MAXN ] ;
pair < int , int > combine ( pair < int , int > a, pair < int , int > b ) {
if ( a. first > b. first )
return a ;
if ( b. first > a. first )
return b ;
return make_pair ( a. first , a. second + b. second ) ;
}
pair < int , int > get_max ( int v, int tl, int tr, int l, int r ) {
if ( l > r )
return make_pair ( - INF, 0 ) ;
if ( l == tl && r == tr )
return t [ v ] ;
int tm = ( tl + tr ) / 2 ;
return combine (
get_max ( v * 2 , tl, tm , l, min ( r, tm ) ) ,
get_max ( v * 2 + 1 , tm + 1 , tr, max ( l, tm + 1 ) , r )
) ;
}
void update ( int v, int tl, int tr, int pos, int new_val ) {
if ( tl == tr )
t [ v ] = make_pair ( new_val, 1 ) ;
else {
int tm = ( tl + tr ) / 2 ;
if ( pos <= tm )
update ( v * 2 , tl, tm , pos, new_val ) ;
else
update ( v * 2 + 1 , tm + 1 , tr, pos, new_val ) ;
t [ v ] = combine ( t [ v * 2 ] , t [ v * 2 + 1 ] ) ;
}
}
another prefix of the array, but this will lead to a solution in time .
Instead, you can use the same idea as in the previous paragraph, and look for the desired
position with one descent on the tree: passing each time to the left or right son, depending on
the amount of the amount in the left son. Then the answer to the search query will be one such
descent along the tree, and, therefore, will be performed for .
void update ( int v, int tl, int tr, int pos, int new_val ) {
if ( tl == tr )
t [ v ] = make_data ( new_val ) ;
else {
int tm = ( tl + tr ) / 2 ;
if ( pos <= tm )
update ( v * 2 , tl, tm , pos, new_val ) ;
else
update ( v * 2 + 1 , tm + 1 , tr, pos, new_val ) ;
t [ v ] = combine ( t [ v * 2 ] , t [ v * 2 + 1 ] ) ;
}
}
It remains to understand the answer to the query. To do this, we, just as before, go down the
tree, thus breaking the length of the query into several sub-sub-sections that coincide
with segments of the tree of segments, and combine the answers in them into a single answer
to the whole problem. Then it is clear that the work does not differ from the work of the usual
tree of segments, but instead of simply summing / minimum / maximum values, use the function
. The following implementation is slightly different from the implementation of the
query : it does not allow cases when the left border request exceeds the right border
(otherwise there will be unpleasant incidents - which structure to return when the query
section is empty? ..).
data query ( int v, int tl, int tr, int l, int r ) {
if ( l == tl && tr == r )
return t [ v ] ;
int tm = ( tl + tr ) / 2 ;
if ( r <= tm )
return query ( v * 2 , tl, tm , l, r ) ;
if ( l > tm )
return query ( v * 2 + 1 , tm + 1 , tr, l, r ) ;
return combine (
query ( v * 2 , tl, tm , l, tm ) ,
query ( v * 2 + 1 , tm + 1 , tr, tm + 1 , r )
) ;
}
Search for the smallest number, greater than or equal to the specified, in the specified
interval. No modification requests
It is required to respond to requests of the following type: , which means finding the
minimum number in the interval , which is greater than or equal to .
We construct a tree of segments in which at each vertex we store a sorted list of all numbers
occurring on the corresponding segment. For example, the root will contain an array in
sorted form. How to build such a tree segments as effectively as possible? To do this, we
approach the problem, as usual, from the point of view of recursion: let these lists have already
been constructed for the left and right sons of the current vertex, and we need to build this list
for the current vertex. With such a statement of the question, it becomes almost obvious that
this can be done in linear time: we just need to combine the two sorted lists into one, which is
done by one pass through them with two pointers. Users of C ++ are even easier, because this
merge algorithm is already included in the standard STL library:
vector < int > t [ 4 * MAXN ] ;
is processed in time .
int query ( int v, int tl, int tr, int l, int r, int x ) {
if ( l > r )
return INF ;
if ( l == tl && tr == r ) {
vector < int > :: iterator pos = lower_bound ( t [ v ] . begin ( ) , t [ v ] . end ( ) , x ) ;
if ( pos ! = t [ v ] . end ( ) )
return * pos ;
return INF ;
}
int tm = ( tl + tr ) / 2 ;
return min (
query ( v * 2 , tl, tm , l, min ( r, tm ) , x ) ,
query ( v * 2 + 1 , tm + 1 , tr, max ( l, tm + 1 ) , r, x )
) ;
}
Constant is equal to some large number, certainly greater than any number in the array. It
carries the meaning of "there is no answer in the given interval".
Search for the smallest number, greater than or equal to the specified, in the specified
interval. Modification requests are allowed
The task is similar to the previous one, only modification requests are now allowed: process the
assignment .
The solution is similar to the solution of the previous problem, but instead of simple lists at each
vertex of the tree of segments, we will store a balanced list that allows you to quickly find the
required number, delete it, and insert a new number. Given that, generally speaking, the
number in the input array can be repeated, the optimal choice is the data structure STL
.
The construction of such a tree of segments occurs approximately the same as in the previous
problem, only now it is necessary to combine not sorted lists, but , which leads to
the fact that the asymptotics of the construction deteriorate to (although, apparently,
red-black trees allow merging two trees in linear time, but the STL library does not guarantee
this).
The answer to the search query is almost equivalent to the above code, only now
should be called on .
Finally, the modification request . To process it, we must go down the tree, making changes to
all lists containing the affected element. We simply delete the old value of this
element (not forgetting that we do not need to delete all the repetitions of this number with it)
and insert its new value.
void update ( int v, int tl, int tr, int pos, int new_val ) {
t [ v ] . erase ( t [ v ] . find ( a [ pos ] ) ) ;
t [ v ] . insert ( new_val ) ;
if ( tl ! = tr ) {
int tm = ( tl + tr ) / 2 ;
if ( pos <= tm )
update ( v * 2 , tl, tm , pos, new_val ) ;
else
update ( v * 2 + 1 , tm + 1 , tr, pos, new_val ) ;
}
else
a [ pos ] = new_val ;
}
Search for the smallest number, greater than or equal to the specified, in the specified
interval. Acceleration with the technique of "partial cascading"
Improve the response time to the search query before the time using the technique
of "fractional cascading" ("fractional cascading").
Partial cascading is a simple technique that allows you to improve the running time of multiple
binary searches, which are conducted at the same value. In fact, the answer to the search query
is that we divide our task into several subtasks, each of which is then solved by binary search
by the number . Partial cascading allows you to replace all of these binary searches with one.
The simplest and most obvious example of partial cascading is the f ollowing problem : there
are several sorted lists of numbers, and we must find in each list the first number greater than or
equal to the given number.
If we solved the problem "on the forehead," we would have to run a binary search for each of
these lists, which, if there are many, becomes a very significant factor: if all the lists , then the
asymptotics will be , where - the total amount of all lists (asymptotic
behavior is because the worst case - when all lists are approximately equal to each other in
length, that are ).
Instead, we could combine all of these lists into one sorted list in which each number will
keep a list of the positions: the position in the first list of the first number is greater than or equal
to a similar position in the second list, and so on. In other words, for every number we keep
occurring at the same time the number of binary search on it in each of the lists. In this case, the
asymptotic behavior of the response to the request is received , it is much
better, but we are forced to pay a large memory consumption: namely, we need
memories.
Equipment partial cascading goes further in this task and achieves the memory consumption
at the same time respond to the request .(To do this, we do not store a
large list of lengths , and come back to the list, but with each list contains every other
element from the following list, we again have with each number to record its position in both
lists (current and next), but it will continue to respond effectively to the request: we do a binary
search on the first list, and then go to these lists in order, moving each time in the following list
to help predposchitannyh pointers, and making one step to the left, considering thus that half chi
sat next list was not taken into account).
But we in our application to the wood pieces do not need the full power of this technique. The
fact that the list of the current top contains all the numbers that can occur in the left and right
sons. Therefore, to avoid a binary search through the list of his son, it is sufficient for each list in
the segments tree count for each of its positions in the list of left and right sons (more precisely,
the position of the first number is less than or equal to the current).
Thus, instead of the usual list of all the numbers we keep a list of triples: the number itself, the
position in the list of the left son, the position in the list of the right son. This will allow us for
to determine the position in the list left or right son, instead of doing a binary list on it.
The easiest way to apply this technique to the problem when the modification request is absent
- then these positions are just numbers, and counting them in the construction of the tree very
easily inside the algorithm merge two sorted sequences.
In the event that allowed modification requests, everything is somewhat more complicated:
these positions are now to be stored in the form of iterators inside , and when you
request an update - the right to decrease / increase for those items for which it is required.
Either way, the task has been reduced to net realizable subtleties, but the main idea - replacing
binary search a binary search through the list at the root of the tree - is described
completely.
void update ( int v, int tl, int tr, int l, int r, int add ) {
if ( l > r )
return ;
if ( l == tl && tr == r )
t [ v ] + = add ;
else {
int tm = ( tl + tr ) / 2 ;
update ( v * 2 , tl, tm , l, min ( r, tm ) , add ) ;
update ( v * 2 + 1 , tm + 1 , tr, max ( l, tm + 1 ) , r, add ) ;
}
}
int get ( int v, int tl, int tr, int pos ) {
if ( tl == tr )
return t [ v ] ;
int tm = ( tl + tr ) / 2 ;
if ( pos <= tm )
return t [ v ] + get ( v * 2 , tl, tm , pos ) ;
else
return t [ v ] + get ( v * 2 + 1 , tm + 1 , tr, pos ) ;
}
void update ( int v, int tl, int tr, int l, int r, int color ) {
if ( l > r )
return ;
if ( l == tl && tr == r )
t [ v ] = color ;
else {
push ( v ) ;
int tm = ( tl + tr ) / 2 ;
update ( v * 2 , tl, tm , l, min ( r, tm ) , color ) ;
update ( v * 2 + 1 , tm + 1 , tr, max ( l, tm + 1 ) , r, color ) ;
}
}
other destinations
There were considered only the basic application segments trees in problems with modifications
in the segment. The remaining tasks are obtained based on the same ideas that are described
here.
It is only important to be very careful when dealing with pending modifications: it must be
remembered that even if the current top we have "pushed" a modification is pending, then the
left and right sons, most likely, have not done so. So it is often necessary is the cause
also of the left and right sons of the current node, or the carefully take into account the pending
modifications in them.
int sum_x ( int vx, int tlx, int trx, int lx, int rx, int ly, int ry ) {
if ( lx > rx )
return 0 ;
if ( lx == tlx && trx == rx )
return sum_y ( vx, 1 , 0 , m - 1 , ly, ry ) ;
int tmx = ( tlx + trx ) / 2 ;
return sum_x ( vx * 2 , tlx, tmx, lx, min ( rx,tmx ) , ly, ry )
+ sum_x ( vx * 2 + 1 , tmx + 1 , trx, max ( lx,tmx + 1 ) , rx, ly, ry ) ;
}
This function works in the time since it first goes down the tree in the first
coordinate, and each traversed the top of the tree - makes a request of a conventional wood
pieces on the second coordinate.
Finally, consider a modification request . We want to learn how to modify the wood segments
in accordance with the change in the value of any element .It is clear that
changes will occur only in those sections of the first tree tops, which are covered with a
coordinate (and there is ), but for tree segments corresponding to it - the changes
will be only in the tops, which are covered with a coordinate (and there will be ).
Therefore, the implementation of the modification request will not be much different from the
one-dimensional case, but now we will first descend in the first coordinate, and then - for the
second.
void update_y ( int vx, int lx, int rx, int vy, int ly, int ry, int x, int y, int new_val ) {
if ( ly == ry ) {
if ( lx == rx )
t [ vx ] [ vy ] = new_val ;
else
t [ vx ] [ vy ] = t [ vx * 2 ] [ vy ] + t [ vx * 2 + 1 ] [ vy ] ;
}
else {
int my = ( ly + ry ) / 2 ;
if ( y <= my )
update_y ( vx, lx, rx, vy * 2 , ly, my, x, y, new_val ) ;
else
update_y ( vx, lx, rx, vy * 2 + 1 , my + 1 , ry, x, y, new_val ) ;
t [ vx ] [ vy ] = t [ vx ] [ vy * 2 ] + t [ vx ] [ vy * 2 + 1 ] ;
}
}
void update_x ( int vx, int lx, int rx, int x, int y, int new_val ) {
if ( lx ! = rx ) {
int mx = ( lx + rx ) / 2 ;
if ( x <= mx )
update_x ( vx * 2 , lx, mx, x, y, new_val ) ;
else
update_x ( vx * 2 + 1 , mx + 1 , rx, x, y, new_val ) ;
}
update_y ( vx, lx, rx, 1 , 0 , m - 1 , x, y, new_val ) ;
}
Let the following problem: there are points in the plane defined by its coordinates ,
and receives requests such as "count the number of points lying in a rectangle
." It is clear that in the case of such a task becomes unnecessarily
wasteful to build a two-dimensional tree lengths from the elements. Most of this memory
will be wasted, because each single point can be reached only in the segments of
wood pieces in the first coordinate, and therefore, the total "useful" the size of all the segments
of the trees along the second coordinate is the value of .
Then proceed as follows: at each vertex of the tree segments in the first coordinate will store
tree sections, built only on the second coordinates, which are found in this segment of the first
coordinates. In other words, the construction of the tree segments in some vertex with the
number and boundaries , we will consider only those points that fall in this
segment , and build a segment tree just above them.
In this way we will achieve that segments each tree along the second coordinate will take
exactly as much memory as it should. As a result, the total amount of memory is reduced to
. Respond to the request , we will continue for , only now the call
request from the wood pieces on the second coordinate, we'll have to do a binary search on the
second coordinate, but it does not worsen the asymptotic behavior.
But payback will be impossible to make an arbitrary modification request : in fact, if a new
point, it will lead to the fact that we will have in any tree lengths of the second coordinate to add
a new element into the middle of that done effectively impossible.
Finally, we note that in this manner the compressed two-dimensional tree segments is
practically equivalent to the above modification segments dimensional tree (cm. "Preservation
of all subarray in each node of the tree segments"). In particular, it turns out that a
two-dimensional tree described here segments - this is just a special case of the conservation of
the subarray in each node of the tree, where he subarray is stored in the form of pieces of
wood. It follows that if you have to abandon the two-dimensional pieces of wood because of the
impossibility of any given request, it makes sense to try to replace the embedded segment tree
to any more powerful data structure, such as a Cartesian tree .
vertex * update ( vertex * t, int tl, int tr, int pos, int new_val ) {
if ( tl == tr )
return new vertex ( new_val ) ;
int tm = ( tl + tr ) / 2 ;
if ( pos <= tm )
return new vertex (
update ( t - > l, tl, tm , pos, new_val ) ,
t - > r
) ;
else
return new vertex (
t - > l,
update ( t - > r, tm + 1 , tr, pos, new_val )
) ;
}
With this approach it is possible to turn into a persistent-data structure of almost any segment
tree.