ICPC Primer S05: Trees & Range Queries
Mastering hierarchical data and efficient updates. Transitioning from basic prefix sums to Segment Trees and Fenwick Trees.
Session Architecture
- Topic: Trees & Range Queries
- Focus: Subtree Queries & Segment/Fenwick Introduction
- Goal: Handling hierarchical data and updates efficiently.

Problem 1: List Removals
- Source: CSES 1749
- Topic: Segment Tree / Fenwick Tree with Binary Search
- Constraint:
- Target Complexity:
Description: You are given a list consisting of integers. Your task is to process removals from the list and print the value of the removed elements in the exact order they are removed.
Input: The first input line contains an integer : the size of the list. The second line has integers : the contents of the list. The third line has integers : the positions of the elements to be removed. During the -th operation, you remove the element at the -th position from the current list.
Output: Print the elements in the order they are removed.
Analysis & Hints
If we use a standard std::vector and call .erase(), the operation takes time per query, resulting in an Time Limit Exceeded (TLE) error.
Instead of actually removing elements, what if we just mark them as "deleted"?
- Create a Fenwick Tree (Binary Indexed Tree) or a Segment Tree initialized with s at every valid index, representing that the element is still present.
- The prefix sum up to index tells us exactly how many elements are still in the list from the start up to .
- To find the -th remaining element, we can binary search over our Fenwick Tree to find the smallest index where the prefix sum equals .
- Once found, print the original array value at that index, and update the tree by changing the to a .
Implementation (C++)
// Compilation & Execution Instructions:
// g++ main.cpp -o main
// .\main
// PowerShell: Get-Content input.txt | .\main.exe
// Bash: cat input.txt | .\main.exe
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solve() {
// ---------------------------------------------------------
// The solution for this problem will be revealed live
// during the ICPC Primer S05 session on April 15th.
// ---------------------------------------------------------
cout << "Solution Hidden" << '\n';
}
int main() {
// Fast I/O Optimization
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int t = 1;
// cin >> t;
while (t--) {
solve();
}
return 0;
}
Problem 2: Distinct Values Queries
- Source: CSES 1734
- Topic: Offline Processing & Range Queries
- Constraint:
- Target Complexity:
Description: You are given an array of integers and queries of the form . For each query, you must report the number of distinct values in the subarray from index to index .
Input: The first line contains two integers and . The second line contains integers: the values in the array. The following lines describe the queries. Each line has two integers and .
Output: Print the number of distinct values for each query.
Analysis & Hints
Answering these queries "online" (exactly in the order they are given) is extremely difficult without advanced data structures like a Persistent Segment Tree. However, we have all the queries upfront. This means we can process them offline.
- Sort the Queries: Group or sort the queries based on their right endpoint .
- Sweep Line: Iterate through the array from left to right. As you visit each element, keep track of its "last seen" index using a Hash Map (
std::map). - Point Updates: If you see an element for the first time, add a to its current index in a Fenwick Tree. If you have seen it before, remove the from its old position and add a to its new current position. This guarantees that any distinct number only ever contributes exactly to the active range.
- Range Sums: When your sweep line reaches the right endpoint of a query, the answer to that query is simply a range sum on the Fenwick Tree from to .
Implementation (C++)
// Compilation & Execution Instructions:
// g++ main.cpp -o main
// .\main
// PowerShell: Get-Content input.txt | .\main.exe
// Bash: cat input.txt | .\main.exe
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solve() {
// ---------------------------------------------------------
// The solution for this problem will be revealed live
// during the ICPC Primer S05 session on April 15th.
// ---------------------------------------------------------
cout << "Solution Hidden" << '\n';
}
int main() {
// Fast I/O Optimization
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int t = 1;
// cin >> t;
while (t--) {
solve();
}
return 0;
}
Problem 3: Binary Assignment
- Source: ICPC 2022 HCMC Regional - B
- Topic: Advanced Segment Tree with Lazy Propagation
- Constraint:
- Target Complexity:
Description: Given a binary string of length , let be the length of the shortest binary string that is not a subsequence of . Let be the number of strings of length that are not subsequences of . You must process sequential modifications to . Modifications are of three types:
0 l r– Set all bits in the range to .1 l r– Set all bits in the range to .F l r– Flip all bits in the range ( and ).
After each query, output the values of and modulo .
Input: The first line contains and . The second line contains the binary string . The next lines contain the descriptions of the modifications.
Output: For each modification, output two space-separated integers: and .
Analysis & Hints
This is a heavily constrained regional-level problem that tests your ability to map abstract mathematical properties onto a Segment Tree.
- Lazy Propagation is Mandatory: The queries involve massive range updates (set to 0, set to 1, flip). This immediately signals that a Segment Tree with Lazy Propagation is required.
- Subsequence DP on a Tree: To compute and , you need to know how characters combine to form subsequences. Each node in the Segment Tree must represent a substring and store a state (usually a DP matrix or a set of counts) that describes the shortest non-subsequence data.
- State Merging: How do you merge the DP state of a left child and a right child? If you know the properties of the left half and the right half, you can mathematically deduce the properties of the parent.
- Handling Flips: Because there is a
Flipoperation, every node must maintain its standard state and its completely flipped state simultaneously. When a flip update arrives, you swap the two states and push the lazy tag down.
Implementation (C++)
// Compilation & Execution Instructions:
// g++ main.cpp -o main
// .\main
// PowerShell: Get-Content input.txt | .\main.exe
// Bash: cat input.txt | .\main.exe
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solve() {
// ---------------------------------------------------------
// The solution for this problem will be revealed live
// during the ICPC Primer S05 session on April 15th.
// ---------------------------------------------------------
cout << "Solution Hidden" << '\n';
}
int main() {
// Fast I/O Optimization
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int t = 1;
// cin >> t;
while (t--) {
solve();
}
return 0;
}
Additional Practice (Homework)
To lock in these data structures before we hit Advanced DP in Session 06, check out these extensions:
- Range Update Queries - CSES (Classic Lazy Prop / Difference Array on Fenwick)
- Salary Queries - CSES (Coordinate Compression + Point Updates)
- Inversion Probability - Codeforces (Applying Segment Trees to Expected Values)