Count Complete Substrings
HardPrompt
You are given a string word and an integer k.
A substring s of word is complete if:
- Each character in
soccurs exactlyktimes. - The difference between two adjacent characters is at most
2. That is, for any two adjacent charactersc1andc2ins, the absolute difference in their positions in the alphabet is at most2.
Return the number of complete substrings of word.
A substring is a non-empty contiguous sequence of characters in a string.
Example 1:
Input: word = "igigee", k = 2
Output: 3
Explanation: The complete substrings where each character appears exactly twice and the difference between adjacent characters is at most 2 are: igigee, igigee, igigee.Example 2:
Input: word = "aaabbbccc", k = 3
Output: 6
Explanation: The complete substrings where each character appears exactly three times and the difference between adjacent characters is at most 2 are: aaabbbccc, aaabbbccc, aaabbbccc, aaabbbccc, aaabbbccc, aaabbbccc.
Constraints:
1 <= word.length <= 105wordconsists only of lowercase English letters.1 <= k <= word.length
Approaches
3 approaches with complexity analysis and trade-offs.
The most straightforward approach is to check every single substring of the given word. We can generate all substrings and, for each one, verify if it meets the two conditions for being 'complete': the adjacency character difference and the exact frequency count.
Algorithm
- Initialize a counter for complete substrings to zero.
- Generate all possible substrings of the input
wordusing two nested loops. The outer loop iterates through the starting indexi, and the inner loop iterates through the ending indexj. - For each substring
s = word.substring(i, j + 1): a. Check Adjacency Condition: Iterate from the first to the second-to-last character ofs. Ifabs(s.charAt(p) - s.charAt(p + 1)) > 2for anyp, the substring is not complete. Continue to the next substring. b. Check Frequency Condition: If the adjacency condition holds, create a frequency map (e.g., an array of size 26) for the characters ins. c. Iterate through the frequency map. If any character has a frequency that is non-zero but not equal tok, the substring is not complete. d. If both conditions are satisfied, increment the counter. - Return the final count.
Walkthrough
This method involves three nested loops. The first two loops define the boundaries of a substring. The third loop (or loops) is used to validate the substring against the problem's conditions. While simple to conceptualize, its performance is very poor for large inputs.
class Solution { public int countCompleteSubstrings(String word, int k) { int n = word.length(); int count = 0; for (int i = 0; i < n; i++) { for (int j = i; j < n; j++) { String sub = word.substring(i, j + 1); if (isComplete(sub, k)) { count++; } } } return count; } private boolean isComplete(String s, int k) { // Check adjacency condition for (int i = 0; i < s.length() - 1; i++) { if (Math.abs(s.charAt(i) - s.charAt(i + 1)) > 2) { return false; } } // Check frequency condition int[] freq = new int[26]; for (char c : s.toCharArray()) { freq[c - 'a']++; } for (int f : freq) { if (f > 0 && f != k) { return false; } } return true; }}Complexity
Time
O(N^3) - There are O(N^2) substrings. For each substring of average length O(N), we perform a check that takes O(N) time. This results in a total time complexity of O(N^3).
Space
O(1) - The space used for the frequency map is constant (size 26).
Trade-offs
Pros
Simple to understand and implement.
Correct for small input sizes.
Cons
Extremely inefficient due to its cubic time complexity.
Will result in a 'Time Limit Exceeded' error for the given constraints (
Nup to 10^5).
A significant optimization comes from realizing that the adjacency condition (abs(c1 - c2) <= 2) allows us to partition the problem. We can split the word into chunks where this condition holds for all adjacent characters. Then, we only need to search for complete substrings within these chunks. For each chunk, we can use a more optimized O(M^2) approach (where M is the chunk length) instead of O(M^3).
Algorithm
- Split into Chunks: First, iterate through the
wordand split it into smaller chunks wherever two adjacent charactersword[i]andword[i+1]have a differenceabs(word[i] - word[i+1]) > 2. Any complete substring must be entirely contained within one of these chunks. - Process Each Chunk: For each chunk, find the number of complete substrings and add it to a total count.
- Find Substrings in a Chunk: To find complete substrings within a chunk
s: a. Iterate through each possible starting indexiofs. b. For eachi, start a new sliding window. Use a second pointerjto expand the window to the right, fromito the end of the chunk. c. Maintain a frequency map for the characters in the current windows[i...j]. d. Asjadvances, update the frequency map. If any character's count exceedsk, this window and any further extensions fromiare invalid, so we can break the inner loop overj. e. At each stepj, check if the substring is complete. A key observation is that if no character count exceedsk, the substring is complete if and only if(number of distinct characters) * k == (length of substring). If this condition is met, increment the total count.
Walkthrough
This approach reduces the problem space by first breaking the string down. Within each valid chunk, we iterate through all possible starting points. For each starting point, we expand a window to the right, character by character, maintaining a frequency count. We can quickly discard invalid windows if any character count surpasses k. The check for a complete substring is simplified by comparing the product of distinct characters and k with the current window's length.
class Solution { public int countCompleteSubstrings(String word, int k) { int totalCount = 0; int n = word.length(); int start = 0; for (int i = 1; i <= n; i++) { if (i == n || Math.abs(word.charAt(i) - word.charAt(i - 1)) > 2) { // Process the chunk word.substring(start, i) totalCount += countInChunk(word.substring(start, i), k); start = i; } } return totalCount; } private int countInChunk(String s, int k) { int count = 0; int n = s.length(); for (int i = 0; i < n; i++) { int[] freq = new int[26]; int distinct = 0; for (int j = i; j < n; j++) { int charIndex = s.charAt(j) - 'a'; if (freq[charIndex] == 0) { distinct++; } freq[charIndex]++; if (freq[charIndex] > k) { // This window and any larger ones starting at i are invalid break; } if (distinct * k == (j - i + 1)) { // If length is distinct * k, and no freq > k, all must be == k. count++; } } } return count; }}Complexity
Time
O(N^2) - In the worst case, the entire string is one large chunk. The two nested loops over the chunk lead to a quadratic time complexity. `sum(M_i^2)` over all chunks can be `O(N^2)`.
Space
O(1) - The frequency map requires constant space.
Trade-offs
Pros
More efficient than the naive brute-force approach.
Correctly utilizes the adjacency property to partition the problem.
Cons
The O(N^2) time complexity is still too slow for the problem's constraints and will likely time out.
The most efficient solution also starts by splitting the word into valid chunks. The key improvement is how we process each chunk. Instead of checking every possible substring, we iterate through the possible number of unique characters a complete substring can have (from 1 to 26). For each number of unique characters, d, we know the exact length a complete substring must have: d * k. We can then use a fixed-size sliding window of this length to find all such substrings in linear time for that d.
Algorithm
- Split into Chunks: Same as the previous approach, split
wordinto chunks whereabs(char_diff) <= 2for all adjacent characters. This takesO(N)time. - Process Each Chunk with a Specialized Sliding Window: For each chunk
s: a. Initialize a local counter for the chunk to zero. b. Iterate through the possible number of distinct charactersda complete substring can have, fromd = 1tod = 26. c. For eachd, the required length of a complete substring isL = d * k. IfLexceeds the chunk's length, break the loop ford. d. Use a fixed-size sliding window of lengthLto traverse the chunk. e. Maintain two counters for the window:distinctCount(number of unique characters) andkCount(number of characters with frequency exactlyk). f. Initialize the window with the firstLcharacters, calculating the initialdistinctCountandkCount. g. Slide the window one position at a time to the right. In each step, efficiently updatedistinctCountandkCountinO(1)time by accounting for the character leaving and the character entering the window. h. After each slide, ifdistinctCount == dandkCount == d, it means the current window is a complete substring. Increment the chunk's counter. - Sum Results: The total count is the sum of counts from all chunks.
Walkthrough
This approach cleverly transforms the problem. By fixing the number of distinct characters d, we also fix the target substring length L = d * k. This allows us to use a very efficient fixed-size sliding window. The total work for a chunk of length M becomes 26 * O(M), which is O(M). Since the sum of all chunk lengths is N, the total time complexity is linear.
class Solution { public int countCompleteSubstrings(String word, int k) { int totalCount = 0; int n = word.length(); int start = 0; for (int i = 1; i <= n; i++) { if (i == n || Math.abs(word.charAt(i) - word.charAt(i - 1)) > 2) { totalCount += countInChunk(word.substring(start, i), k); start = i; } } return totalCount; } private int countInChunk(String s, int k) { int count = 0; int n = s.length(); for (int d = 1; d <= 26; d++) { int len = d * k; if (len > n) { break; } int[] freq = new int[26]; int distinctCount = 0; int kCount = 0; // count of chars with frequency == k // Initialize first window for (int i = 0; i < len; i++) { int cIdx = s.charAt(i) - 'a'; if (freq[cIdx] == 0) distinctCount++; freq[cIdx]++; if (freq[cIdx] == k) kCount++; if (freq[cIdx] == k + 1) kCount--; } if (distinctCount == d && kCount == d) { count++; } // Slide window for (int i = len; i < n; i++) { // Add char at i int inIdx = s.charAt(i) - 'a'; if (freq[inIdx] == 0) distinctCount++; freq[inIdx]++; if (freq[inIdx] == k) kCount++; if (freq[inIdx] == k + 1) kCount--; // Remove char at i - len int outIdx = s.charAt(i - len) - 'a'; if (freq[outIdx] == k) kCount--; if (freq[outIdx] == k + 1) kCount++; freq[outIdx]--; if (freq[outIdx] == 0) distinctCount--; if (distinctCount == d && kCount == d) { count++; } } } return count; }}Complexity
Time
O(N) - The initial split takes O(N). Processing each chunk of length `M` takes O(26 * M), which is O(M). Since the sum of all chunk lengths is `N`, the total time is O(N) + sum(O(M_i)) = O(N).
Space
O(1) - The space for the frequency map is constant (size 26), regardless of the input size.
Trade-offs
Pros
Optimal O(N) time complexity, making it highly efficient for large inputs.
Handles all constraints of the problem effectively.
Cons
The implementation is more complex, requiring careful management of the sliding window and its associated counters.
Solutions
Solution
class Solution {public int countCompleteSubstrings(String word, int k) { int n = word.length(); int ans = 0; for (int i = 0; i < n;) { int j = i + 1; while (j < n && Math.abs(word.charAt(j) - word.charAt(j - 1)) <= 2) { ++j; } ans += f(word.substring(i, j), k); i = j; } return ans; }private int f(String s, int k) { int m = s.length(); int ans = 0; for (int i = 1; i <= 26 && i * k <= m; ++i) { int l = i * k; int[] cnt = new int[26]; for (int j = 0; j < l; ++j) { ++cnt[s.charAt(j) - 'a']; } Map<Integer, Integer> freq = new HashMap<>(); for (int x : cnt) { if (x > 0) { freq.merge(x, 1, Integer : : sum); } } if (freq.getOrDefault(k, 0) == i) { ++ans; } for (int j = l; j < m; ++j) { int a = s.charAt(j) - 'a'; int b = s.charAt(j - l) - 'a'; freq.merge(cnt[a], -1, Integer : : sum); ++cnt[a]; freq.merge(cnt[a], 1, Integer : : sum); freq.merge(cnt[b], -1, Integer : : sum); --cnt[b]; freq.merge(cnt[b], 1, Integer : : sum); if (freq.getOrDefault(k, 0) == i) { ++ans; } } } return ans; }}class Solution {public: int countCompleteSubstrings(string word, int k) { int n = word.length(); int ans = 0; auto f = [&](string s) { int m = s.length(); int ans = 0; for (int i = 1; i <= 26 && i * k <= m; ++i) { int l = i * k; int cnt[26]{}; for (int j = 0; j < l; ++j) { ++cnt[s[j] - 'a']; } unordered_map<int, int> freq; for (int x : cnt) { if (x > 0) { freq[x]++; } } if (freq[k] == i) { ++ans; } for (int j = l; j < m; ++j) { int a = s[j] - 'a'; int b = s[j - l] - 'a'; freq[cnt[a]]--; cnt[a]++; freq[cnt[a]]++; freq[cnt[b]]--; cnt[b]--; freq[cnt[b]]++; if (freq[k] == i) { ++ans; } } } return ans; }; for (int i = 0; i < n;) { int j = i + 1; while (j < n && abs(word[j] - word[j - 1]) <= 2) { ++j; } ans += f(word.substr(i, j - i)); i = j; } return ans; }};class Solution: def countCompleteSubstrings(self, word: str, k: int) -> int: def f(s: str) -> int: m = len(s) ans = 0 for i in range(1, 27): l = i * k if l > m: break cnt = Counter(s[: l]) freq = Counter(cnt . values()) ans += freq[k] == i for j in range(l, m): freq[cnt[s[j]]] -= 1 cnt[s[j]] += 1 freq[cnt[s[j]]] += 1 freq[cnt[s[j - l]]] -= 1 cnt[s[j - l]] -= 1 freq[cnt[s[j - l]]] += 1 ans += freq[k] == i return ans n = len(word) ans = i = 0 while i < n: j = i + 1 while j < n and abs(ord(word[j]) - ord(word[j - 1])) <= 2: j += 1 ans += f(word[i: j]) i = j return ansVideo 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.