Here is the link to the contest: Link
A. The Blacksmith
Approach 1
Step 1
$$$n \le 500$$$, so the total number of pairs is at most $$$\frac{n(n-1)}{2} = \frac{500 \cdot 499}{2} = 124750$$$. This is easily manageable within time limits.
Step 2
By checking every possible pair, we never miss any valid combination. Therefore, keeping the maximum among all valid pairs yields the correct answer. If the compatibility condition never holds, we never assign a product, so the result must be −1.
t = int(input())
for _ in range(t):
n, k = map(int, input().split())
s = list(map(int, input().split()))
ans = -1
for i in range(n):
for j in range(i + 1, n):
if abs(s[i] - s[j]) >= k:
ans = max(ans, s[i] * s[j])
print(ans)
Time Complexity: We check all pairs ($$$i$$$, $$$j$$$) with $$$i \lt j$$$. This results in roughly $$$\frac{n(n-1)}{2} \approx O(n^2)$$$ iterations. Each iteration does constant work (difference, product, max update). Therefore, the overall time complexity is $$$O(n^2)$$$ per testcase.
Space Complexity: We only use a few variables (
ans,i,j, and the input array). No extra data structures are created. Therefore, the extra space complexity is $$$O(1)$$$.
Approach 2
Step 1
Products involving the largest number $$$m$$$ are typically better than products involving only smaller numbers. That is, for any $$$a \lt m$$$, we have $$$a \cdot b \le m \cdot b$$$. Therefore, any optimal product must use $$$m$$$, otherwise replacing the larger of the two numbers with $$$m$$$ would increase or maintain the product.
Step 2
We only need to test compatibility between $$$m$$$ and every other element $$$x$$$ in the array. If $$$m − x \lt k$$$ for every $$$x$$$, then no pair including $$$m$$$ is valid. But since all other values are $$$\le M$$$ and give even smaller differences and products, no pair can possibly be valid. Therefore, the answer is $$$−1$$$.
t = int(input())
for _ in range(t):
n, k = map(int, input().split())
s = list(map(int, input().split()))
m = max(s)
ans = -1
for x in s:
if m - x >= k:
ans = max(ans, m * x)
print(ans)
Time Complexity: We first find the maximum element $$$m$$$ (which takes $$$O(n)$$$ time) and then iterate through the array once to check compatibility with $$$m$$$ (which also takes $$$O(n)$$$ time). Each check is constant time. Total time complexity is $$$O(2n) = O(n)$$$ per testcase.
Space Complexity: Only a few variables are used (
m,ans,x), with no extra arrays or structures. Extra space complexity is $$$O(1)$$$.
B. Jazz
Step 1
A Jazz Ensemble is a set of notes where every note can reach every other. In graph theory, this is exactly a Strongly Connected Component (SCC) of the directed graph. To find SCCs, we can use Kosaraju’s or Tarjan’s algorithm, both of which run in $$$O(n + m)$$$ time for a graph with $$$n$$$ nodes and $$$m$$$ edges. Given the problem constraints, these algorithms are efficient enough.
Step 2
Once SCCs are identified, collapse each into a single node. Add edges between SCCs if an edge exists in the original graph connecting them. The resulting graph is a DAG because cycles only exist inside SCCs, and collapsing removes all inter-SCC cycles.
Step 3
Current SCCs represent ensembles. Each SCC alone is a valid ensemble. Its size is the number of notes inside. Adding one edge can merge SCCs along a path. For instance, if we pick a path in the DAG $$$S_0 \rightarrow S_1 \rightarrow S_2 \rightarrow \dots \rightarrow S_k$$$ and add an edge from any node in $$$S_k$$$ to any node in $$$S_0$$$, we form a cycle along this path. Now every SCC on the path is mutually reachable, and hence merged into a single ensemble. The size of this new ensemble is the sum of sizes of SCCs along the path.
Step 4
Any SCC formed by adding one edge must lie along some path in the DAG. Therefore, the size of the largest ensemble achievable with one extra edge is the same as the maximum sum of SCC sizes along any directed path in the DAG. We compute this max sum starting from each SCC using DFS and store results using dynamic programming. The largest value (that is, the maximum path sum) is the answer.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t; cin >> t;
while (t--) {
int n, m; cin >> n >> m;
vector<vector<int>> graph(n), reversedGraph(n), dag(n);
vector<int> sccIndex(n, -1), sccSize(n, 0);
vector<bool> visited(n, false);
vector<int> order;
for (int i = 0; i < m; i++) {
int u, v; cin >> u >> v; u--; v--;
graph[u].push_back(v);
reversedGraph[v].push_back(u);
}
function<void(int)> dfsOriginal = [&](int node) {
visited[node] = true;
for (int neighbor : graph[node])
if (!visited[neighbor]) dfsOriginal(neighbor);
order.push_back(node);
};
function<void(int, int)> dfsReversed = [&](int node, int sccId) {
sccIndex[node] = sccId;
sccSize[sccId]++;
for (int neighbor : reversedGraph[node])
if (sccIndex[neighbor] == -1) dfsReversed(neighbor, sccId);
};
for (int i = 0; i < n; i++) if (!visited[i]) dfsOriginal(i);
int sccCount = 0;
for (int i = n - 1; i >= 0; i--) {
int node = order[i];
if (sccIndex[node] == -1) dfsReversed(node, sccCount++);
}
for (int node = 0; node < n; node++)
for (int neighbor : graph[node])
if (sccIndex[node] != sccIndex[neighbor])
dag[sccIndex[node]].push_back(sccIndex[neighbor]);
for (int i = 0; i < sccCount; i++) {
sort(dag[i].begin(), dag[i].end());
dag[i].erase(unique(dag[i].begin(), dag[i].end()), dag[i].end());
}
vector<int> dp(sccCount, -1);
function<int(int)> dfsDP = [&](int node) {
if (dp[node] != -1) return dp[node];
int res = sccSize[node];
for (int neighbor : dag[node])
res = max(res, sccSize[node] + dfsDP(neighbor));
return dp[node] = res;
};
int result = 0;
for (int i = 0; i < sccCount; i++)
result = max(result, dfsDP(i));
cout << result << "\n";
}
}
- Time Complexity: The algorithm first computes the strongly connected components (SCCs) using Kosaraju’s method, which involves two depth-first searches over the graph and its reverse. Each DFS visits every node and edge exactly once, giving $$$O(n + m)$$$ time. Constructing the DAG from the SCCs iterates over all original edges to check if they connect different SCCs, which is also $$$O(n + m)$$$. Finally, the depth-first search for the maximum path sum in the DAG visits every SCC and every DAG edge once, which again is $$$O(n + m)$$$. Combining all these steps, the total time complexity remains $$$O(n + m)$$$ per testcase.
- Space Complexity: The algorithm uses several structures to represent the graph and auxiliary data. The original graph and its reverse each store all edges, taking $$$O(n + m)$$$ space each. The DAG representing SCC connections stores at most one edge for each original edge, giving $$$O(n + m)$$$ space. Arrays such as
sccIndexandsccSizetrack the SCC ID and size for each node, and thevisitedanddparrays store flags and DP values, each taking $$$O(n)$$$ space. Overall, the algorithm uses $$$O(n + m)$$$ space per testcase, which is necessary to store the graph, SCC information, and dp memoization.
C. K-Boosted Subarray
Step 1
One important observation is that the two operations can be described as scaling a subarray by a factor. Multiplying by $$$k$$$ increases all values, which is beneficial if the subarray sum is positive. Dividing by $$$k$$$ reduces all values, which might be beneficial if the subarray sum is negative, because floor division of negative numbers gives a less negative value.
Step 2
Claim: Only the subarray that achieves the maximum subarray sum in the original array is relevant for modification. Modifying any other subarray cannot yield a larger sum than modifying this maximum subarray.
For a rigorous justification of this claim, refer to the proof section below.
Step 3
Let $$$S$$$ be the maximum subarray sum of the original array. If we choose to multiply the subarray contributing to $$$S$$$, the resulting sum becomes $$$k \cdot S$$$. If we choose to floor-divide the same subarray, the resulting sum is at most $$$\left\lfloor \frac{S}{k} \right\rfloor$$$. Since exactly one operation must be applied, we only need to consider these two candidates:
When $$$S$$$ is positive, multiplying produces the larger sum. When $$$S$$$ is negative, floor division produces the larger sum. This formula captures the optimal choice in all cases, whether the maximum subarray sum is positive, zero, or negative.
Heads-Up for C++ Users
Python’s // operator performs floor division, rounding down to the nearest integer. In C++, integer division rounds towards zero, so negative values behave differently. For example, -5 // 2 is -3 in Python, but -5 / 2 is -2 in C++. To match Python’s behavior in C++, you can use:
int floor_div(int a, int b) {
if (a >= 0) return a / b;
return (a - b + 1) / b; // adjust for negative values
}
This makes your calculations consistent with the Python solution when handling negative numbers.
Let $$$S$$$ be the maximum subarray sum of the original array. We analyze the behavior of the two operations based on the sign of $$$S$$$ and the value of $$$k$$$.
Case 1: $$$k = 1$$$
If $$$k$$$ is 1, this simply means the operations will not modify the array. And therefore $$$S$$$ is the answer.
Case 2: $$$k \gt 1$$$ and the original Maximum Subarray Sum is Positive ($$$S \gt 0$$$)
When the array contains positive segments, our goal is to make the positive sum as large as possible. Since $$$k \ge 1$$$, multiplying a positive number by $$$k$$$ increases it, while dividing it decreases it. Therefore, the Multiplication Operation is the natural choice for positive subarrays.
If we choose to multiply, the optimal strategy is simply to take the subarray that already has the maximum sum $$$S$$$ and multiply every element in it by $$$k$$$. The new sum becomes $$$S \cdot k$$$. But, if we choose to divide, the optimal strategy is to find a negative "bridge" subarray and divide that bridge to connect two positive subarrays.
Let's say we have two subarrays with positive sums $$$A$$$ and $$$C$$$. And then, let's say the sum of the subarray between them is $$$-B$$$. And also, let's say $$$A \ge C$$$. Look at the image below for illustration.

