Topics › Strings › Hard String Problems
Count and say
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
The count-and-say sequence is a sequence of digit strings. The first term is "1". Every later term is produced by reading the previous term aloud: split it into maximal runs of equal digits, and for each run, from left to right, write the length of the run followed by the digit. Given n ≥ 1, return the n-th term.
Constraints: 1 ≤ n ≤ 30.
The first terms are:
1: 1
2: 11 // "one 1"
3: 21 // "two 1s"
4: 1211 // "one 2, one 1"
5: 111221 // "one 1, one 2, two 1s"
6: 312211 // "three 1s, two 2s, one 1"
7: 13112221
8: 1113213211
Example 1
n = 4
answer = "1211" // term 3 is "21": one 2, then one 1
Example 2
n = 1
answer = "1" // the seed itself; nothing is read aloud
The core operation: run-length encoding
There is no shortcut formula for the n-th term, so every solution generates the terms one after another. The real work is one step: turning a string into its “spoken” form. That step is run-length encoding: walk through the string, measure each block of equal characters, and emit count then character.
Take "111221". Its runs are 111, 22, 1, so the next term is "3" + "1" then "2" + "2" then "1" + "1", which is "312211".
Two observations shape the code:
- A run ends either when the next character is different or when the string ends. Forgetting the second case drops the last run, the single most common bug in this problem.
- A count is written as a number, not a single digit. For this sequence the count never exceeds
3: four equal digits in a row would mean the previous term had two adjacent runs of the same digit, which is impossible because runs are maximal. Still, appending theintcount, rather than doing(char) ('0' + count), stays correct regardless.
Both approaches below perform this step n - 1 times. They differ in how they build strings, and that turns out to matter.
Approach 1: Brute force, recursion with string concatenation
Intuition. The definition is recursive: term n is the spoken form of term n - 1. So compute term n - 1 recursively, then scan it run by run, gluing the pieces onto a result String with +. This mirrors the definition word for word.
The hidden cost is that Java strings are immutable. result = result + ... allocates a new string and copies everything built so far, so building a string of length L piece by piece costs O(L^2) character copies.
Algorithm.
- If
n == 1, return"1". - Compute
prev, the term forn - 1. - Starting at
i = 0, advancejwhileprev[j] == prev[i]. The run has lengthj - i. - Append the length and the digit to
result, seti = j, and repeat until the end.
static String bruteForce(int n) {
if (n == 1) return "1";
String prev = bruteForce(n - 1);
String result = "";
int i = 0;
while (i < prev.length()) {
int j = i;
while (j < prev.length() && prev.charAt(j) == prev.charAt(i)) j++;
result = result + (j - i) + prev.charAt(i); // count of the run, then its digit
i = j;
}
return result;
}
Complexity. Let L_k be the length of term k. Step k makes up to L_k concatenations, each copying up to L_k characters, so time is O(L_k^2) per step and O(L_n^2) overall in the worst case. Space O(L_n) for the strings plus O(n) recursion depth.
How big is L_n? Conway proved that the length grows by a factor of about 1.3036 per term, so it is exponential in n. Term 30 already has 4462 characters, which is why the problem caps n at 30.
Approach 2: Optimal, iteration with a StringBuilder
Intuition. Keep the same idea but remove both overheads. A loop from 2 to n replaces the recursion, and a StringBuilder replaces repeated concatenation: appending to it is amortised O(1), so building each term costs time proportional to its length.
The scanning loop is written so the “run ended” logic lives in one place. Index i runs from 1 to cur.length() inclusive. If cur[i] exists and equals cur[i - 1], the run continues and the count grows. Otherwise the run ending at i - 1 is flushed. Letting i reach cur.length() is what flushes the final run without a separate statement after the loop.
Algorithm.
- Set
cur = "1". - Repeat for
step = 2 .. n:- Set
count = 1and scani = 1 .. len. - If
i < lenandcur[i] == cur[i - 1], incrementcount. - Otherwise append
countandcur[i - 1]tonext, and resetcount = 1. - Set
cur = next.
- Set
- Return
cur.
static String optimal(int n) {
String cur = "1";
for (int step = 2; step <= n; step++) {
StringBuilder next = new StringBuilder();
int count = 1;
for (int i = 1; i <= cur.length(); i++) {
if (i < cur.length() && cur.charAt(i) == cur.charAt(i - 1)) {
count++; // the run continues
} else {
next.append(count).append(cur.charAt(i - 1)); // the run ended at i - 1
count = 1;
}
}
cur = next.toString();
}
return cur;
}
Dry run from term 5 ("111221") to term 6:
| i | cur[i - 1] | cur[i] | action | next so far |
|---|---|---|---|---|
| 1 | 1 | 1 | count = 2 | |
| 2 | 1 | 1 | count = 3 | |
| 3 | 1 | 2 | flush “31” | 31 |
| 4 | 2 | 2 | count = 2 | 31 |
| 5 | 2 | 1 | flush “22” | 3122 |
| 6 | 1 | end | flush “11” | 312211 |
Complexity. Time O(L_2 + L_3 + ... + L_n). Because the lengths grow geometrically, this sum is a constant multiple of L_n, so the total is O(L_n), linear in the size of the answer. No algorithm can beat that, since it must at least write the answer. Space O(L_n) for the current and next terms.
Edge cases and pitfalls
n = 1. Return"1"with no processing. The iterative loop handles it naturally because it runs zero times.- The last run. Every term ends with a run that is not followed by a different digit. Flush it explicitly, or let the index run one past the end as in the optimal code.
- Strings in a loop.
String +=inside a loop is the classic hidden quadratic. UseStringBuilderwhenever a string is built piece by piece. - Counts as numbers. Append the count with
append(int). Converting with(char) (count + '0')works here only because counts stay at most3. - Growth. Do not try
n = 100: the term would have hundreds of billions of characters. The constraintn ≤ 30is part of the problem’s design. - Recursion depth. The brute force recurses
nlevels, which is harmless forn ≤ 30but another reason to prefer the loop.
Related problems
- Encode and Decode Strings is another exercise in designing a string encoding and parsing it back.
- Sort Characters by Frequency counts character occurrences, the unordered cousin of counting runs.
- Longest Repeating Character Replacement reasons about runs of equal characters inside a sliding window.
- String to Integer (atoi) practises careful digit-by-digit string scanning.