Alternating Groups III
HardPrompt
There are some red and blue tiles arranged circularly. You are given an array of integers colors and a 2D integers array queries.
The color of tile i is represented by colors[i]:
colors[i] == 0means that tileiis red.colors[i] == 1means that tileiis blue.
An alternating group is a contiguous subset of tiles in the circle with alternating colors (each tile in the group except the first and last one has a different color from its adjacent tiles in the group).
You have to process queries of two types:
queries[i] = [1, sizei], determine the count of alternating groups with sizesizei.queries[i] = [2, indexi, colori], changecolors[indexi]tocolori.
Return an array answer containing the results of the queries of the first type in order.
Note that since colors represents a circle, the first and the last tiles are considered to be next to each other.
Example 1:
Input: colors = [0,1,1,0,1], queries = [[2,1,0],[1,4]]
Output: [2]
Explanation:

First query:
Change colors[1] to 0.

Second query:
Count of the alternating groups with size 4:


Example 2:
Input: colors = [0,0,1,0,1,1], queries = [[1,3],[2,3,0],[1,5]]
Output: [2,0]
Explanation:

First query:
Count of the alternating groups with size 3:


Second query: colors will not change.
Third query: There is no alternating group with size 5.
Constraints:
4 <= colors.length <= 5 * 1040 <= colors[i] <= 11 <= queries.length <= 5 * 104queries[i][0] == 1orqueries[i][0] == 2- For all
ithat:queries[i][0] == 1:queries[i].length == 2,3 <= queries[i][1] <= colors.length - 1queries[i][0] == 2:queries[i].length == 3,0 <= queries[i][1] <= colors.length - 1,0 <= queries[i][2] <= 1
Approaches
3 approaches with complexity analysis and trade-offs.
This approach directly simulates the process described in the problem for each query. For a type 1 query asking for the count of alternating groups of a specific size, it iterates through every possible starting position in the circular array. For each start position, it then checks if the subsequent size tiles form an alternating sequence. This check involves another loop comparing adjacent tiles. For type 2 queries, it performs a simple update on the colors array.
Algorithm
- For each query of type 1,
[1, size], initialize a counter for alternating groups to zero. - Iterate through each possible starting tile index
ifrom0ton-1, wherenis the number of tiles. - For each starting index
i, check if the contiguous group of tiles of lengthsizeforms an alternating group. - To check for an alternating group, iterate from
j = 0tosize-2and verify thatcolors[(i+j)%n] != colors[(i+j+1)%n]. The modulo operator%nhandles the circular nature of the tiles. - If all adjacent pairs in the group have different colors, increment the counter.
- After checking all starting positions, the value of the counter is the result for the query.
- For each query of type 2,
[2, index, color], simply update the color of the tile at the given index:colors[index] = color.
Walkthrough
The brute-force method is the most straightforward way to solve the problem. It processes each query independently without any pre-computation or auxiliary data structures to speed up future queries.
For a type 1 query [1, size], we iterate through all n possible starting positions. For each starting position i, we then verify if the group of tiles colors[i], colors[(i+1)%n], ..., colors[(i+size-1)%n] is alternating. This verification takes O(size) time. Thus, a single type 1 query takes O(n * size) time.
For a type 2 query [2, index, color], the update is a direct array modification, which takes O(1) time.
class Solution { public int[] resultsArray(int[] colors, int[][] queries) { java.util.List<Integer> results = new java.util.ArrayList<>(); int n = colors.length; for (int[] query : queries) { if (query[0] == 1) { int size = query[1]; int count = 0; for (int i = 0; i < n; i++) { boolean isAlternating = true; if (size > 1) { for (int j = 0; j < size - 1; j++) { if (colors[(i + j) % n] == colors[(i + j + 1) % n]) { isAlternating = false; break; } } } if (isAlternating) { count++; } } results.add(count); } else { int index = query[1]; int newColor = query[2]; colors[index] = newColor; } } return results.stream().mapToInt(i -> i).toArray(); }}Complexity
Time
O(Q * n * k), where `Q` is the number of queries, `n` is the number of colors, and `k` is the size in a query. In the worst case, `k` can be close to `n`, making the complexity O(Q * n^2).
Space
O(1) (excluding the storage for the results array).
Trade-offs
Pros
Simple to understand and implement.
Requires minimal extra space.
Cons
Extremely inefficient for type 1 queries, leading to a Time Limit Exceeded error on larger inputs.
Repeatedly recalculates the same information for overlapping groups in different queries.
This approach improves upon the brute-force method by introducing an intermediate representation. Instead of checking tile colors directly, we first compute a boolean array, let's call it good, which marks whether each adjacent pair of tiles (including the wrap-around pair) has different colors. An alternating group of size k is then equivalent to finding a contiguous block of k-1 true values in the good array.
For each type 1 query, we build the good array from scratch and then scan it to count the blocks of trues. This avoids the nested loop structure of the brute-force approach for a single query, reducing its complexity.
Algorithm
- An alternating group of size
kcorresponds tok-1consecutive adjacent pairs of tiles with different colors. - Define a boolean array
goodof sizen, wheregood[i]is true ifcolors[i] != colors[(i+1)%n], and false otherwise. - For a type 1 query
[1, k], first construct thisgoodarray in O(n) time. - Traverse the circular
goodarray to find all contiguous blocks oftruevalues. - For each block of
trues of lengthL, it can formmax(0, L - (k-1) + 1)alternating groups of sizek. - Sum these counts over all blocks to get the total for the query.
- For a type 2 query, update the
colorsarray in O(1).
Walkthrough
The core idea is to transform the problem from checking colors to checking properties of adjacent pairs. We define good[i] = (colors[i] != colors[(i+1)%n]).
For a type 1 query [1, k]:
- We build the
goodarray in O(n) time. - We then iterate through the
goodarray to find the lengths of all contiguous blocks oftrues. To handle the circularity, we can find afalseentry and 'unroll' the array from there, or iterate through2nelements while being careful not to double-count. - A block of
Lconsecutivetrues corresponds to an alternating sequence ofL+1tiles. This sequence containsL - (k-1) + 1(orL-k+2) alternating groups of sizek. - We sum these contributions to get the final answer. This step also takes O(n).
This makes each type 1 query O(n). Type 2 queries are still O(1).
class Solution { public int[] resultsArray(int[] colors, int[][] queries) { java.util.List<Integer> results = new java.util.ArrayList<>(); int n = colors.length; for (int[] query : queries) { if (query[0] == 1) { int k = query[1]; boolean[] good = new boolean[n]; for (int i = 0; i < n; i++) { good[i] = (colors[i] != colors[(i + 1) % n]); } int totalCount = 0; int currentRun = 0; // To handle circularity, find a false point to break the circle int startNode = 0; for(int i=0; i<n; i++){ if(!good[i]){ startNode = i + 1; break; } } // If all are good, special case if(startNode == 0 && good[0]){ totalCount = n; } else { for (int i = 0; i < n; i++) { int idx = (startNode + i) % n; if (good[idx]) { currentRun++; } else { if (currentRun > 0) { totalCount += Math.max(0, currentRun - (k - 1) + 1); } currentRun = 0; } } if (currentRun > 0) { totalCount += Math.max(0, currentRun - (k - 1) + 1); } } results.add(totalCount); } else { colors[query[1]] = query[2]; } } return results.stream().mapToInt(i -> i).toArray(); }}Complexity
Time
O(Q * n). This is an improvement but still not sufficient for the given constraints.
Space
O(n) for storing the `good` array for each query.
Trade-offs
Pros
More efficient than the brute-force approach.
Reduces the complexity of a single query from O(n*k) to O(n).
Cons
Still too slow for the given constraints as it recomputes the
goodarray and block counts for every type 1 query.The O(n) work per query is the bottleneck.
This optimal approach uses advanced data structures to handle both query types efficiently. The key insight is that updates are local, but queries are global. We need a way to quickly assess the global state after a local change.
We maintain the set of 'breaks' (where adjacent colors are the same) in a TreeSet. These breaks define segments of alternating colors. The lengths of these segments are what determine the answer to type 1 queries. We use Fenwick Trees to maintain an aggregate of these lengths, allowing us to answer count queries quickly.
When a color is updated, at most two good array positions change. This may create or remove a break. We update our TreeSet of breaks, calculate the change in segment lengths, and propagate these changes to the Fenwick Trees. Both updates and queries can then be handled in logarithmic time.
Algorithm
- Define
good[i]astrueifcolors[i] != colors[(i+1)%n]. A point wheregood[i]isfalseis a 'break'. - Use a
TreeSetto store the indices of all breaks. This allows for efficient lookup of adjacent breaks. - The breaks divide the circular
goodarray into segments of consecutivetrues. The length of these segments can be calculated from the indices of adjacent breaks. - Use two Fenwick Trees (or Binary Indexed Trees) to maintain counts of these segments.
ftCountstores the number of segments of a certain lengthL, andftSumstores the sum ofLfor all segments of lengthL. - Initialization: Build the
goodarray, populate thebreaksTreeSet, and initialize the Fenwick Trees based on the initial segment lengths. This takes O(n log n). - Type 1 Query
[1, k]: Ifbreaksis empty, the whole circle is alternating, so the answer isn. Otherwise, use the Fenwick Trees to calculatesum_{L=k-1 to n-1} C(L) * (L - k + 2)in O(log n) time, whereC(L)is the count of segments of lengthL. - Type 2 Query
[2, index, color]: An update tocolors[index]affectsgood[index]andgood[index-1]. If agoodvalue flips, it either creates a new break or removes an existing one. This corresponds to splitting or merging segments. Update thebreaksTreeSetand the Fenwick Trees accordingly. This operation takes O(log n) time.
Walkthrough
This approach provides an efficient solution by maintaining the structure of alternating segments across updates.
Data Structures:
good[]: A boolean array wheregood[i]indicates ifcolors[i]andcolors[(i+1)%n]differ.breaks: Ajava.util.TreeSet<Integer>storing indicesiwheregood[i]is false.ftCount,ftSum: Two Fenwick Trees.ftCountstores counts of linear segments by length, andftSumstores the sum of lengths.
Logic:
-
Query
[1, k]: An alternating group of sizekrequires a run ofk-1trues in thegoodarray. A linear segment ofLtrues containsmax(0, L - k + 2)such groups. The total count is the sum of these contributions over all segments.- If
breaksis empty, the entire circle is one alternating group of lengthn. The answer isn(sincek <= n). - Otherwise, all segments are linear. The total count is
sum_{L=k-1..n-1} count(L) * (L - k + 2). This can be rewritten as(sum L*count(L)) - (k-2)*(sum count(L)), which can be computed in O(log n) using range queries on our two Fenwick Trees.
- If
-
Update
[2, index, color]: Changingcolors[index]affectsgood[index]andgood[(index-1+n)%n]. For each of these positionsp:- If
good[p]flips fromtruetofalse, a new break is created atp. This splits a larger segment into two smaller ones. We find the old segment's length usingbreaks.lower(p)andbreaks.higher(p), remove its contribution from the Fenwick Trees, and add the contributions of the two new, smaller segments. - If
good[p]flips fromfalsetotrue, a break atpis removed. This merges two adjacent segments. We remove their contributions and add the contribution of the new, larger merged segment.
- If
Each update involves a few lookups in the TreeSet and updates to the Fenwick Trees, all taking O(log n) time.
// FenwickTree class implementation is standardclass FenwickTree { long[] tree; int size; public FenwickTree(int size) { this.size = size; this.tree = new long[size]; } public void add(int i, long delta) { while (i < size) { tree[i] += delta; i += i & -i; } } public long query(int i) { long sum = 0; while (i > 0) { sum += tree[i]; i -= i & -i; } return sum; }} class Solution { int n; int[] colors; boolean[] good; java.util.TreeSet<Integer> breaks; FenwickTree ftCount, ftSum; public int[] resultsArray(int[] colors, int[][] queries) { this.n = colors.length; this.colors = colors.clone(); this.good = new boolean[n]; this.breaks = new java.util.TreeSet<>(); this.ftCount = new FenwickTree(n + 2); this.ftSum = new FenwickTree(n + 2); for (int i = 0; i < n; i++) { good[i] = (this.colors[i] != this.colors[(i + 1) % n]); if (!good[i]) breaks.add(i); } if (!breaks.isEmpty()) { Integer first = breaks.first(), prev = breaks.last(); for (Integer curr : breaks) { addSegment(getLength(prev, curr)); prev = curr; } } java.util.List<Integer> resultList = new java.util.ArrayList<>(); for (int[] q : queries) { if (q[0] == 1) { resultList.add((int) countGroups(q[1])); } else { updateColor(q[1], q[2]); } } return resultList.stream().mapToInt(i -> i).toArray(); } private int getLength(int b1, int b2) { if (b1 < b2) return b2 - b1 - 1; return n - b1 - 1 + b2; } private void addSegment(int len) { if (len <= 0) return; ftCount.add(len + 1, 1); ftSum.add(len + 1, len); } private void removeSegment(int len) { if (len <= 0) return; ftCount.add(len + 1, -1); ftSum.add(len + 1, -len); } private long countGroups(int k) { if (k > n) return 0; if (breaks.isEmpty()) return n; int kPrime = k - 1; long totalSumL = ftSum.query(n + 1) - ftSum.query(kPrime); long totalCount = ftCount.query(n + 1) - ftCount.query(kPrime); return totalSumL - (long) (k - 2) * totalCount; } private void updateColor(int index, int newColor) { if (colors[index] == newColor) return; colors[index] = newColor; updateGood((index - 1 + n) % n); updateGood(index); } private void updateGood(int p) { boolean newGood = (colors[p] != colors[(p + 1) % n]); if (good[p] == newGood) return; good[p] = newGood; if (newGood) { // 0 -> 1: removing a break Integer bPrev = breaks.lower(p); if (bPrev == null) bPrev = breaks.last(); Integer bNext = breaks.higher(p); if (bNext == null) bNext = breaks.first(); breaks.remove(p); removeSegment(getLength(bPrev, p)); removeSegment(getLength(p, bNext)); if (breaks.isEmpty()) return; // All good now, handled by countGroups addSegment(getLength(bPrev, bNext)); } else { // 1 -> 0: adding a break if (breaks.isEmpty()) { // Was all good breaks.add(p); addSegment(n - 1); return; } Integer bPrev = breaks.lower(p); if (bPrev == null) bPrev = breaks.last(); Integer bNext = breaks.higher(p); if (bNext == null) bNext = breaks.first(); removeSegment(getLength(bPrev, bNext)); addSegment(getLength(bPrev, p)); addSegment(getLength(p, bNext)); breaks.add(p); } }}Complexity
Time
O(n log n + Q log n). Initialization takes O(n log n) to build the `TreeSet`. Each query and update takes O(log n).
Space
O(n) to store the `good` array, the `breaks` `TreeSet`, and the Fenwick Trees.
Trade-offs
Pros
Highly efficient, with logarithmic time complexity for both updates and queries.
Scales well for large inputs, passing all constraints.
Maintains the state of the system incrementally, avoiding redundant calculations.
Cons
Significantly more complex to implement correctly, especially the logic for handling circularity and updating segments.
Higher constant factor in runtime compared to simpler approaches, though asymptotically superior.
Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.