We are going to compare two strategies:
1. Take the largest existing segment (that is, $$$A$$$) and multiply it, resulting in $$$A \cdot k$$$.
2. Keep $$$A$$$ and $$$C$$$ as they are, but reduce the penalty $$$B$$$, resulting in $$$A + \left\lfloor \frac{-B}{k} \right\rfloor + C$$$.
So, let's make a claim that the following is true.
This means we can make the following logical steps:
Here, since $$$k \gt 1$$$, the right-hand side is always positive. Because even for the smallest possible value of $$$k = 2$$$, $$$A (k - 1) - C$$$ simplifies to $$$A (2 - 1) - C = A - C$$$. And since we said $$$A \ge C$$$, $$$A - C$$$ is positive or zero.
And since $$$-B$$$ is a negative number, floor dividing it by $$$k$$$ still gives a negative number. And therefore, the left-hand side is negative. And that means the statement is wrong, because it implies that a negative value is greater than a positive or zero value. And therefore the claim $$$A + \left\lfloor \frac{-B}{k} \right\rfloor + C \gt A \cdot k$$$ is wrong. This means it is always better to pick the $$$A \cdot k$$$ option. So, if the maximum subarray sum is positive, it is always best to multiply that subarray by $$$k$$$, resulting in an answer of $$$S \cdot k$$$.
Case 3: The original Maximum Subarray Sum is Negative or Zero ($$$S \le 0$$$)
If the maximum subarray sum is negative or zero, then this means the array contains only zeros and negative numbers. And hence $$$S$$$ is the single element with the smallest absolute value (closest to 0). If that value is $$$S = 0$$$, then it doesn't matter whether we multiply it or divide it, the answer is $$$0$$$ either way. Otherwise, if the number is negative, $$$S \lt 0$$$, multiplying by $$$k$$$ makes it more negative (which is bad), and dividing by $$$k$$$ makes it less negative (which is good).
Therefore, the best strategy is to take the max element and divide it by $$$k$$$, resulting in $$$\left\lfloor \frac{S}{k} \right\rfloor$$$.
And based on all the available cases, it can be seen that the answer is always either $$$S \cdot k$$$ or $$$\left\lfloor \frac{S}{k} \right\rfloor$$$.
import math
def max_subarray_sum(arr):
max_sum = current = arr[0]
for x in arr[1:]:
current = max(x, current + x)
max_sum = max(max_sum, current)
return max_sum
t = int(input())
for _ in range(t):
n, k = map(int, input().split())
a = list(map(int, input().split()))
S = max_subarray_sum(a)
print(max(k * S, S // k))
Time Complexity: The main step is to compute the maximum subarray sum $$$S$$$ of the original array. This is done using Kadane’s algorithm, which iterates through the array once. For an array of length $$$n$$$, this takes $$$O(n)$$$ time. After computing $$$S$$$, we simply calculate the answer using the formula described in the tutorial section. It only involves constant-time arithmetic operations, that is, $$$O(1)$$$. Therefore, for each test case, the total time is: $$$O(n)$$$.
Space Complexity: We only need a few variables to run Kadane’s algorithm:
current_sumto track the current subarray sum andmax_sumto track the maximum found so far. We do not need any extra arrays or data structures beyond the input array. Therefore, the extra space is $$$O(1)$$$, ignoring the input array itself.
D. Highland Victories
- Problem Idea: abenezer_m54
- Problem Setter: eulmelk
Step 1
Setup a happiness counter and start from $$$0$$$. Iterate through the array of strengths starting from the second day. And then for each day, compare the strength of the current day with the previous day. If it is strictly greater, increment a happiness counter. And then return the counter.
t = int(input())
for _ in range(t):
n = int(input())
a = list(map(int, input().split()))
happy_days = 0
for i in range(1, n):
if a[i] > a[i-1]:
happy_days += 1
print(happy_days)
Time Complexity: For each test case, we iterate over $$$n$$$ elements once. Therefore, the time complexity is $$$O(n)$$$ per testcase.
Space Complexity: The algorithm only uses a counter and loop variables. No additional data structures are required. Therefore, the auxiliary space complexity is $$$O(1)$$$ per testcase.
E. The Vase
- Problem Idea: YouTube Video
- Problem Setter: eulmelk
Step 1
We have exactly $$$2$$$ identical test vases, and the total number of drops per testcase cannot exceed $$$45$$$. This is essentially the classic 2-egg problem from algorithm theory. One effective approach is to drop the first vase in intervals and then use the second vase for a linear search within the reduced interval once the first vase breaks.
Step 2
Suppose we drop the first vase on floor $$$k$$$, then on floor $$$2 \cdot k$$$, then on floor $$$3 \cdot k$$$, and so on. Once the vase breaks on floor $$$i \cdot k$$$, we know the critical floor is between $$$(i-1)\cdot k$$$ and $$$i \cdot k$$$. For a given $$$k$$$, this strategy finds the critical floor in approximately $$$\frac{n}{k} + k$$$ moves. The optimal $$$k$$$ is roughly $$$\sqrt{n}$$$. For $$$n = 1000$$$, that gives $$$k \approx 32$$$, requiring about $$$64$$$ moves in total, which exceeds the $$$45$$$-move limit. We need a better approach.
Step 3
Instead, drop the first vase on floor $$$k$$$, then on floor $$$k + (k-1)$$$, then on floor $$$k + (k-1) + (k-2)$$$, and so on. When the first vase breaks, we can perform a linear search using the second vase starting from the previous floor where the first vase was dropped.
Step 4
If the first vase breaks on floor $$$k$$$, the critical floor lies between floors $$$1$$$ and $$$k-1$$$. We can find it in $$$k-1$$$ moves with the second vase, plus $$$1$$$ move for the drop at floor $$$k$$$, totaling $$$k$$$ moves. If the first vase breaks on floor $$$k + (k-1)$$$, the critical floor is between floors $$$k+1$$$ and $$$k + (k-1) - 1 = 2k - 2$$$. Linear searching this range with the second vase takes $$$(2k-2) - (k+1) + 1 = k-2$$$ moves. Including the two drops of the first vase, the total moves also equal $$$k$$$.
Step 5
To cover all $$$n$$$ floors, we need $$$k + (k-1) + (k-2) + \dots + 2 + 1 \ge n$$$. Using the formula for the sum of an arithmetic series, this is $$$\frac{k(k+1)}{2} \ge n$$$. The minimum number of moves required is the smallest $$$k$$$ satisfying this inequality. For $$$n = 1000$$$, the optimal value is $$$k = 45$$$. This strategy guarantees that the critical floor can be found within $$$45$$$ moves, meeting the problem’s constraints.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
// Determine the minimum starting step dynamically
int step = 1;
while (step * (step + 1) / 2 <= n) step++;
int prev_floor = 0;
int floor = step;
int vase = 1;
// Drop the first vase with decreasing steps
int current_step = step;
while (floor <= n) {
cout << "? " << vase << " " << floor << endl;
int response;
cin >> response;
if (response == 1) break; // vase breaks
prev_floor = floor; // last safe floor
current_step--; // decrease step
floor += current_step; // next floor to drop
}
// Use second vase for linear search
int critical_floor = prev_floor;
vase = 2;
for (int f = prev_floor + 1; f < floor && f <= n; f++) {
cout << "? " << vase << " " << f << endl; // switch vase
int response;
cin >> response;
if (response == 1) break; // vase breaks
critical_floor = f;
}
// Output the final answer
cout << "! " << critical_floor << endl;
}
return 0;
}
Query Complexity: The primary constraint in this interactive problem is the number of drops (queries) per testcase. Using the decreasing-step strategy, the first vase is dropped at most $$$k$$$ times, and the second vase is used for linear search in the last interval. By construction, the total number of queries per testcase is exactly $$$k$$$, which is the smallest integer satisfying $$$\frac{k(k+1)}{2} \ge n$$$. For the maximum $$$n = 1000$$$, this gives $$$k = 45$$$, ensuring that the number of queries never exceeds the limit.
Time Complexity: Each query and the associated computations take $$$O(1)$$$ time. Since there are at most $$$k$$$ queries, the total time complexity per testcase is $$$O(k) \lt 2 \cdot \sqrt{n} = O(\sqrt{n})$$$ in terms of queries, which is within bounds for the given constraints.
Space Complexity: The solution only stores a few integer variables (current floor, previous safe floor, step size, etc.) and does not use any additional arrays or data structures. Therefore, the space complexity is $$$O(1)$$$
F. Name Chain
Step 1
We can model this problem using graph theory. Each full name corresponds to an edge, while the first name and last name individually act as nodes. A full name therefore connects its first name node with its last name node. Under this interpretation, the problem is asking us to find the longest path between any two nodes in a graph.
Step 2
The constraints in the statement allow us to further classify the structure of the graph. It says that every name is reachable from every other name, so the graph is connected. It also guarantees that there is exactly one unique chain between any two names, which means there is exactly one simple path between any two nodes. A connected graph with a unique path between every pair of nodes is, by definition, a tree. Thus, the task reduces to finding the diameter of this tree.
Step 3
There are several standard ways to compute the diameter of a tree. A common technique is the two-phase Breadth-First Search (BFS) method. First, pick any arbitrary node and run BFS to find the farthest node from it. Let this node be one endpoint of the diameter. And then run a second BFS starting from this endpoint to find the farthest reachable node. The distance obtained here is the tree’s diameter.
Step 4
Since the problem requires outputting the chain itself, and not just its length, we must reconstruct the path. To do this, we store a parent array during BFS and backtrack from the final endpoint to recover the entire chain of names.
from collections import deque, defaultdict
def bfs(start, adj):
q = deque([start])
dist = {start: 0}
parent = {start: None}
farthest = start
while q:
u = q.popleft()
if dist[u] > dist[farthest]:
farthest = u
for v in adj[u]:
if v not in dist:
dist[v] = dist[u] + 1
parent[v] = u
q.append(v)
return farthest, dist, parent
def reconstruct_path(end, parent):
path = []
cur = end
while cur is not None:
path.append(cur)
cur = parent[cur]
return path
t = int(input())
for _ in range(t):
n = int(input())
adj = defaultdict(list)
first_node = None
for _ in range(n):
f, l = input().split()
adj[f].append(l)
adj[l].append(f)
if first_node is None:
first_node = f
# BFS 1: find one endpoint of the diameter
a, _, _ = bfs(first_node, adj)
# BFS 2: find the other endpoint and parent map
b, _, parent = bfs(a, adj)
path = reconstruct_path(b, parent)
print(len(path))
print(" ".join(path))
Time Complexity: For each testcase, we first build the graph by adding an edge for each full name. Since there are $$$n$$$ full names, this takes $$$O(n)$$$ time. Then we run BFS twice: the first BFS finds one endpoint of the diameter, and the second BFS finds the other endpoint while recording parent pointers to reconstruct the path. Each BFS visits every node and edge once, which takes $$$O(V + E)$$$ time. Since the graph is a tree, there are $$$n$$$ edges and $$$n+1$$$ nodes, so this simplifies to $$$O(n)$$$. Reconstructing the path from the parent map also takes $$$O(n)$$$ time. Therefore, the total time per testcase is $$$O(n)$$$.
Space Complexity: We store the graph in an adjacency list, which requires $$$O(V + E)$$$ space, simplifying to $$$O(n)$$$ for the same reason as above. BFS additionally uses a distance map, a parent map, and a queue, each needing at most $$$O(n)$$$ space. The reconstructed path also takes up to $$$O(n)$$$ space. Hence, the total space used per testcase is $$$O(n)$$$.
G. Uniform Bamboo Pavilion
Step 1
Notice that for a bamboo with segment length $$$l_i$$$ and segment count $$$c_i$$$, the possible heights after cutting are exactly:
So for each bamboo, the set of achievable heights is the set of multiples of its segment length up to its total height. The problem then reduces to finding a common number that belongs to all these sets.
Step 2
This is equivalent to saying that the common height for all bamboos must be a common multiple of all the segment lengths. If there are two possible heights $$$h_1$$$ and $$$h_2$$$ with $$$h_1 \le h_2$$$, it is always optimal to achieve the smaller one $$$h_1$$$. This is because if $$$h_2$$$ is achievable, we can reduce the segment counts accordingly to achieve $$$h_1$$$, so $$$h_1$$$ is always achievable. Therefore, it is best to pick the Least Common Multiple (LCM) of all $$$l_i$$$ as the candidate common height. If this height is achievable, we output it; otherwise, no other height is possible.
Step 3
Let $$$h$$$ be the LCM of all $$$l_i$$$ across the array. Then, for each bamboo, we can determine the cut position $$$x_i$$$ using the formula:
If $$$x_i \le c_i$$$ for all $$$i$$$, the height $$$h$$$ is achievable and we print the $$$x_i$$$ values. Otherwise, it is impossible and we print $$$-1$$$.
from math import gcd
def lcm(a, b):
return a * b // gcd(a, b)
t = int(input())
for _ in range(t):
n = int(input())
l = list(map(int, input().split()))
c = list(map(int, input().split()))
# compute LCM of all segment lengths iteratively
h = l[0]
for i in range(1, n):
h = lcm(h, l[i])
# find cut positions
x = []
possible = True
for i in range(n):
xi = h // l[i]
if xi > c[i]:
possible = False
break
x.append(xi)
if possible:
print(h)
print(" ".join(map(str, x)))
else:
print(-1)
Time Complexity: Computing the LCM of $$$n$$$ segment lengths takes $$$O(n \log M)$$$ time using pairwise GCD, where $$$M \le 40$$$ is the maximum segment length. Calculating the cut positions $$$x_i = h / l_i$$$ for all bamboos takes $$$O(n)$$$ time. Hence, the total time complexity per testcase is $$$O(n \log M)$$$.
Space Complexity: We store the input arrays $$$l_i$$$ and $$$c_i$$$, which requires $$$O(n)$$$ space. The output array of cut positions $$$x_i$$$ is also of size $$$O(n)$$$, but since these are required by the problem, they don't count as extra space. Apart from these, the algorithm only uses a constant amount of additional memory for variables such as the LCM, loop counters, and flags. Therefore, the auxiliary space used per testcase is $$$O(1)$$$.
H. Abba Gorgoryos and Scrolls
- Problem Idea: abenezer_m54
- Problem Setter: eulmelk
Approach 1
Step 1
Notice that removing symbols from the end of the scroll any number of times is equivalent to choosing a prefix of the scroll. Therefore, the problem reduces to finding the longest prefix of the scroll string that appears at least $$$k$$$ times, including as a prefix.
Step 2
Suppose we guess that the length of the longest prefix appearing at least $$$k$$$ times is $$$m$$$. To verify this, we can take the first $$$m$$$ symbols as the candidate prefix. Then we need to count how many times this prefix appears in the scroll. This can be done efficiently using algorithms such as Knuth-Morris-Pratt (KMP) or the Z-algorithm.
Step 3
Let $$$c$$$ be the number of times the current prefix appears in the scroll.
- If $$$c \ge k$$$, the prefix is valid, but we may be able to find a longer prefix that also works.
- If $$$c \lt k$$$, the prefix is too long and cannot appear enough times, so we need a shorter prefix.
This naturally leads to using binary search on the prefix length from $$$1$$$ to $$$n$$$. By checking a prefix of length $$$m$$$ and adjusting the search range, we can efficiently find the longest valid prefix.
Step 4
Once the longest prefix of length $$$m$$$ is determined, the minimum number of symbols to remove from the end is:
If no valid prefix exists (no prefix appears at least $$$k$$$ times), the answer is $$$-1$$$.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n, k;
string s;
cin >> n >> k >> s;
// Precompute full LPS array for the entire string
vector<int> full_lps(n, 0);
for (int i = 1, len = 0; i < n;) {
if (s[i] == s[len]) full_lps[i++] = ++len;
else if (len) len = full_lps[len - 1];
else full_lps[i++] = 0;
}
// Function to check if prefix of length m occurs at least k times
auto check = [&](int m) -> bool {
int cnt = 0, j = 0;
for (int i = 0; i < n; i++) {
while (j > 0 && (j >= m || s[i] != s[j])) j = full_lps[j - 1];
if (s[i] == s[j]) j++;
if (j == m) {
cnt++;
j = full_lps[j - 1];
}
}
return cnt >= k;
};
// Binary search for the longest valid prefix
int lo = 1, hi = n, ans = -1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (check(mid)) {
ans = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
if (ans == -1) cout << -1 << "\n";
else cout << n - ans << "\n"; // minimum symbols to remove
}
}
Time Complexity: Let $$$n$$$ be the length of the scroll. Each binary search step requires counting occurrences of a prefix, which can be done in $$$O(n)$$$ time using KMP or Z-algorithm. Since the binary search makes $$$O(\log n)$$$ steps, the total time per testcase is $$$O(n \log n)$$$.
Space Complexity: We need to store the scroll string and auxiliary arrays for the KMP or Z-algorithm, which requires $$$O(n)$$$ space per testcase.
Approach 2
Step 1
Notice that removing symbols from the end of the scroll any number of times is equivalent to choosing a prefix of the scroll. Therefore, the problem reduces to finding the longest prefix of the scroll string that appears at least $$$k$$$ times, including as a prefix.
Step 2
We can compute the Z-array of the string using the Z-algorithm. In the Z-array, $$$z_i$$$ is the length of the longest substring starting at index $$$i$$$ that matches a prefix of the string. This array can help us efficiently determine how many times each prefix appears in the string.
Step 3
Let the length of the prefix be $$$p$$$. If a prefix of length $$$p$$$ appears $$$x_p$$$ times, then all prefixes shorter than $$$p$$$ also appear at least $$$x_p$$$ times. We can count how many times each value occurs in the Z-array: let $$$C(x)$$$ be the number of indices $$$i$$$ such that $$$z_i = x$$$. Then the number of occurrences of a prefix of length $$$p$$$ can be written using sigma notation as:
Equivalently, in expanded form:
Step 4
We can use the concept of prefix sums (or more precisely, suffix sums) on the $$$C$$$ array to efficiently calculate $$$x_p$$$ for all $$$p$$$ from $$$1$$$ to $$$n$$$. After that, the largest $$$p$$$ such that $$$x_p \ge k$$$ is the length of the longest valid prefix. The minimum number of symbols to remove is then:
If no such prefix exists, the answer is $$$-1$$$.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n, k;
string s;
cin >> n >> k >> s;
vector<int> z(n, 0);
int l = 0, r = 0;
// Compute Z-array
for (int i = 1; i < n; i++) {
if (i <= r) z[i] = min(r - i + 1, z[i - l]);
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) z[i]++;
if (i + z[i] - 1 > r) {
l = i;
r = i + z[i] - 1;
}
}
// Count occurrences of each length in Z-array
vector<int> C(n + 1, 0);
for (int i = 0; i < n; i++) {
C[z[i]]++;
}
C[n]++; // include full string as prefix (z_0)
// Compute suffix sums
for (int i = n - 1; i >= 1; i--) {
C[i] += C[i + 1];
}
// Find longest prefix length that occurs at least k times
int ans = -1;
for (int p = 1; p <= n; p++) {
if (C[p] >= k) ans = p;
}
if (ans == -1) cout << -1 << "\n";
else cout << n - ans << "\n";
}
}
Time Complexity: Computing the Z-array takes $$$O(n)$$$ time. Counting occurrences and computing suffix sums also take $$$O(n)$$$ time. Therefore, the total time per testcase is $$$O(n)$$$.
Space Complexity: We store the scroll string, the Z-array, and the occurrence array $$$C$$$, each of size $$$O(n)$$$. Therefore, the total space used per testcase is $$$O(n)$$$. Note that the output does not count as extra space.
I. The Candy Seller
Step 1
Notice that there are only two possible answers for this problem. Let the sum of all the children's maximum candy requests be:
- If $$$S \ge k$$$, then the candy seller does not have enough candies to satisfy all the children, and he ends up selling exactly $$$k$$$ candies.
- Otherwise, if $$$S \lt k$$$, the candy seller can give every child as many candies as they want, and the total candies sold will be the sum of the array $$$S$$$.
Therefore, the answer is simply:
# Read input
n, k = map(int, input().split())
a = list(map(int, input().split()))
# Compute the total number of candies sold
total_sold = min(sum(a), k)
# Print the answer
print(total_sold)
Time Complexity: Calculating the sum of the array takes $$$O(n)$$$ time, so the total time per testcase is $$$O(n)$$$.
Space Complexity: We only store the input array of size $$$n$$$, and this is not extra space, so the space complexity is $$$O(1)$$$. No additional space is required beyond the input.
J. Shmena
Step 1
We can tackle this problem by thinking about smaller parts of the thread sequence. Instead of the whole array, consider sequences ending at a certain thread and how many interruptions have been used so far. By understanding these smaller sequences, we can extend them step by step to include more threads.
Step 2
Let’s define a subproblem using two parameters:
- $$$i$$$: the position of the last thread included in the Patterned Sequence,
- $$$x$$$: the number of interruptions used so far (at most $$$k$$$).
We are interested in the length of the longest Patterned Sequence ending at thread $$$i$$$ using at most $$$x$$$ interruptions.
Step 3
To compute the value for a sequence ending at thread $$$i$$$ with $$$x$$$ interruptions, we look at earlier threads $$$j \lt i$$$ and see how a sequence ending at $$$j$$$ can be extended to include $$$a_i$$$:
If $$$a_j \lt a_i$$$, we can extend the sequence without using a new interruption.
If $$$a_j \ge a_i$$$, we can still extend the sequence, but this uses one interruption.
If no earlier sequence can extend into $$$a_i$$$, the value naturally becomes $$$1$$$ (since a sequence consisting only of $$$a_i$$$ is always valid).
Step 4
The final answer is the maximum length among all sequences ending at any thread, considering all allowed interruptions.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n, k;
cin >> n >> k;
vector<int> a(n);
for (int i = 0; i < n; ++i) cin >> a[i];
// Memoization table: memo[i][x] = max length ending at i with x interruptions
vector<vector<int>> memo(n, vector<int>(k + 1, -1));
function<int(int, int)> dp = [&](int i, int x) -> int {
if (memo[i][x] != -1) return memo[i][x];
int best = 1;
for (int j = 0; j < i; ++j) {
if (a[j] < a[i])
best = max(best, dp(j, x) + 1);
else if (x > 0)
best = max(best, dp(j, x - 1) + 1);
}
return memo[i][x] = best;
};
int ans = 0;
for (int i = 0; i < n; ++i)
ans = max(ans, dp(i, k));
cout << ans << "\n";
}
return 0;
}
# Use PyPy for submission.
t = int(input())
for _ in range(t):
n, k = map(int, input().split())
a = list(map(int, input().split()))
dp = [[1] * (k + 1) for _ in range(n)]
ans = 1
for i in range(n):
for j in range(i):
for x in range(k + 1):
if a[j] < a[i]:
dp[i][x] = max(dp[i][x], dp[j][x] + 1)
elif x > 0:
dp[i][x] = max(dp[i][x], dp[j][x - 1] + 1)
ans = max(ans, max(dp[i]))
print(ans)
Top-Down Approach
Time Complexity: In the worst case, the memoized recursive function visits all valid states $$$(i,x)$$$ once.
For each state, it may scan all previous threads $$$j \lt i$$$ to decide if a new thread can extend the sequence with or without using an interruption. This results in $$$O(n^2 \cdot k)$$$ time in the worst case.Space Complexity: We store a memoization table of size $$$n \times (k+1)$$$, giving $$$O(nk)$$$ space. The recursion stack may go up to $$$O(n)$$$ depth. Therefore, the total space is $$$O(nk)$$$ plus $$$O(n)$$$ for the recursion stack, dominated by $$$O(nk)$$$ when $$$k$$$ is large.
Bottom-Up Approach
Time Complexity:
We fill a DP table $$$d[i][x]$$$ for each thread index $$$i=1..n$$$ and each number of allowed interruptions $$$x=0..k$$$.
For each $$$d[i][x]$$$, we may need to compare with all previous threads $$$j \lt i$$$ to check if we can extend a Patterned Sequence with or without using an interruption. Therefore, each state can take up to $$$O(n)$$$ time, giving a total of $$$O(n^2 \cdot k)$$$ per test case, similar to the top-down approach.Space Complexity:
We store the DP table of size $$$n \times (k+1)$$$, which gives $$$O(nk)$$$ space.
Additionally, if any auxiliary arrays are used to track sequences, they would be at most $$$O(n)$$$, so the total space remains $$$O(nk)$$$.
Try to find a solution that runs faster than $$$O(n^2 \cdot k)$$$.
Hint: Think about ways to track useful information about previous threads so that you do not need to compare each state with all earlier threads every time.
K. The Puzzle for Pardon
Step 1
For a given prime $$$p$$$, it can only form a twin prime pair with either $$$p + 2$$$ or $$$p - 2$$$. Therefore, the number of twin prime pairs that include $$$p$$$ is equal to the number of occurrences of $$$p+2$$$ and $$$p-2$$$ within the given range. We only consider them if they are prime; otherwise, we ignore them. That is, if $$$p+2$$$ is prime, we include its counts, otherwise we ignore it. The same applies for $$$p-2$$$.
Step 2
Using this method to count pairs for every prime in the range gives us the total number of twin prime pairs. Specifically, we start from the $$$l$$$ of the current query and go to $$$r$$$, applying Step 1. Using a sieve of Eratosthenes precomputation allows us to efficiently check if a value is prime. This gives the answer for the current query. This approach has a complexity of $$$O(n)$$$ per query. For $$$q$$$ queries, the overall complexity becomes $$$O(nq)$$$, which is not feasible, so a better approach is needed.
Step 3
Suppose we know the answer for a given range, and a new value $$$p$$$ is inserted at either end. If $$$p$$$ is not prime, the answer remains the same. If $$$p$$$ is prime, the only new twin prime pairs involve $$$p$$$. By keeping track of the counts of primes in the range, we can find new twin prime pairs by adding the counts of $$$p+2$$$ and $$$p-2$$$ to the answer, provided these are prime.
Similarly, when removing elements from either end, if the removed value is not prime, the answer remains unchanged. If it is prime, we subtract the counts of $$$p+2$$$ and $$$p-2$$$ to update the answer for the new range.
Step 4
If we know the answer for a query with range $$$l$$$ to $$$r$$$, we can calculate the answer for the following ranges in $$$O(1)$$$ time:
- Range $$$[l+1, r]$$$
- Range $$$[l, r-1]$$$
- Range $$$[l-1, r]$$$
- Range $$$[l, r+1]$$$
These conditions are exactly what is required for MO's algorithm. Using MO's algorithm, we can answer $$$q$$$ queries over an array of size $$$n$$$ in $$$O((n + q)\sqrt{n})$$$ time.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
// Precompute primes up to 1e6
const int MAXA = 1e6 + 5;
vector<bool> is_prime(MAXA, true);
is_prime[0] = is_prime[1] = false;
for (int i = 2; i * i < MAXA; i++) {
if (is_prime[i]) {
for (int j = i * i; j < MAXA; j += i) {
is_prime[j] = false;
}
}
}
vector<int> count(MAXA, 0);
while (t--) {
int n, q;
cin >> n >> q;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
vector<tuple<int,int,int>> queries(q);
for (int i = 0; i < q; i++) {
int l, r;
cin >> l >> r;
--l; --r; // 0-based indexing
queries[i] = {l, r, i};
}
int block_size = max(1, (int)sqrt(n));
sort(queries.begin(), queries.end(), [&](auto &x, auto &y) {
int bx = get<0>(x) / block_size, by = get<0>(y) / block_size;
if (bx != by) return bx < by;
return (bx & 1) ? get<1>(x) > get<1>(y) : get<1>(x) < get<1>(y);
});
vector<long long> ans(q);
int curr_l = 0, curr_r = -1;
long long res = 0;
auto add = [&](int x) {
if (!is_prime[x]) return;
res += 1LL * count[x-2] * (x-2 >= 0 && is_prime[x-2]);
res += 1LL * count[x+2] * (x+2 < MAXA && is_prime[x+2]);
count[x]++;
};
auto remove = [&](int x) {
if (!is_prime[x]) return;
count[x]--;
res -= 1LL * count[x-2] * (x-2 >= 0 && is_prime[x-2]);
res -= 1LL * count[x+2] * (x+2 < MAXA && is_prime[x+2]);
};
for (auto &[l, r, idx] : queries) {
while (curr_r < r) add(a[++curr_r]);
while (curr_r > r) remove(a[curr_r--]);
while (curr_l < l) remove(a[curr_l++]);
while (curr_l > l) add(a[--curr_l]);
ans[idx] = res;
}
for (long long x : ans) cout << x << '\n';
// Cleanup count array for the next testcase
for (int i = 0; i < n; i++) count[a[i]] = 0;
}
}
Time Complexity: The initial step of precomputing prime numbers using the Sieve of Eratosthenes takes $$$O(A \log \log A)$$$ time, where $$$A$$$ is $$$10^6 + 5$$$. Sorting the queries for Mo's algorithm requires $$$O(q \log q)$$$ time, where $$$q$$$ is the number of queries. Processing all queries using Mo's algorithm involves adding and removing elements at the ends of the current range. Each add or remove operation takes constant time, and the total number of operations across all queries is bounded by $$$O((n+q)\sqrt{n})$$$, where $$$n$$$ is the size of the array. Therefore, the overall time complexity per testcase is $$$O(A \log \log A + (n+q)\sqrt{n})$$$, which is efficient under the given constraints.
Space Complexity: The algorithm uses a prime array of size $$$A$$$ to keep track of which numbers are prime, and a count array of the same size to store the frequency of each number in the current range. Additionally, we store all queries and the results in separate arrays, requiring $$$O(q)$$$ space. Combining these components, the total space complexity is $$$O(A + q)$$$ extra space, which fits comfortably within the problem's memory limits.
L. Prefix and Suffix Sort
Step 1
It is important to note that first you have to sort the prefix of length $$$k$$$, and then sort the suffix of length $$$k$$$. This makes greedy approaches difficult to apply, so we should consider a different strategy that does not rely on greediness.
Step 2
Suppose we guess that the answer is $$$p$$$. We can check whether this is correct by sorting the first $$$p$$$ elements and the last $$$p$$$ elements, then verifying if the entire array becomes sorted. If $$$p$$$ is a valid answer, then any value larger than $$$p$$$ will also work. Since we are asked for the minimum $$$k$$$, we need to continue searching for smaller valid values. If $$$p$$$ is not valid, we need to look for larger values.
Step 3
The approach in Step 2 naturally leads to a binary search on the answer. The minimum possible $$$k$$$ is $$$1$$$ and the maximum is $$$n$$$, the length of the array. Therefore, we can perform a binary search in the range $$$[1, n]$$$ to efficiently find the smallest valid $$$k$$$.
def can_sort_with_k(a, k):
temp = a[:]
if k > 0:
temp[:k] = sorted(temp[:k])
temp[-k:] = sorted(temp[-k:])
return temp == sorted(a)
def min_k_to_sort(a):
n = len(a)
left, right = 0, n
while left <= right:
mid = (left + right) // 2
if can_sort_with_k(a, mid):
right = mid - 1
else:
left = mid + 1
return left
t = int(input())
for _ in range(t):
n = int(input())
a = list(map(int, input().split()))
print(min_k_to_sort(a))
Time Complexity: For a single test case of size $$$n$$$, each check creates a copy of the array and sorts the prefix and suffix of length $$$k$$$, which takes $$$O(k \log k + k \log k) = O(k \log k)$$$ time. In the worst case, $$$k$$$ can be up to $$$n$$$, so each check can take $$$O(n \log n)$$$ time. Binary search over possible $$$k$$$ values requires $$$O(\log n)$$$ checks. Therefore, for one test case, the overall time complexity is $$$O(n \log n \cdot \log n) = O(n (\log n)^2)$$$.
Space Complexity: For a single test case of size $$$n$$$, each call to the checker creates a temporary copy of the array, which requires $$$O(n)$$$ additional space. Other than that, storing the input array also requires $$$O(n)$$$ space. Therefore, the total space complexity per test case is $$$O(n)$$$.
M. Almaz and Ceremonial Mat
Step 1
For a rectangle with dimensions $$$n \times m$$$, the largest square that can fit inside the rectangle has side length equal to the smaller of $$$n$$$ and $$$m$$$. Therefore, if we let $$$k = \min(n, m)$$$, the area of the largest possible square mat is $$$k^2$$$.
t = int(input())
for _ in range(t):
n, m = map(int, input().split())
k = min(n, m)
print(k * k)
Time Complexity: For each test case, determining the minimum of two numbers takes $$$O(1)$$$ time, and computing the square also takes $$$O(1)$$$ time. Therefore, the total time complexity per test case is $$$O(1)$$$.
Space Complexity: The algorithm uses only a constant amount of extra space. No additional data structures are needed, so the space complexity per test case is also $$$O(1)$$$.
N. Head Value Sum
Step 1
If you play around with the formula and examine the binary representations of $$$a_i$$$ and $$$a_j$$$, you will notice that the formula simply implies that $$$a_i$$$ is a binary prefix of $$$a_j$$$. In other words, we can obtain $$$a_i$$$ by repeatedly floor-dividing $$$a_j$$$ by $$$2$$$.
Step 2
Instead of calculating $$$f(a_i)$$$ individually for all elements, focus on the contribution of each element to the total sum. For a specific element $$$a_i$$$, it contributes $$$h(a_i)$$$ to all numbers in the array that can be obtained by repeatedly floor-dividing $$$a_i$$$ by $$$2$$$. By keeping track of the counts of all elements, we can efficiently compute the total sum while also supporting updates to the array.
Step 3
Maintain two arrays: one array $$$c$$$ to count the number of occurrences of each unique value, and another array $$$p$$$ to track the sum of head values of all elements for which that value is a binary prefix. Then, for all unique values $$$x$$$ in the array, the total sum of $$$f(a_i)$$$ over all elements is equal to the sum of $$$c_x \cdot p_x$$$.
Step 4
When processing a query, dynamically update the answer by first removing the contribution of the old value and then adding the contribution of the new value. For each operation (removal or addition), repeatedly divide the value by $$$2$$$ and update its $$$c$$$ and $$$p$$$ values. Update the total sum accordingly by subtracting or adding the product $$$c_x \cdot p_x$$$.
Mod Reminder
Since the problem requires the answer modulo $$$998244353$$$, ensure that every arithmetic operation, including updates to $$$c$$$, $$$p$$$, and the total sum, is performed modulo $$$998244353$$$.
#include <bits/stdc++.h>
using namespace std;
const int MOD = 998244353;
unordered_map<int, int> inds;
vector<int> counts;
vector<long long> heads;
long long ans = 0;
int getH(int val) {
return 1 << (31 - __builtin_clz(val));
}
void update(int val, int sign) {
int h = getH(val);
for (int i = val; i > 0; i /= 2) {
if (inds.count(i)) {
int k = inds[i];
long long change = (long long)counts[k] * h % MOD;
if (sign == 1) {
ans = (ans + change) % MOD;
heads[k] = (heads[k] + h) % MOD;
} else {
ans = (ans - change + MOD) % MOD;
heads[k] = (heads[k] - h + MOD) % MOD;
}
}
}
int j = inds[val];
if (sign == 1) {
ans = (ans + heads[j]) % MOD;
counts[j]++;
} else {
ans = (ans - heads[j] + MOD) % MOD;
counts[j]--;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n, q;
cin >> n >> q;
vector<int> arr(n);
vector<pair<int, int>> queries(q);
inds.clear();
inds.reserve(n + q);
ans = 0;
int ind_counter = 0;
for (int i = 0; i < n; ++i) {
cin >> arr[i];
if (!inds.count(arr[i]))
inds[arr[i]] = ind_counter++;
}
for (int k = 0; k < q; ++k) {
cin >> queries[k].first >> queries[k].second;
queries[k].first--; // 0-based
if (!inds.count(queries[k].second))
inds[queries[k].second] = ind_counter++;
}
counts.assign(ind_counter, 0);
heads.assign(ind_counter, 0);
for (int x : arr)
update(x, 1);
cout << ans;
for (auto [idx, val] : queries) {
update(arr[idx], -1);
arr[idx] = val;
update(arr[idx], 1);
cout << " " << ans;
}
cout << "\n";
}
return 0;
}
Time Complexity: For each test case, the algorithm begins by reading the array and the queries, which takes $$$O(n + q)$$$ time. Assigning unique indices to all distinct values in the array and queries also takes $$$O(n + q)$$$ time. Next, the algorithm computes the initial total contribution by iterating over each element and repeatedly dividing it by 2, which takes $$$O(\log V)$$$ time per element, giving a total of $$$O(n \cdot \log V)$$$. During query processing, each query involves two operations: removing the contribution of the old value and adding the contribution of the new value. Each operation again takes $$$O(\log V)$$$ time, resulting in $$$O(q \cdot \log V)$$$ for all queries. Combining all steps, the total time complexity per test case is $$$O((n + q) \cdot \log V)$$$, where $$$V$$$ is the maximum element value in the input.
Space Complexity: The algorithm maintains arrays to track counts and contributions for each distinct value, which requires space proportional to the number of distinct values. It also stores the array and queries, and a mapping from each distinct value to its tracking index. Since the number of distinct values is at most $$$n + q$$$, the total space used per test case is $$$O(n + q)$$$.
O. Guards of Gojjam
- Problem Idea: abenezer_m54
- Problem Setter: eulmelk
Step 1
The province’s network of strongholds can be modeled as a graph. Since there are $$$n$$$ strongholds and $$$n-1$$$ roads, and all strongholds are reachable from any other, this graph is a tree. The task is to partition the nodes into connected groups of at most $$$k$$$ nodes each, such that the number of groups is minimized. Each group corresponds to a collection of strongholds assigned to single guard.
Step 2
Root the tree arbitrarily. A natural strategy is to process the tree from the leaves up to the root. At each node, decide whether to merge with children’s groups or start a new group. Each node can return the size of its current incomplete group to its parent, allowing the parent to decide whether to merge or keep the group separate.
Step 3
Claim: It is optimal to always merge smallest child groups first into the current node. Start by collecting sizes of child groups that are not yet complete. Merge the smallest groups iteratively until the current group reaches size $$$k$$$ or cannot accommodate more. If the current group reaches exactly $$$k$$$, return a size of $$$0$$$ to the parent; otherwise, return the current group size. Any leftover child groups that could not merge are counted as separate groups.
For a rigorous justification of this claim, refer to the "Why it works" section below.
Step 4
This approach can be implemented using Depth-First Search (DFS). For each node:
- Recursively call DFS on all children.
- Collect non-zero group sizes returned from children.
- Sort these sizes and merge starting from the smallest.
- Update the number of groups whenever a child group cannot merge or when the current group reaches $$$k$$$.
- Return the current group size modulo $$$k$$$ to the parent.
A global counter tracks the total number of groups formed. At the end of the DFS, this counter gives the minimum number of guards needed.
The First claim is: It is always optimal to consider the solution of the subtrees rooted at the children of the node, and use those solutions to find the best solution for the current node.
Assume for contradiction that at some node $$$q$$$ you make a suboptimal choice. Let the parent of $$$q$$$ be $$$p$$$. Let the optimal solution for the subtree rooted at $$$q$$$ require $$$g_q$$$ guards, and suppose the group containing $$$q$$$ has size $$$r_q$$$. Now suppose we choose a suboptimal strategy that uses $$$g_q' \gt g_q$$$ guards instead, and produces a group of size $$$r_q'$$$ containing $$$q$$$. We now analyze how this affects the solution at $$$p$$$.
Scenario A: At node $$$p$$$ there exists at least one group that can take the extra $$$r_q'$$$ nodes.
There are two cases:
- If $$$r_q' = k$$$, then this group is already full, so we do not need to assign a guard for it at $$$p$$$.
- If $$$r_q' \lt k$$$, then $$$p$$$ can merge this group into one of its existing groups, also without needing an extra guard.
In either case, the most $$$p$$$ can possibly “save” is one guard. But since we already paid at least one extra guard inside $$$q$$$ (because $$$g_q' \gt g_q$$$), the maximum possible net profit is $$$0$$$. So either we break even or we lose.
Scenario B: At node $$$p$$$ no group has enough space to take $$$r_q'$$$ nodes.
Again two cases:
- If $$$r_q' = k$$$, then this group forms a standalone full group, costing no new guard at $$$p$$$.
- If $$$r_q' \lt k$$$, then $$$p$$$ must assign a new guard for this group, because no existing group can absorb it.
Here too, the maximum possible improvement is $$$1$$$, but since $$$q$$$ already introduced at least one extra guard, the best net profit is again $$$0$$$. So we either break even or lose.
From both scenarios, the conclusion is the same: making a suboptimal choice at any node cannot result in a net improvement for its parent. In fact, it risks cascading losses all the way up to the root. Therefore, each node must use the optimal solution for its subtree and pass that upward.
The Second Claim is: We can collect the number of nodes in the group containing each child (after each child optimally solves its own subtree), and use those numbers to determine the optimal strategy for the current node.
Let $$$r_i$$$ be the size of the group that contains the $$$i$$$-th child. If $$$r_i = k$$$, this child’s group is already full and cannot be expanded, so it can be ignored. For the remaining values, we want to use as few guards as possible, which means we want to merge as many of these groups as we can.
A key observation is that all children are only connected through the current node. That means if we merge multiple child groups, the current node must be included in that same merged group. Since one node cannot belong to two different groups, we can only form a single merged group. All remaining child groups must stay separate.
To minimize the number of leftover groups, it is optimal to choose the smallest $$$r_i$$$ values first, because merging small components leaves more room to merge additional ones. Therefore, we sort the $$$r_i$$$ in increasing order, add them greedily to the current node’s group until adding another one would exceed $$$k$$$, and return the resulting group size (modulo $$$k$$$) to the parent. All unmerged child groups contribute one guard each, and the final group at the current node also contributes one guard.
#include <bits/stdc++.h>
using namespace std;
int n, k;
vector<vector<int>> tree;
int guards = 0;
int dfs(int node, int parent) {
vector<int> childGroups;
for (int child : tree[node]) {
if (child == parent) continue;
int sizeFromChild = dfs(child, node);
if (sizeFromChild > 0)
childGroups.push_back(sizeFromChild);
}
sort(childGroups.begin(), childGroups.end());
int currentSize = 1;
for (int sz : childGroups) {
if (currentSize + sz <= k) {
currentSize += sz;
} else {
guards++;
}
}
if (currentSize == k) {
guards++;
return 0;
} else {
return currentSize;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
cin >> n >> k;
tree.assign(n + 1, vector<int>());
for (int i = 0; i < n - 1; i++) {
int u, v;
cin >> u >> v;
tree[u].push_back(v);
tree[v].push_back(u);
}
guards = 0;
int leftover = dfs(1, 0);
if (leftover > 0) guards++; // assign guard to remaining nodes
cout << guards << "\n";
}
return 0;
}
Time Complexity: For a single test case, reading all $$$n$$$ strongholds and $$$n-1$$$ roads takes $$$O(n)$$$ time. The main computation is a depth-first search (DFS) over the tree. Each stronghold is visited exactly once, and for each stronghold, we merge the results from its children, which takes time proportional to the number of children. Since the total number of edges is $$$n-1$$$, the DFS merges across all strongholds also take $$$O(n)$$$ time. Therefore, the total time complexity for one test case is $$$O(n)$$$.
Space Complexity: For a single test case, storing the tree as an adjacency list requires $$$O(n)$$$ space. The recursion stack for DFS can grow up to the height of the tree, which in the worst case is $$$O(n)$$$. Temporary storage for collecting child group sizes is also bounded by $$$O(n)$$$ across all nodes. Thus, the total space complexity for one test case is $$$O(n)$$$.







