A simple introduction to "Segment tree beats"

Revision en11, by jiry_2, 2018-01-27 10:12:20

Hi, I’d like to introduce a simple trick about segment tree in this blog as I promised in this comment. Sorry for the long delay, as a sophomore in Peking University, I've just finished a tired semester and a painful final exam. And now I finally have enough time to do a simple introduction to this interesting algorithm.

It may be a huge project for me since my English is not good. I think I will finish this blog in several steps and I will try to finish it as soon as possible :)

In China, all of the 15 candidates for the Chinese National Team are asked to write a simple research report about algorithms in informatics Olympiad, and the score will be counted in the final selection. There are many interesting ideas and algorithms in these reports. And I find that some of them are quite new for competitors in CF although they are well known in China from the final standings of some recent contests. For example, In the last contest CF Round #458, the problem G can be easily solved using "power series beats" (Thanks for matthew99 to give this name :)) in O(2nn2) which was imported in China by vfleaking in 2015.

This blog is about my report which is written about two years ago. I am satisfied with this work although there is a flaw about time complexity. xyz111 gave me a lot of inspiration, and the name “Segment tree beats” is given by C_SUNSHINE which is from a famous Japanese anime “Angel Beats”.

Here is the link about the Chinese version of this report.

Part1. What it can do.

This work has two parts:

  1. Transform interval max/min (for ) operations into interval add/minus operations in O(1) or
  2. Transform history max/min/sum queries into interval max/min operations in O(1).

Here are some sample problems I used in my report. At first you have a array A of length n and two auxiliary arrays B, C. Initially, B is equal to A and C is all zero.

Task 1. There are two kinds of operations:

  1. For all , change Ai to max(Ai, x)
  2. Query for the sum of Ai in [l, r]

It can be solved in

Task 2. We can add more operations to Task 1:

  1. For all , change Ai to max(Ai, x)
  2. For all , change Ai to min(Ai, x)
  3. For all , change Ai to Ai + x, x can be a negative number
  4. Query for the sum of Ai in [l, r]

It can be solved in and I could not proved the exact time complexity yet, maybe It is still ;)

Task 3. And we can query for some other things:

  1. For all , change Ai to max(Ai, x)
  2. For all , change Ai to min(Ai, x)
  3. For all , change Ai to Ai + x, x can be a negative number
  4. Query for the sum of Bi in [l, r]

After each operation, for each i, if Ai changed in this operation, add 1 to Bi.

It’s time complexity is the same as Task 2.

Task 4. We can also deal with several arrays synchronously. Assume there are two arrays A1 and A2 of length n.

  1. For all , change Aa, i to min(Aa, i, x)
  2. For all , change Aa, i to Aa, i + x, x can be a negative number
  3. Query for the max A1, i + A2, i in [l, r]

It can be solved in and if there are k arrays, the time complexity will be raised to .

Task 5. And there are some tasks about historic information.

  1. For all , change Ai to Ai + x, x can be a negative number.
  2. Query for the sum of Bi in [l, r].
  3. Query for the sum of Ci in [l, r]

After each operation, for each i, change Bi to max(Bi, Ai) and add Ai to Ci.

The query for Bi can be solved in and the query for Ci can be solved in .

Task 6. We can even merged the two parts together.

  1. For all , change Ai to max(Ai, x)
  2. For all , change Ai to Ai + x, x can be a negative number.
  3. Query for the sum of Bi in [l, r].

After each operation, for each i, change Bi to min(Bi, Ai).

It can be solved in .

There are 11 sample tasks in my report and here are 6 of them. All of them are interesting and are hard to solve using the traditional techniques such as lazy tags.

Part2. The main idea

Interval min/max operations

To make the description clearer, I think it’s better to introduce an extended segment tree template.

I think most of the competitors’ templates of the lazy tag is like this ([l, r] is the node's interval and [ll, rr] is the operation's interval):

void modify(int node, int l, int r, int ll, int rr) {
  if (l > rr || r < ll) return;
  if (l >= ll && r <= rr) {
      puttag(node); return;
  }
  pushdown(node);
  int mid = (l + r) >> 1;
  modify(node * 2, l, mid, ll, rr);
  modify(node * 2 + 1, mid + 1, r, ll ,rr);
  update();
}

