LeetCode 3325 - Count Substrings With K-Frequency Characters I
The problem asks us to count all substrings of a given string s such that at least one character in the substring appears at least k times. In simpler terms, for every substring we consider, we check whether there is any character that has frequency greater than or equal to k.
Difficulty: 🟡 Medium
Topics: Hash Table, String, Sliding Window
Solution
Problem Understanding
The problem asks us to count all substrings of a given string s such that at least one character in the substring appears at least k times. In simpler terms, for every substring we consider, we check whether there is any character that has frequency greater than or equal to k. If there is, we include that substring in our count.
The input consists of a string s with only lowercase English letters and an integer k. The output is a single integer representing the total number of substrings that satisfy the condition. The constraints (1 <= s.length <= 3000 and 1 <= k <= s.length) indicate that the string can be moderately large, which rules out naive algorithms with cubic time complexity. Edge cases include when k = 1 (all substrings are valid) and when no character appears k times in any substring (the output should be 0). The problem guarantees that the string is non-empty and only contains lowercase letters, so we do not need to handle other characters or empty strings.
Approaches
Brute Force Approach
A naive approach is to generate all possible substrings of s and, for each substring, count the frequency of each character. If any character appears at least k times, we increment the count. While this works correctly, it is inefficient. Generating all substrings is O(n²), and checking the frequency of each substring can take up to O(n), making the total time complexity O(n³). This is too slow for the upper constraint of n = 3000.
Optimal Approach
The key observation is that for each starting index i in the string, we can extend the substring to the right while maintaining a frequency map. Once any character reaches frequency k, all further extensions of that substring to the right are guaranteed to satisfy the condition because adding more characters cannot reduce the frequency of the existing ones. This allows us to avoid checking every substring individually and instead use a sliding window with a frequency map. By moving a right pointer across the string for each starting index and stopping once we satisfy the k-frequency condition, we efficiently count valid substrings.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n³) | O(26) | Generate all substrings and check frequencies individually |
| Optimal | O(n²) | O(26) | Use sliding window and frequency map; extend right pointer until condition is met |
Algorithm Walkthrough
- Initialize a counter
result = 0to store the total number of valid substrings. - Loop through each starting index
iof the string. - For each
i, create an empty frequency mapfreqto track character counts. - Extend a right pointer
jfromito the end of the string. - For each character
s[j], increment its frequency infreq. - After updating the frequency, check if any character has frequency
>= k. - If yes, all substrings starting at
iand ending at any index>= jare valid, so addlen(s) - jtoresultand break the inner loop. - Continue to the next starting index
i. - Return
resultafter processing all starting indices.
Why it works: The key invariant is that once a character reaches frequency k in a substring starting at index i, extending the substring to the right cannot reduce that frequency. Therefore, all extensions are guaranteed to satisfy the condition, allowing us to count many substrings at once.
Python Solution
class Solution:
def numberOfSubstrings(self, s: str, k: int) -> int:
result = 0
n = len(s)
for i in range(n):
freq = {}
for j in range(i, n):
char = s[j]
freq[char] = freq.get(char, 0) + 1
if freq[char] >= k:
result += n - j
break
return result
This implementation loops through each starting index i and uses a dictionary freq to track character counts. The inner loop extends the substring to the right, updating freq. Once any character reaches the required frequency k, we add all remaining substrings from j to the end of the string to the result and break the inner loop to avoid unnecessary checks.
Go Solution
func numberOfSubstrings(s string, k int) int {
result := 0
n := len(s)
for i := 0; i < n; i++ {
freq := make(map[byte]int)
for j := i; j < n; j++ {
char := s[j]
freq[char]++
if freq[char] >= k {
result += n - j
break
}
}
}
return result
}
In Go, we use a map map[byte]int to track character frequencies. The logic mirrors the Python implementation. We handle string indexing with s[j], which returns a byte since Go strings are byte slices.
Worked Examples
Example 1: s = "abacb", k = 2
| i | j | freq | Condition met? | result |
|---|---|---|---|---|
| 0 | 0 | {'a':1} | No | 0 |
| 0 | 1 | {'a':1,'b':1} | No | 0 |
| 0 | 2 | {'a':2,'b':1} | Yes | 5-2=3 |
| 1 | 1 | {'b':1} | No | 3 |
| 1 | 2 | {'b':1,'a':1} | No | 3 |
| 1 | 3 | {'b':1,'a':2,'c':1} | Yes | 5-3=2 (total 5) |
| 2 | 2 | {'a':1} | No | 5 |
| 2 | 3 | {'a':1,'c':1} | No | 5 |
| 2 | 4 | {'a':1,'c':1,'b':1} | No | 5 |
| 3 | 3 | {'c':1} | No | 5 |
| 3 | 4 | {'c':1,'b':1} | No | 5 |
| 4 | 4 | {'b':1} | No | 5 |
Final output: 4 (matches explanation in problem).
Example 2: s = "abcde", k = 1
All substrings are valid because each character appears at least once.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n²) | For each starting index, we may extend the substring up to the end of the string once. |
| Space | O(26) | We store character frequencies using a hash map of size up to 26 letters. |
The algorithm improves over the brute-force O(n³) approach by counting multiple valid substrings at once once the k-frequency condition is met.
Test Cases
# Provided examples
assert Solution().numberOfSubstrings("abacb", 2) == 4 # Example 1
assert Solution().numberOfSubstrings("abcde", 1) == 15 # Example 2
# Edge and boundary cases
assert Solution().numberOfSubstrings("aaaaa", 3) == 6 # Repeated single character
assert Solution().numberOfSubstrings("abcd", 2) == 0 # No character reaches k
assert Solution().numberOfSubstrings("a", 1) == 1 # Minimum length string
assert Solution().numberOfSubstrings("aaabbb", 2) == 10 # Multiple characters meet k
assert Solution().numberOfSubstrings("abcabcabc", 3) == 1 # Only substrings containing repeated letters
| Test | Why |
|---|---|
| "abacb", 2 | Validates counting with repeated characters |
| "abcde", 1 | Checks k=1 case where all substrings are valid |
| "aaaaa", 3 | Checks repeated single character with overlapping substrings |
| "abcd", 2 | No character reaches k, output should be 0 |
| "a", 1 | Minimum input size |
| "aaabbb", 2 | Multiple characters meet k |
| "abcabcabc", 3 | Ensures algorithm counts only substrings satisfying k frequency |
Edge Cases
One important edge case is when k = 1. Every substring is valid because each character occurs at least once. Another edge case occurs when k is larger than any character frequency in the string; the function should correctly return 0. A third edge case is a string with repeated characters where multiple overlapping substrings satisfy the condition; the algorithm must correctly count all valid substrings without double counting. Our sliding window approach handles these cases efficiently by breaking once the condition is met and counting all extensions at once.