Dan4Life's blog

By Dan4Life, 2 months ago, In English

Hi Codeforces!

This is my first attempt at a tutorial blog XD.

Disclaimer: No headaches occured during the making of this blog

Problem statement

Given an integer array $$$A$$$ of length $$$n$$$ and $$$q$$$ of the following 5 types of queries:

  1. For all $$$i \in [l,r]$$$, change $$$A_i$$$ to $$$\max(A_i, v)$$$
  2. For all $$$i \in [l,r]$$$, change $$$A_i$$$ to $$$\min(A_i, v)$$$
  3. For all $$$i \in [l,r]$$$, change $$$A_i$$$ to $$$A_i + v$$$
  4. For all $$$i \in [l,r]$$$, change $$$A_i$$$ to $$$v$$$
  5. Output $$$A_i$$$
  • $$$ 1 \leq n,q \leq 10^6 $$$
  • $$$ 1 \leq l \leq r \leq n $$$
  • $$$ -10^9 \leq A_i, v \leq 10^9 $$$

The above is solvable with techniques like Segment Tree Beats in $$$\mathcal{O}(n+q\log^2{n})$$$, but as a binary search enthusiast, you didn't learn segment tree beats?

Don't worry, here's an easier, faster, and shorter solution that solves this particular problem in $$$\mathcal{O}(n+q\log{n})$$$

Prerequisites

You should know the following to solve the full problem:

  1. Basics of functions (including composite functions)
  2. Associative, commutative, and distributive properties of $$$\max$$$ / $$$\min$$$
  3. Basics of a lazy propagation segment tree (not required for some subproblems)

Assuming you know the above, let's begin! But first, I'll relax the constraints a bit...

Subproblem 1

These are the queries:

  1. For all $$$i \in \textbf{[1,n]}$$$, change $$$A_i$$$ to $$$\max(A_i, v)$$$
  2. For all $$$i \in \textbf{[1,n]}$$$, change $$$A_i$$$ to $$$\min(A_i, v)$$$

Print the final array $$$A$$$ $$$\newline$$$

Example input 1

We'll use the example input to try coming up with a solution to this problem.

Let $$$f_i(x)$$$ = output after applying only $$$i$$$th query to integer $$$x$$$

Let's define a composite function $$$F(x)$$$ = $$$f_Q(f_{Q-1}(\dots f_2(f_1(x))\dots))$$$

We basically want to output $$$F(x)$$$ for all $$$x \in A$$$

We can try plotting the graph of $$$F(x)$$$ against $$$x$$$ to hopefully find a pattern.

y = F(x)

Some observations about the above graph.

  • $$$F(x)$$$ is monotonically non-decreasing i.e. $$$F(x) \leq F(x+1)$$$
And why is that?
  • The graph can be divided into 3 sections depending on $$$x$$$:
$$$ \displaystyle y = F(x) = \begin{cases} 3 &\quad\text{for } x < 3 \\ x &\quad\text{for } 3 \leq x \leq 8 \\ 8 &\quad\text{for } 8 < x \\ \end{cases} $$$

In other words, $$$F(x) = \min(8,\max(x,3))$$$

In fact, it can be proven that $$$F(x) = \min(a,\max(x,b))$$$ for constants $$$a$$$, $$$b$$$ in the general case.

Mathy proof
Intuitive proof

From the mathy proof, we can repeatedly spam $$$f_{i+1}(f_i(x))$$$ till we get $$$F(x)$$$.

Our starting $$$ f_0(x) = \min( \infty , \max( x, -\infty ) ) $$$ as this does not affect $$$x$$$

Now, that we have calculated our $$$F(x)$$$ in $$$\mathcal{O}(Q)$$$ , we can use it to calculate our final array in $$$\mathcal{O}(1)$$$ per $$$x$$$.

Subproblem 1 Code (Mathy version)

From the intuitive proof, we can create this solution:

Subproblem 1 Code (Intuitive version)

Bonus: Solve this subproblem using $$$\max(a,\min(x,b))$$$ instead.

Ok, that was easy enough. Let's make it a bit harder.

Subproblem 2

These are the queries:

  1. For all $$$i \in [l,r]$$$, change $$$A_i$$$ to $$$\max(A_i, v)$$$
  2. For all $$$i \in [l,r]$$$, change $$$A_i$$$ to $$$\min(A_i, v)$$$
  3. Output $$$A_i$$$ $$$\newline$$$
Example input 2

In the first subproblem (mathy solution), we applied $$$f_i(x)$$$ to the entire array for each $$$i \in [1,q]$$$, but now we want to apply $$$f_i(x)$$$ only to $$$l_i, r_i$$$ for each $$$i \in [1,q]$$$. Doing this naively will take $$$\mathcal{O}(nq)$$$ time which is too slow.

We can speed this up with lazy segment trees instead. The idea is to only apply $$$f_i(x)$$$ to the $$$\mathcal{O}(\log n)$$$ segment tree nodes and lazily push down these functions from a segtree node to its children only when necessary (to make sure we apply the functions in the correct order).

