3773. Maximum Number of Equal Length Runs π
Problem Description
You are given a string s consisting of lowercase English letters.
A run in s is a non-empty substring made up of equal letters that cannot be extended any further (meaning the characters immediately before and after the run are different or don't exist). For example, the runs in "hello" are "h", "e", "ll", and "o".
You are allowed to select runs that all have the same length in s.
Your task is to return an integer representing the maximum number of runs you can select in s.
In other words, you need to group all the runs by their length, and find which length appears the most often. The count of runs sharing that most common length is your answer.
How We Pick the Algorithm
Why Hash Table / Counting?
This problem maps to Hash Table / Counting through a short path in the full flowchart.
Counting frequencies of run lengths across the string to find the most common length maps directly to hash table counting.
Open in FlowchartIntuition
The key observation is that we need to find runs sharing the same length and pick as many of them as possible. Since selected runs must all have an identical length, the problem naturally turns into a counting question: among all the runs in s, which specific length appears most frequently?
To solve this, we first need to break the string into its individual runs. A run ends whenever the current character differs from the next one, so we can scan through s and group together consecutive equal characters. Each such group is one run, and we record its length.
Once we know the length of every run, the answer becomes clear. If we count how many runs there are for each possible length, the largest of these counts tells us the maximum number of runs we can select. This is because choosing the most common length gives us the most runs that share an equal length.
So the whole idea reduces to two simple steps: find the length of each run, then count occurrences of each length and return the highest count.
Solution Approach
Solution 1: Hash Table
We use a hash table cnt to record the number of occurrences of each run length. We traverse the string s, and for each run, we calculate its length m and increment cnt[m] by 1. Finally, the answer is the maximum value in cnt.
In the implementation, we rely on the groupby function to split the string into consecutive groups of equal characters. Each group produced by groupby corresponds to exactly one run. For every group g, we compute its length using len(list(g)), which gives us the run length m, and then we do cnt[m] += 1 to count how many runs have that length.
After processing all runs, the hash table cnt holds the frequency of each distinct run length. We simply return max(cnt.values()), which is the largest count and therefore the maximum number of runs that share the same length.
The time complexity is O(n), and the space complexity is O(n), where n is the length of the string s. This is because we scan the string once and the hash table stores at most O(n) distinct run lengths.
Example Walkthrough
Let's trace through the solution approach using the string s = "aaabbbccd".
Step 1: Break the string into runs using groupby
Scanning through s and grouping consecutive equal characters, we get the following runs:
| Run | Characters | Length m |
|---|---|---|
| 1 | "aaa" | 3 |
| 2 | "bbb" | 3 |
| 3 | "cc" | 2 |
| 4 | "d" | 1 |
So the string splits into 4 runs with lengths [3, 3, 2, 1].
Step 2: Count occurrences of each length in the hash table cnt
As we process each run, we increment cnt[m] by 1:
- Run
"aaa"β length3βcnt = {3: 1} - Run
"bbb"β length3βcnt = {3: 2} - Run
"cc"β length2βcnt = {3: 2, 2: 1} - Run
"d"β length1βcnt = {3: 2, 2: 1, 1: 1}
Step 3: Return the maximum count
Now the hash table tells us how many runs share each length:
- Length
3appears 2 times - Length
2appears 1 time - Length
1appears 1 time
The largest value among cnt.values() is 2, corresponding to length 3.
Result: We can select the two runs "aaa" and "bbb" (both of length 3), giving us a maximum of 2 runs. So the answer is max(cnt.values()) = 2.
This confirms the core idea: by grouping runs by length and picking the most frequent length, we maximize the number of equal-length runs we can select.
Solution Implementation
1from collections import Counter
2from itertools import groupby
3
4
5class Solution:
6 def maxSameLengthRuns(self, s: str) -> int:
7 # Counter to record how many runs share the same length
8 # key -> length of a consecutive run of identical characters
9 # value -> number of runs that have this length
10 length_count = Counter()
11
12 # groupby splits the string into consecutive groups of identical chars.
13 # The first returned value (the character) is ignored with "_",
14 # and "group" is the iterator over that run's characters.
15 for _, group in groupby(s):
16 run_length = len(list(group)) # compute the length of this run
17 length_count[run_length] += 1 # tally one more run of this length
18
19 # Return the highest frequency among all run lengths,
20 # i.e. the maximum number of runs that share the same length.
21 return max(length_count.values())
221class Solution {
2 /**
3 * Finds the maximum number of consecutive-character runs that share the
4 * same length within the given string.
5 *
6 * Approach:
7 * 1. Scan the string and split it into maximal "runs" of identical
8 * characters (e.g. "aaabb" -> runs "aaa" and "bb").
9 * 2. For each run, record its length in a frequency map.
10 * 3. Track the highest frequency seen for any single length, which is
11 * the answer.
12 *
13 * @param s the input string
14 * @return the maximum count of runs having the same length
15 */
16 public int maxSameLengthRuns(String s) {
17 // Maps a run length -> how many runs of that length have appeared.
18 Map<Integer, Integer> lengthCount = new HashMap<>();
19 int maxRuns = 0;
20 int length = s.length();
21
22 // Iterate over the string, advancing run by run.
23 for (int start = 0; start < length; ) {
24 // Extend 'end' while the characters stay the same as s[start].
25 int end = start + 1;
26 while (end < length && s.charAt(end) == s.charAt(start)) {
27 ++end;
28 }
29
30 // Compute the current run's length.
31 int runLength = end - start;
32
33 // Increment the count for this run length and update the maximum.
34 int updatedCount = lengthCount.merge(runLength, 1, Integer::sum);
35 maxRuns = Math.max(maxRuns, updatedCount);
36
37 // Move to the start of the next run.
38 start = end;
39 }
40
41 return maxRuns;
42 }
43}
441class Solution {
2public:
3 int maxSameLengthRuns(string s) {
4 // Map from run length -> number of runs that have exactly this length
5 unordered_map<int, int> lengthCount;
6
7 // Tracks the maximum number of runs sharing the same length
8 int maxRuns = 0;
9
10 int n = s.size();
11
12 // Iterate over the string, processing one consecutive run at a time
13 for (int start = 0; start < n;) {
14 // Advance 'end' until the character differs from s[start]
15 int end = start + 1;
16 while (end < n && s[end] == s[start]) {
17 ++end;
18 }
19
20 // Length of the current run of identical characters
21 int runLength = end - start;
22
23 // Increment the count for this run length and update the answer
24 maxRuns = max(maxRuns, ++lengthCount[runLength]);
25
26 // Move to the start of the next run
27 start = end;
28 }
29
30 return maxRuns;
31 }
32};
331/**
2 * Finds the maximum number of consecutive-character "runs" that share
3 * the same length within the string.
4 *
5 * A "run" is a maximal block of identical consecutive characters.
6 * For example, in "aabbb" there are two runs: "aa" (length 2) and "bbb" (length 3).
7 *
8 * @param s - The input string to scan.
9 * @returns The highest frequency among all run lengths.
10 */
11function maxSameLengthRuns(s: string): number {
12 // Maps a run length to how many runs have that exact length.
13 const lengthCount: Record<number, number> = {};
14 const length = s.length;
15 let answer = 0;
16
17 // Iterate over the string; `start` marks the beginning of each run.
18 for (let start = 0; start < length; ) {
19 // Advance `end` until the character differs, capturing the full run.
20 let end = start + 1;
21 while (end < length && s[end] === s[start]) {
22 ++end;
23 }
24
25 // Compute the current run's length.
26 const runLength = end - start;
27
28 // Record this run length and update its frequency.
29 lengthCount[runLength] = (lengthCount[runLength] || 0) + 1;
30
31 // Track the maximum frequency seen so far.
32 answer = Math.max(answer, lengthCount[runLength]);
33
34 // Jump to the start of the next run.
35 start = end;
36 }
37
38 return answer;
39}
40Time and Space Complexity
-
Time Complexity:
O(n), wherenis the length of the strings. Thegroupbyfunction iterates through the entire string once to form consecutive groups, which takesO(n)time. For each group,len(list(g))consumes the group's elements, and the total number of elements consumed across all groups equalsn, so this also amounts toO(n)in total. Finally,max(cnt.values())traverses the counter, whose size is at most the number of groups (bounded byn), takingO(n)time. Therefore, the overall time complexity isO(n). -
Space Complexity:
O(n), wherenis the length of the strings. TheCounterobjectcntstores the frequencies of distinct run lengths, and in the worst case (e.g., all distinct run lengths) it can hold up toO(n)entries. Additionally,list(g)temporarily holds the elements of a single group, which is also bounded byO(n). Hence, the overall space complexity isO(n).
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Calling max() on an Empty Counter
The most common mistake is assuming length_count is always non-empty when calling max(length_count.values()). If the input string s is empty (""), then groupby produces no groups, leaving the Counter empty. Calling max() on an empty sequence raises a ValueError: max() arg is an empty sequence, crashing the program.
Solution: Guard against the empty case by either checking the Counter first or providing a default value to max():
from collections import Counter
from itertools import groupby
class Solution:
def maxSameLengthRuns(self, s: str) -> int:
length_count = Counter()
for _, group in groupby(s):
run_length = len(list(group))
length_count[run_length] += 1
# Use default=0 so an empty string safely returns 0
return max(length_count.values(), default=0)
Pitfall 2: Exhausting the groupby Iterator Before Measuring It
groupby yields lazy iterators for each group, and these iterators share the same underlying position in the string. A subtle bug arises if you try to inspect the group in a way that advances it, or if you reorder operations. For example, the following loses the data:
for char, group in groupby(s):
# WRONG: advancing the outer groupby invalidates the inner group iterator
if some_condition(char):
continue
run_length = len(list(group)) # may now be 0 or incomplete
If you advance the outer groupby (e.g., by continuing to the next iteration) before consuming group, the inner iterator becomes invalid. The same problem occurs if you store all groups in a list first:
groups = list(groupby(s)) # stores (char, iterator) pairs
for char, group in groups:
run_length = len(list(group)) # WRONG: all iterators are now empty -> length 0
Solution: Always consume each group iterator immediately within the loop body before moving to the next group. The original solution does this correctly with len(list(group)) right inside the loop. If you must materialize groups, capture their lengths at iteration time:
# Correct: measure length immediately while the iterator is still valid
run_lengths = [len(list(group)) for _, group in groupby(s)]
length_count = Counter(run_lengths)
Pitfall 3: Manual Run-Counting Off-by-One Errors
Developers who avoid groupby and write a manual scan often mishandle the last run. A typical loop increments a counter while characters match and finalizes a run only when a different character appearsβforgetting to finalize the final run after the loop ends:
# WRONG: the last run is never recorded
count = 1
for i in range(1, len(s)):
if s[i] == s[i - 1]:
count += 1
else:
length_count[count] += 1
count = 1
# missing: length_count[count] += 1 for the final run
Solution: After the loop, explicitly record the trailing run (and handle the empty-string edge case):
def maxSameLengthRuns(self, s: str) -> int:
if not s:
return 0
length_count = Counter()
count = 1
for i in range(1, len(s)):
if s[i] == s[i - 1]:
count += 1
else:
length_count[count] += 1
count = 1
length_count[count] += 1 # finalize the last run
return max(length_count.values())
Using groupby is preferable precisely because it eliminates these boundary-handling errors automatically.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapThe three-steps of Depth First Search are:
- Identify states;
- Draw the state-space tree;
- DFS on the state-space tree.
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity describes how the time needed
Want a Structured Path to Master System Design Too? Donβt Miss This!