The main idea is return whenever we can, put the tag whenever we can:

  1. When the operation's interval and the node's interval are no longer intersected, the information inside this subtree must not be affected. So we can return immediately.
  2. When the node's interval is contained by the operation's interval, all the information inside the subtree will be changed together. So we can put the tag on it and return.

In other words, we can replace the two conditions arbitrarily, i.e., we can extend the template like this:

void modify(int node, int l, int r, int ll, int rr) {
  if (break_condition()) return;
  if (tag_condition()) {
      puttag(node); return;
  }
  pushdown(node);
  int mid = (l + r) >> 1;
  modify(node * 2, l, mid, ll, rr);
  modify(node * 2 + 1, mid + 1, r, ll ,rr);
  update();
}

What's the use of such a modification? In some advanced data structure tasks, it's impossible for us to put tags in such a weak condition l >= ll && r <= rr. But we can put it when the condition is stronger. We can use this template to deal with this kind of tasks but we need to analyze the time complexity carefully.

Simple task: There are three kinds of operations:

  1. For all , change Ai to
  2. For all , change Ai to x
  3. Query for the sum of Ai in [l, r]

It's a classic problem (it's the simple extension of 438D - The Child and Sequence) and the traditional solution is to use balanced tree such as splay/treap to maintain the continuous segments with the same Ai and for operation 2, we find out all the segments with Ai ≥ x and change the value of each one.

But if we use segment tree, we can get a much simpler solution: let break_condition be l > rr || r < ll || max_value[node] < x and let tag_condition be l >= ll && r <= rr and max_value[node] == min_value[node]. And we can find that the time complexity of this segment tree is also .

And now, we can easily describe the main idea of "segment tree beats". For each node, we maintain the maximum value max_value[node] and the strict second maximum value second_value[node].

When we are doing the interval min operation for number x. let break_condition be l > rr || r < ll || max_value[node] <= x and let tag_condition be l >= ll && r <= rr && second_value[node] < x. Under such an condition, after put this tag, all of the maximum values inside this subtree will be changed to x and they are still the maximum values of this subtree.

To make it easier to merge with other operations, we can maintain the values in this way: For each node, we maintain the maximum value inside this subtree and other values separately (the maximum values are the first kind and others are the second). Then, interval max operation will be changed to "add a number to the first kind values in some intervals". Keep the meanings of each kinds of values and the tags, you will find that the processes of pushdown and update will be much clearer.

That's the main idea of the first part of "segment tree beats". It is very simple, right? And it also has a very nice time complexity. (I will give out the proof of the time complexity in the third part of this article).

And let's see the first two sample tasks in part 1.

Task 1. In this task, we can maintain the number of the first kind of values inside each node t[node], and when we put tag "add x to the first kind values in this subtree", the sum will be added by t[node] * x. Then we can easily maintain the information we need.

Task 2. In this task, we can maintain the maximum values/ minimum values and others separately, i.e., there are three kinds of numbers now, and we will use three sets of tags for each kind of values. Also, to deal with the queries of the interval's sum, we need to maintain the numbers of the first kind and the second kind of values. Pay attention to some boundary conditions, if there are just two different values in this subtree, there will be no third kind of values and if all the values are the same, the set of the first kind and the second kind will be the same

Historic information

In this part, we will focus on three special values which I named "historic information":

  1. historic maximal value: after each operation, change Bi to max(Bi, Ai)
  2. historic minimal value: after each operation, change Bi to min(Bi, Ai)
  3. historic sum: after each operation, add Ci by Ai.

You may wonder why we need to consider these values. But in China, there had been several problems about these values before I wrote this report. And maintaining these values is a much harder task than it looks like. Here is a data structure task in Tsinghua University Training 2015:

Sample Task: This is the link of this task. There are five kinds of operations (x is a positive integer.):

  1. For all , change Ai to Ai + x.
  2. For all , change Ai to max(Ai + x, 0).
  3. For all , change Ai to x.
  4. Query for Ai.
  5. Query for Bi.

After each operation, change Bi to max(Bi, Ai).