Each node in the segment tree will contain the pair of integers $$$[a,b]$$$ which determines the function that is currently applied to that node (and eventually, all its decendants). Initially, each node's $$$[a,b]$$$ represents $$$f_0(x)$$$ and hence is equal to $$$[\infty,-\infty]$$$.

We apply to the $$$\mathcal{O}(\log n)$$$ nodes in the range $$$l_i, r_i$$$ for each query. Along the route to the target nodes, we push down the current node's $$$[a,b]$$$ to both children(if it's not $$$f_0(x)$$$) (to preserve order). $$$\newline$$$

Subproblem 2 Code

Bonus: Solve this subproblem using the intuitive solution instead.

Ok, now we're getting somewhere. It's still not hard enough though...

Subproblem 3

These are the queries:

  1. For all $$$i \in \textbf{[1,n]}$$$, change $$$A_i$$$ to $$$\max(A_i, v)$$$
  2. For all $$$i \in \textbf{[1,n]}$$$, change $$$A_i$$$ to $$$\min(A_i, v)$$$
  3. For all $$$i \in \textbf{[1,n]}$$$, change $$$A_i$$$ to $$$A_i + v$$$

Print the final array $$$A$$$ $$$\newline$$$

Example input 3

The example input3 is exactly the same as the example input1, but I added a couple of type 3 queries. We'll use example input3 to observe a pattern. On to plotting...

y = F(x)

It seems both graphs have the same structure. Let's compare both of them.

Graph with type 3 queries vs Same graph without type 3 queries

The new graph has the same 3-part structure as the old one, but with a horizontal translation by some $$$c$$$ for some new constants $$$a,b,c$$$

In other words:

$$$\displaystyle F(x) = \min(a,\max(b,x+c))$$$ for constants $$$a, b, c$$$ in the general case.

Proof is quite similar to the first subproblem.

Mathy proof
Intuitive proof

Our starting $$$ f_0(x) = \min( \infty , \max(-\infty, x+0) ) $$$ as this does not affect $$$x$$$

Subproblem 3 Code (Mathy version)

From the intuitive proof, we can create this solution:

Subproblem 3 Code (Intuitive version)

Bonus: We could also interpret the new graph as a vertical translation. Solve this subproblem using $$$\max(a,\min(x,b))+c$$$ instead.

Original problem

Armed with the ability to solve subproblems 1, 2, and 3, you should now be able to solve the original problem. Try solving it first : D

Full problem statement

How to solve? It's just subproblem 2+3 with an extra step

Handle range queries?
Handle Type 4 queries?
Final Code

Bonus: Solve this problem using the intuitive solution instead.

Final time complexity: $$$\mathcal{O}(n+q\log{n})$$$

Practice problems

I'll update this list if you find any more problems

  1. IOI 2014 Wall
  2. ABC 196 E Filters
  3. IOI 2021 Candies (Subtask 3)

Conclusion

Thanks to madlogic for proofreading this blog and useful suggestions!

If you found this trick useful, you might as well upvote :)

In the case of any bugs or questions, feel free to let me know.

  • Vote: I like it
  • +100
  • Vote: I do not like it

»
2 months ago, # |
  Vote: I like it +13 Vote: I do not like it

DAN4LIFE ORZ!!!!

»
2 months ago, # |
  Vote: I like it +13 Vote: I do not like it

Can you show the details for "Handle range queries?". As far as I can see it won't work and saying that this can replace seg tree beats for this type of problem is a sort of a big claim imo.

  • »
    »
    2 months ago, # ^ |
      Vote: I like it 0 Vote: I do not like it

    Didn't say it works for range queries, just pointed out that segtree beats can solve harder variations and is overkill in this case

»
2 months ago, # |
  Vote: I like it +47 Vote: I do not like it

Yes, lazy propagation can do many types of range-range operations — as long as you can combine updates into $$$O(1)$$$ info in a node.

But please remove the segment tree beats from the title. You're wasting time of people who think that you're proposing its easier replacement. It doesn't help that the following sentence doesn't clearly say if you can do the sum:

The above (and harder variations e.g. range sum queries) is solvable with techniques like Segment Tree Beats in O(n+qlog2n), but as a binary search enthusiast, you didn't learn segment tree beats?

»
2 months ago, # |
  Vote: I like it +11 Vote: I do not like it

The idea is not related to chmin, cmax, add, and assign per se. It's just a general idea: if there is some constant $$$k$$$ such that any sequence of $$$k+1$$$ updates to a sequence of numbers can be shrunk to a sequence of $$$k$$$ updates, we can use lazy segment trees to store these updates. Then, if we only care about point queries, we can already answer them. However, if we want to answer queries of calculating some function $$$F$$$ on a segment, we also need to be able to quickly (ideally, in constant time) recalculate the value of $$$F$$$ on a segment after the application of a single update to all the elements of this segment. Hence, for example, if our query is "sum all the elements on a segment", segment trees are good to go with updates like add and assign and fail with updates like chmin and chmax. On the other hands, if our query is "find minimum on a segment", there is no trouble to add chmin and chmax updates.