Segment Tree Beats
Authors: Benjamin Qi, Dustin Miao
Perform chmin and chmax range updates
Prerequisites
Segment Tree Beats
Focus Problem – try your best to solve this problem before continuing!
Solution - The Child and Sequence
First consider a lazy segment tree. A pseudocode for the update function looks something like:
function update(upd_left, upd_right, upd_value, tree_node, tree_left, tree_right) if upd_right < tree_left or tree_right < upd_left return if upd_left ≤ tree_left and tree_right ≤ upd_right apply update return push lazy updates down let tree_mid = (tree_left + tree_right) / 2 let left_child = 2 * tree_node let right_child = 2 * tree_node + 1 update(upd_left, upd_right, upd_value, left_child, tree_left, tree_mid) update(upd_left, upd_right, upd_value, right_child, tree_mid + 1, tree_right) merge values from children
At first, this problem may seem like an ordinary lazy segment tree problem, but the range modulo updates prevent updates from stacking. That is, for a given node, it is difficult to calculate what the sum value of the node will be after an update. Furthermore, in the lazy array, modulo, unlike sum, does not satisfy or any other simple identity. How do we get around this?
As it turns out, we can take advantage of an important property of modulo. Modulo either does not affect a number, or decreases it by at least half of what it was. If the number in question is , and the modulo was by , then this can be proved using casework:
- If , then is unaffected by
- If , then after the modulo operation must be strictly less than
- If , then . This then reduces to the second case.
Let us ignore operations of type 3 for the time being. Because of this property of modulo, an element with value will get decreased at most times (although a greater number of updates may not affect the element). Taking this into account, we can slightly modify the modulo update function to encorporate these optimizations.
function update(upd_left, upd_right, upd_value, tree_node, tree_left, tree_right) let cur_max = the maximum element in [tree_left, tree_right] if upd_right < tree_left or tree_right < upd_left or cur_max < upd_value return if tree_left = tree_right apply update return let tree_mid = (tree_left + tree_right) / 2 let left_child = 2 * tree_node let right_child = 2 * tree_node + 1 update(upd_left, upd_right, upd_value, left_child, tree_left, tree_mid) update(upd_left, upd_right, upd_value, right_child, tree_mid + 1, tree_right) merge values from children
Note: Because we are no longer doing range updates with lazy propagation, there is no need for a lazy tag.
We will store in a separate array as a separate (mergable) value. Although it is possible that a single query processes nodes, over all queries this amortizes to the acceptable time complexity of .
Consider adding in operations of type 3. Although the implementation is relatively straightforward (simply a point update on segment tree), the proof of complexity from the previous section falls apart because elements can be increased back to their maximum value.
Define the entropy of the array to be , or equivalently, the maximum number of modulo operations to decrease the array to its base state of all 0s. Note that each update operation runs in , so if there are no point updates, then the time complexity is . Each point update increases the entropy by a fixed amount . If there are point updates, then the total entropy over all updates is bounded by . If we factor out these point update operations, each modulo update is still bounded by the total entropy. This means that even with point updates, our solution still runs in .
Although strictly speaking, The Child and Sequence is not a segment tree beats problem, the techniques used in it are closely related. In short, segment tree beats is a technique that allows a non-polylogarithmic range update complexity that amortizes to or .
Implementation - The Child and Sequence
C++
#include <bits/stdc++.h>using namespace std;const int MAXN = 100001;int N, Q;long long tsum[MAXN * 4], tmax[MAXN * 4];void update_mod(int l, int r, long long v, int t = 1, int tl = 1, int tr = N) {if (r < tl || tr < l || tmax[t] < v) {
Resources | ||||
---|---|---|---|---|
CF |
Focus Problem – try your best to solve this problem before continuing!
Solution - Range Chmin Chmax Add Set Sum
The solution to The Child and Sequence
uses a simplified but similar solution
to segment tree beats. For the problem above, let us divide it into three
subtasks:
- Allow only operations 0 and 3
- Allow only operations 0, 2, and 3
- All operations are allowed
Subtask 1
Build a segment tree over the range. In each node of the segment tree, maintain four values: , , , and , which correspond respectively to the sum of the elements of said range, the strict maximum value, the strict second largest value (if there is no such value, ), and the number of occurences of the maximum element. We would like to perform the following operations:
- For each , let (this operation will henceforth be referred as chmin)
- Query
The issue, again, is that in lazy propagation, it is difficult to update the sum to reflect the chmin update. We will use a similar strategy to the previous task where we build a seemingly slow solution, and then optimize it to pass in time.
Firstly, if the update value is larger than the maximum value in the range (stored in ), then we can return as the update will not effect any element in the range. Secondly, if the update value is between and , the new sum can be easily calculated using .
function update(upd_left, upd_right, upd_value, tree_node, tree_left, tree_right) if upd_right < tree_left or tree_right < upd_left or max1 < upd_value return if upd_left < tree_left and tree_right < upd_right and max2 < upd_value apply update return push lazy updates down let tree_mid = (tree_left + tree_right) / 2 let left_child = 2 * tree_node let right_child = 2 * tree_node + 1 update(upd_left, upd_right, upd_value, left_child, tree_left, tree_mid) update(upd_left, upd_right, upd_value, right_child, tree_mid + 1, tree_right) merge values from children
To prove that this runs in , we need to define a variable that represents the sum of the number of distinct elements over all intervals in the segment tree. This number is bounded by , which is the sum of the sizes of every interval.
Why are queries slow? Because they could visit up to nodes in any given
query. Define an extra operation to be when a query is passed onto a node's
children despite being in the query range. In other words, when a node satisfies
query_left ≤ tree_left and tree_right ≤ query_right and upd_value ≤ max2
, an
extra operation is performed.
Each time an extra operation is performed, decreases by at least 1, because both the and elements are decreased to . Because does not increase, the complexity is bounded by .
Subtask 2
Sum range updates can be added without much modification to the existing code, by simply adding another lazy tag. The proof of the time complexity from the previous part breaks down, but a tentative upper bound of the algorithm is . A complete proof can be found here.
Subtask 3
Store three more variables , , and . These will be implemented similar to their counterparts. Take note of the edge case when or vice versa.
Implementation - Range Chmin Chmax Add Range Sum
C++
#include <bits/stdc++.h>using namespace std;using ll = long long;const int MAXN = 200001; // 1-basedint N;ll A[MAXN];struct Node {
Problems
Status | Source | Problem Name | Difficulty | Tags | |
---|---|---|---|---|---|
HDU | Very Easy | Show TagsSegTreeBeats | |||
CSA | Easy | Show TagsSegTreeBeats | |||
CF | Normal | Show TagsSegTreeBeats | |||
HR | Normal | Show TagsSegTreeBeats | |||
CF | Normal | Show TagsSegTreeBeats | |||
CF | Normal | Show TagsSegTreeBeats | |||
CF | Normal | Show TagsSegTreeBeats | |||
CF | Hard | Show TagsSegTreeBeats |
Module Progress:
Join the USACO Forum!
Stuck on a problem, or don't understand a module? Join the USACO Forum and get help from other competitive programmers!