To solve this task, ordinary segment tree is enough. But It's not easy to deal with the relationship between the lazy tags and the historic information. Since I do not have enough time to write today, I will update the solution later.

We have already get the way to deal with the interval min/max operations. And they are good tools to maintain historic information.

Now, let use consider such kind of problems (Part1. Task 5): interval add/minus, query for the interval sum of historic information. It's quite hard since we can't use the traditional lazy tag technique to solve it. But we can use an auxiliary array Di with the initial value of all zero to do some transformation:

  1. Historic maximal value: let Di = Bi - Ai. Then if we change Ai to Ai + x, Di will be changed to max(Di - x, 0).
  2. Historic minimal value: let Di = Bi - Ai. Then if we change Ai to Ai + x, Di will be changed to min(Di - x, 0).
  3. Historic sum: let Di = Ci - tAi while t is the current operation id. Then if we change Ai to Ai + x, Di will be changed to Di - (t - 1)x.

We can use the technique in the previous part about interval min/max operations to maintain the sum of Di. And then we could get the sum of Bi or Ci.

What's more, since we have already transformed interval min/max operations to interval add/minus operations. So we can also maintain the interval sum of historic information under interval min/max operations (Part1. Task 6). This is a simple expansion and I think it's better to leave it as an exercise.

That's all the main idea of "segment tree beats". And I will add the proof about time complexity in the next update. It's a little hard for me to explain the proof in English so maybe it will take me some time to form words. I think that will be the last update, and I will try to finish it soon :).

I've never thought that this article will be so popular. Thanks a lot for the encouragement and the support!

Tags #data structure

History

 
 
 
 
Revisions
 
 
  Rev. Lang. By When Δ Comment
en21 English jiry_2 2018-01-28 04:43:36 12 Tiny change: 'ven by [usr:xyz111] ' -> 'ven by [user:xyz111] '
en20 English jiry_2 2018-01-27 14:17:29 117
en19 English jiry_2 2018-01-27 14:08:14 1735 Tiny change: '90605.png)\n\nThus, ' -> '90605.png){:height="100px" width="400px"}\n\nThus, '
en18 English jiry_2 2018-01-27 13:40:43 7 Tiny change: ', we will also use the t' -> ', we will still use the t'
en17 English jiry_2 2018-01-27 13:39:20 1607 Tiny change: 'the total number of $\Phi(' -> 'the total increase of $\Phi('
en16 English jiry_2 2018-01-27 12:56:03 365
en15 English jiry_2 2018-01-27 12:43:04 4659 Add the proof for task 1
en14 English jiry_2 2018-01-27 12:35:19 33
en13 English jiry_2 2018-01-27 11:56:33 5 Tiny change: '& r <= rr and max_value' -> '& r <= rr && max_value'
en12 English jiry_2 2018-01-27 10:29:51 49
en11 English jiry_2 2018-01-27 10:12:20 4 Tiny change: 'Historic maximal value' -> 'Historic minimal value'
en10 English jiry_2 2018-01-26 17:01:38 2806
en9 English jiry_2 2018-01-25 17:21:34 136
en8 English jiry_2 2018-01-25 17:17:52 5446 Tiny change: 'affected. \n2. When ' -> 'affected. So we can return immediately.\n2. When '
en7 English jiry_2 2018-01-25 13:07:08 12
en6 English jiry_2 2018-01-24 18:05:58 1 Tiny change: 'rged the to parts to' -> 'rged the two parts to'
en5 English jiry_2 2018-01-24 18:02:56 3 Tiny change: 'in China for the final' -> 'in China from the final'
en4 English jiry_2 2018-01-24 17:40:31 5 Tiny change: '2)$ which is import in China ' -> '2)$ which was imported in China '
en3 English jiry_2 2018-01-24 17:26:29 2 Tiny change: ') in $O(2^n n^2)$ whi' -> ') in $O(2^{n} n^2)$ whi'
en2 English jiry_2 2018-01-24 17:25:28 1 Tiny change: ' in $O(2^nn^2)$ whic' -> ' in $O(2^n n^2)$ whic'
en1 English jiry_2 2018-01-24 17:24:51 5058 Initial revision (published)