2947. Count Beautiful Substrings I
Problem Description
You need to find how many substrings in a given string s
are "beautiful" according to specific criteria.
A substring is considered beautiful if it meets both of these conditions:
- The number of vowels equals the number of consonants in the substring
- The product of the number of vowels and consonants is divisible by a given positive integer
k
(i.e.,(vowels * consonants) % k == 0
)
The vowels are defined as the letters 'a'
, 'e'
, 'i'
, 'o'
, and 'u'
. All other letters are considered consonants.
For example, if we have a substring with 2 vowels and 2 consonants:
- Condition 1 is satisfied (2 == 2)
- For condition 2, we check if
(2 * 2) % k == 0
, which depends on the value ofk
The solution uses a brute force approach with two nested loops:
- The outer loop iterates through each starting position
i
in the string - The inner loop extends from position
i
to create all possible substrings starting ati
- For each substring from index
i
toj
, it counts the vowels - The consonants count is calculated as
(j - i + 1) - vowels
(total length minus vowels) - If both conditions are met, the answer counter is incremented
The algorithm checks all possible non-empty substrings and returns the total count of beautiful substrings found.
Intuition
The key insight is that we need to examine every possible substring to check if it's beautiful, since there's no clear pattern that would allow us to skip certain substrings or use a more optimized approach directly.
When we think about counting substrings with specific properties, a natural starting point is to generate all possible substrings. For a string of length n
, there are n * (n + 1) / 2
possible substrings, which suggests an O(n²)
approach might be reasonable.
The observation that makes the brute force approach efficient enough is that we can incrementally build our vowel count as we extend a substring. Instead of recounting vowels for each new substring from scratch, we:
- Fix a starting position
i
- Extend the substring one character at a time by incrementing
j
- Update our vowel count by just checking if the new character
s[j]
is a vowel
Since we know the total length of the current substring is j - i + 1
, we can derive the consonant count without additional iteration: consonants = (j - i + 1) - vowels
.
The two conditions for a beautiful substring are straightforward to check:
- Equal vowels and consonants:
vowels == consonants
- Divisibility:
(vowels * consonants) % k == 0
This incremental approach avoids redundant counting - we build upon previous calculations rather than starting fresh for each substring. While we're still checking all O(n²)
substrings, we're doing constant-time work for each one, making the overall complexity O(n²)
which is acceptable for reasonable string lengths.
Learn more about Math and Prefix Sum patterns.
Solution Approach
The solution implements a nested loop approach to examine all possible substrings:
-
Initialize variables:
n = len(s)
stores the string lengthvs = set("aeiou")
creates a set for O(1) vowel lookupans = 0
initializes the counter for beautiful substrings
-
Outer loop - Starting positions:
for i in range(n):
This loop iterates through each possible starting position of a substring.
-
Inner loop - Extending substrings:
vowels = 0 for j in range(i, n):
For each starting position
i
, we extend the substring to positionj
. We reset the vowel count to 0 before examining substrings starting at positioni
. -
Incremental vowel counting:
vowels += s[j] in vs
This uses Python's boolean-to-integer conversion. If
s[j]
is a vowel (exists in setvs
), the expression evaluates toTrue
which adds 1 tovowels
. Otherwise, it adds 0. -
Calculate consonants:
consonants = j - i + 1 - vowels
The total substring length is
j - i + 1
. Subtracting the vowel count gives us the consonant count. -
Check beautiful conditions:
if vowels == consonants and vowels * consonants % k == 0: ans += 1
Both conditions must be satisfied:
- Equal counts:
vowels == consonants
- Divisibility:
(vowels * consonants) % k == 0
- Equal counts:
The algorithm systematically checks every substring from s[i:j+1]
where i
ranges from 0 to n-1
and j
ranges from i
to n-1
. The use of a set for vowel checking ensures O(1) lookup time, and the incremental counting avoids redundant work within each inner loop iteration.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with s = "baeyh"
and k = 2
.
Initial Setup:
n = 5
(length of string)vs = {'a', 'e', 'i', 'o', 'u'}
(vowel set)ans = 0
(counter for beautiful substrings)
Iteration Process:
i = 0 (starting at 'b'):
- j = 0: substring = "b"
vowels = 0
('b' is not a vowel)consonants = 1 - 0 = 1
- Check:
0 ≠ 1
, not beautiful
- j = 1: substring = "ba"
vowels = 0 + 1 = 1
('a' is a vowel)consonants = 2 - 1 = 1
- Check:
1 == 1
✓ and1 * 1 % 2 = 1 % 2 = 1
✗, not beautiful
- j = 2: substring = "bae"
vowels = 1 + 1 = 2
('e' is a vowel)consonants = 3 - 2 = 1
- Check:
2 ≠ 1
, not beautiful
- j = 3: substring = "baey"
vowels = 2 + 0 = 2
('y' is not a vowel)consonants = 4 - 2 = 2
- Check:
2 == 2
✓ and2 * 2 % 2 = 4 % 2 = 0
✓, beautiful!ans = 1
- j = 4: substring = "baeyh"
vowels = 2 + 0 = 2
('h' is not a vowel)consonants = 5 - 2 = 3
- Check:
2 ≠ 3
, not beautiful
i = 1 (starting at 'a'):
- j = 1: substring = "a"
vowels = 1
,consonants = 0
- Check:
1 ≠ 0
, not beautiful
- j = 2: substring = "ae"
vowels = 2
,consonants = 0
- Check:
2 ≠ 0
, not beautiful
- j = 3: substring = "aey"
vowels = 2
,consonants = 1
- Check:
2 ≠ 1
, not beautiful
- j = 4: substring = "aeyh"
vowels = 2
,consonants = 2
- Check:
2 == 2
✓ and2 * 2 % 2 = 0
✓, beautiful!ans = 2
i = 2 (starting at 'e'):
- j = 2: substring = "e"
vowels = 1
,consonants = 0
- Check:
1 ≠ 0
, not beautiful
- j = 3: substring = "ey"
vowels = 1
,consonants = 1
- Check:
1 == 1
✓ and1 * 1 % 2 = 1
✗, not beautiful
- j = 4: substring = "eyh"
vowels = 1
,consonants = 2
- Check:
1 ≠ 2
, not beautiful
i = 3 (starting at 'y'):
- j = 3: substring = "y"
vowels = 0
,consonants = 1
- Check:
0 ≠ 1
, not beautiful
- j = 4: substring = "yh"
vowels = 0
,consonants = 2
- Check:
0 ≠ 2
, not beautiful
i = 4 (starting at 'h'):
- j = 4: substring = "h"
vowels = 0
,consonants = 1
- Check:
0 ≠ 1
, not beautiful
Final Result: ans = 2
The beautiful substrings found are "baey" and "aeyh", both having 2 vowels and 2 consonants, with their product (4) divisible by k=2.
Solution Implementation
1class Solution:
2 def beautifulSubstrings(self, s: str, k: int) -> int:
3 """
4 Count beautiful substrings where:
5 1. Number of vowels equals number of consonants
6 2. Product of vowels and consonants is divisible by k
7
8 Args:
9 s: Input string to check for beautiful substrings
10 k: Divisor for the product condition
11
12 Returns:
13 Number of beautiful substrings
14 """
15 n = len(s)
16 vowels_set = {'a', 'e', 'i', 'o', 'u'} # Set of vowels for O(1) lookup
17 beautiful_count = 0
18
19 # Iterate through all possible starting positions
20 for start_idx in range(n):
21 vowel_count = 0
22
23 # Extend substring from current starting position
24 for end_idx in range(start_idx, n):
25 # Check if current character is a vowel and update count
26 if s[end_idx] in vowels_set:
27 vowel_count += 1
28
29 # Calculate consonant count for current substring
30 substring_length = end_idx - start_idx + 1
31 consonant_count = substring_length - vowel_count
32
33 # Check both conditions for a beautiful substring
34 if (vowel_count == consonant_count and
35 (vowel_count * consonant_count) % k == 0):
36 beautiful_count += 1
37
38 return beautiful_count
39
1class Solution {
2 public int beautifulSubstrings(String s, int k) {
3 int length = s.length();
4
5 // Create a lookup array to identify vowels
6 // Index represents character (a=0, b=1, ..., z=25)
7 // Value is 1 if vowel, 0 if consonant
8 int[] isVowel = new int[26];
9 for (char vowel : "aeiou".toCharArray()) {
10 isVowel[vowel - 'a'] = 1;
11 }
12
13 int beautifulCount = 0;
14
15 // Iterate through all possible starting positions
16 for (int start = 0; start < length; ++start) {
17 int vowelCount = 0;
18
19 // Extend substring from current starting position
20 for (int end = start; end < length; ++end) {
21 // Check if current character is a vowel and update count
22 vowelCount += isVowel[s.charAt(end) - 'a'];
23
24 // Calculate consonant count in current substring
25 int substringLength = end - start + 1;
26 int consonantCount = substringLength - vowelCount;
27
28 // Check if substring is beautiful:
29 // 1. Equal number of vowels and consonants
30 // 2. Product of vowels and consonants is divisible by k
31 if (vowelCount == consonantCount && (vowelCount * consonantCount) % k == 0) {
32 ++beautifulCount;
33 }
34 }
35 }
36
37 return beautifulCount;
38 }
39}
40
1class Solution {
2public:
3 int beautifulSubstrings(string s, int k) {
4 int n = s.size();
5
6 // Create a lookup array to check if a character is a vowel
7 // isVowel[i] = 1 if character at index i is a vowel, 0 otherwise
8 int isVowel[26] = {0};
9 string vowels = "aeiou";
10 for (char c : vowels) {
11 isVowel[c - 'a'] = 1;
12 }
13
14 int beautifulCount = 0;
15
16 // Iterate through all possible substrings
17 for (int start = 0; start < n; ++start) {
18 int vowelCount = 0;
19
20 // Extend the substring from start to end
21 for (int end = start; end < n; ++end) {
22 // Update vowel count for current character
23 vowelCount += isVowel[s[end] - 'a'];
24
25 // Calculate consonant count for current substring
26 int substringLength = end - start + 1;
27 int consonantCount = substringLength - vowelCount;
28
29 // Check if substring is beautiful:
30 // 1. Equal number of vowels and consonants
31 // 2. Product of vowels and consonants is divisible by k
32 if (vowelCount == consonantCount &&
33 (vowelCount * consonantCount) % k == 0) {
34 ++beautifulCount;
35 }
36 }
37 }
38
39 return beautifulCount;
40 }
41};
42
1/**
2 * Counts the number of beautiful substrings in a given string.
3 * A beautiful substring has equal number of vowels and consonants,
4 * and the product of vowels and consonants is divisible by k.
5 *
6 * @param s - The input string to search for beautiful substrings
7 * @param k - The divisor for checking if vowels * consonants is divisible by k
8 * @returns The count of beautiful substrings
9 */
10function beautifulSubstrings(s: string, k: number): number {
11 const stringLength: number = s.length;
12
13 // Create a lookup array to identify vowels (1 for vowel, 0 for consonant)
14 // Array size is 26 for all lowercase English letters
15 const isVowel: number[] = Array(26).fill(0);
16
17 // Mark vowels in the lookup array
18 const vowelCharacters: string = 'aeiou';
19 for (const vowel of vowelCharacters) {
20 const vowelIndex: number = vowel.charCodeAt(0) - 'a'.charCodeAt(0);
21 isVowel[vowelIndex] = 1;
22 }
23
24 let beautifulSubstringCount: number = 0;
25
26 // Iterate through all possible starting positions
27 for (let startIndex = 0; startIndex < stringLength; ++startIndex) {
28 let vowelCount: number = 0;
29
30 // Expand the substring from current starting position
31 for (let endIndex = startIndex; endIndex < stringLength; ++endIndex) {
32 // Check if current character is a vowel and update count
33 const currentCharIndex: number = s.charCodeAt(endIndex) - 'a'.charCodeAt(0);
34 vowelCount += isVowel[currentCharIndex];
35
36 // Calculate consonant count for current substring
37 const substringLength: number = endIndex - startIndex + 1;
38 const consonantCount: number = substringLength - vowelCount;
39
40 // Check if substring is beautiful:
41 // 1. Equal number of vowels and consonants
42 // 2. Product of vowels and consonants is divisible by k
43 if (vowelCount === consonantCount && (vowelCount * consonantCount) % k === 0) {
44 ++beautifulSubstringCount;
45 }
46 }
47 }
48
49 return beautifulSubstringCount;
50}
51
Time and Space Complexity
Time Complexity: O(n²)
The algorithm uses two nested loops:
- The outer loop runs from
i = 0
toi = n-1
, iteratingn
times - The inner loop runs from
j = i
toj = n-1
, iterating up ton-i
times for eachi
- The total number of iterations is
n + (n-1) + (n-2) + ... + 1 = n(n+1)/2
- Inside the inner loop, all operations are
O(1)
: checking if a character is in the vowel set, arithmetic operations, and modulo operation - Therefore, the overall time complexity is
O(n²)
Space Complexity: O(1)
The algorithm uses:
- A constant-size set
vs
containing 5 vowels, which isO(1)
space - A few integer variables (
n
,ans
,vowels
,consonants
, loop counters), each usingO(1)
space - No additional data structures that grow with input size
- Therefore, the overall space complexity is
O(1)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Inefficient Repeated Vowel Checking
A common mistake is checking if a character is a vowel using a list instead of a set, or worse, using multiple if-conditions:
Inefficient approach:
# Using a list (O(n) lookup time) vowels_list = ['a', 'e', 'i', 'o', 'u'] if s[j] in vowels_list: # O(n) operation vowel_count += 1 # Or using multiple conditions if s[j] == 'a' or s[j] == 'e' or s[j] == 'i' or s[j] == 'o' or s[j] == 'u': vowel_count += 1
Solution: Always use a set for O(1) constant-time lookups when checking membership repeatedly.
2. Integer Overflow with Large Products
When vowel and consonant counts are large, their product might overflow in some languages. While Python handles arbitrary precision integers automatically, this could be an issue in languages like C++ or Java.
Potential issue:
# In languages with fixed integer sizes, this could overflow if vowel_count * consonant_count % k == 0:
Solution: Use modular arithmetic properties or check divisibility differently:
# Alternative approach to avoid large products if vowel_count == consonant_count: # Since they're equal, we can check (vowel_count^2) % k if (vowel_count * vowel_count) % k == 0: beautiful_count += 1
3. Forgetting Edge Cases with k
A subtle pitfall is not considering special values of k, particularly when k is large or has specific prime factorizations.
Issue: When vowel_count = consonant_count = 0 (empty substring), the product is 0, which is divisible by any k > 0. However, the problem specifies non-empty substrings.
Solution: The code correctly starts with substrings of length 1 by using range(start_idx, n)
, avoiding empty substrings.
4. Redundant Condition Checking
Some might check the beautiful conditions for every iteration, even when it's impossible to satisfy them:
Inefficient approach:
for end_idx in range(start_idx, n):
# Checking conditions even when substring length is odd
# (impossible to have equal vowels and consonants)
if vowel_count == consonant_count:
...
Optimization: Skip checking when the substring length is odd:
substring_length = end_idx - start_idx + 1 if substring_length % 2 == 1: continue # Skip odd-length substrings consonant_count = substring_length - vowel_count if vowel_count == consonant_count and (vowel_count * consonant_count) % k == 0: beautiful_count += 1
5. Case Sensitivity Issues
The problem doesn't specify if the input contains uppercase letters. Failing to handle case sensitivity could lead to incorrect results.
Potential issue:
vowels_set = {'a', 'e', 'i', 'o', 'u'} if s[j] in vowels_set: # Won't catch 'A', 'E', etc.
Solution: Either convert the string to lowercase first or include both cases in the set:
# Option 1: Convert to lowercase s = s.lower() # Option 2: Include both cases vowels_set = {'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'}
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
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
Want a Structured Path to Master System Design Too? Don’t Miss This!