Facebook Pixel

2949. Count Beautiful Substrings II

HardHash TableMathStringNumber TheoryPrefix Sum
Leetcode Link

Problem Description

You are given a string s and a positive integer k.

Let vowels and consonants be the number of vowels and consonants in a string.

A string is beautiful if:

  • vowels == consonants.
  • (vowels * consonants) % k == 0, in other terms, the multiplication of vowels and consonants is divisible by k.

Return the number of non-empty beautiful substrings in the given string s.

A substring is a contiguous sequence of characters in a string.

Vowel letters in English are 'a', 'e', 'i', 'o', and 'u'.

Consonant letters in English are every letter except vowels.

Intuition

To solve the problem of finding beautiful substrings, a strategic approach involves leveraging prefix sums and a hash table. Here's the reasoning behind the solution:

  1. Counting Vowels and Consonants:

    • By assigning a specific value to each character in the string, it's possible to distinguish between vowels and consonants.
    • Using a bitmask (like AEIOU_MASK), each character of s can be quickly checked if it's a vowel.
  2. Prefix Sum Technique:

    • As you iterate through the string, compute a running sum that increases or decreases based on whether the current character is a vowel or consonant.
    • A key insight is that when the prefix sum of vowels and consonants at two different indices is the same, the substring between these two indices has equal numbers of vowels and consonants.
  3. Hash Map Utility:

    • Use a hash map to keep track of the number of times a particular prefix sum (combined with the modulus of its index) occurs.
    • For each character, compute a key based on the prefix sum and its index modulus. Check how many times this key has occurred previously.
    • Each previous occurrence of the same key corresponds to a valid beautiful substring.
  4. Perfect Square Optimization via pSqrt Function:

    • The solution exploits properties of perfect squares to limit the search space effectively. By tuning the prefix sums with certain properties, we ensure that our calculations stay efficient and the substrings checked maintain the required criteria.

By applying these principles, the solution efficiently identifies all beautiful substrings using computationally effective operations.

Learn more about Math and Prefix Sum patterns.

Solution Approach

The solution centers around the [Prefix Sum](/problems/subarray_sum) technique combined with a Hash Table to efficiently track and count the desired substrings.

Key Components

  1. AEIOU Mask:

    • The AEIOU_MASK is a bitmask representing vowels. For each character in the string s, this mask helps determine if the character is a vowel or consonant.
  2. Prefix Sum Calculation:

    • As we iterate over the string s, we calculate a running sum sum that increases by 2 for vowels and decreases by 1 for consonants.
    • The idea is to use even and odd values to balance the counts of vowels and consonants.
  3. Hash Table (Map):

    • A Map is used to keep track of the number of occurrences of each (index % l, sum) pair, where l is derived using the pSqrt function.
    • This (index % l, sum) pair effectively represents unique conditions for beautiful strings in terms of position and vowel/consonant balance.
  4. Counting Beautiful Substrings:

    • As each character in the string s is processed, generate a key derived from the current index and sum.
    • If this key exists in the Map, it indicates the presence of previous indices with the same sum, which are potential starting points for a beautiful substring.
    • Increment the count of beautiful substrings (ans) based on the current count of the key in the Map.
  5. Perfect Square Reduction:

    • The function pSqrt(k * 4) provides a reduced length l for prefix balancing. This calculation leverages mathematical properties associated with perfect squares to keep the process efficient and limit unnecessary checks.

Code Overview

Here's a breakdown of the code workflow:

function beautifulSubstrings(s: string, k: number): number {
    const l = pSqrt(k * 4); // Calculate a reduced length for efficient modulo operations.
    const n = s.length;
    let sum = n; // Initialize the sum using the length of the string.
    let ans = 0; // Initialize answer count.
    const counter = new Map(); // Map for tracking (index % l, sum) occurrences.
    counter.set(((l - 1) << 17) | sum, 1); // Initialize the map with a starting key.
  
    for (let i = 0; i < n; i++) {
        const char = s[i];
        const bit = (AEIOU_MASK >> (char.charCodeAt(0) - 'a'.charCodeAt(0))) & 1; // Check if character is a vowel.
        sum += bit * 2 - 1; // Adjust sum based on whether the character is a vowel or consonant.
        const key = (i % l << 17) | sum; // Create a unique key for (index % l, sum).
      
        ans += counter.get(key) || 0; // Increment answer with count of existing key.
        counter.set(key, (counter.get(key) ?? 0) + 1); // Update map with incremented count for the key.
    }
  
    return ans; // Return the total count of beautiful substrings.
}
const AEIOU_MASK = 1065233; // Binary mask for vowels.

function pSqrt(n: number) {
    let res = 1;
    for (let i = 2; i * i <= n; i++) {
        let i2 = i * i;
        while (n % i2 == 0) {
            res *= i;
            n /= i2;
        }
        if (n % i == 0) {
            res *= i;
            n /= i;
        }
    }
    if (n > 1) {
        res *= n;
    }
    return res; // Returns the square root of n, adjusted for perfect square factors.
}

This approach efficiently manages the substring checks using arithmetic properties and hash mapping, ensuring both speed and accuracy.

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Consider the string s = "abbaaa" and k = 2.

  1. Initial Setup:

    • l is calculated using pSqrt(k * 4). For k = 2, it computes to pSqrt(8), which simplifies to 2. This means we use a modulus base of 2 for our indexing.
    • Initialize sum = n = 6 (size of s), ans = 0, and a Map to keep track of seen keys.
  2. Processing Characters:

    • Character 0 (a):

      • a is a vowel, so bit = 1.
      • Update sum using sum += bit * 2 - 1 = 6 + 2 - 1 = 7.
      • Calculate key with key = (0 % 2 << 17) | 7 = 7.
      • Check the Map for key 7 and increment ans by counter[7] which is 0 initially.
      • Update the Map: counter[7] = 1.
    • Character 1 (b):

      • b is a consonant, so bit = 0.
      • Update sum using sum += bit * 2 - 1 = 7 - 1 = 6.
      • Calculate key with key = (1 % 2 << 17) | 6 = 131078.
      • Check the Map for key 131078 and increment ans by counter[131078] which is 0 initially.
      • Update the Map: counter[131078] = 1.
    • Character 2 (b):

      • b is a consonant, so bit = 0.
      • Update sum using sum += bit * 2 - 1 = 6 - 1 = 5.
      • Calculate key with key = (2 % 2 << 17) | 5 = 5.
      • Check the Map for key 5 and increment ans by counter[5] which is 0 initially.
      • Update the Map: counter[5] = 1.
    • Character 3 (a):

      • a is a vowel, so bit = 1.
      • Update sum using sum += bit * 2 - 1 = 5 + 2 - 1 = 6.
      • Calculate key with key = (3 % 2 << 17) | 6 = 131078.
      • Check the Map for key 131078 and increment ans by counter[131078] which is 1.
      • Update ans: ans = 1.
      • Update the Map: counter[131078] = 2.
    • Character 4 (a):

      • a is a vowel, so bit = 1.
      • Update sum using sum += bit * 2 - 1 = 6 + 2 - 1 = 7.
      • Calculate key with key = (4 % 2 << 17) | 7 = 7.
      • Check the Map for key 7 and increment ans by counter[7] which is 1.
      • Update ans: ans = 2.
      • Update the Map: counter[7] = 2.
    • Character 5 (a):

      • a is a vowel, so bit = 1.
      • Update sum using sum += bit * 2 - 1 = 7 + 2 - 1 = 8.
      • Calculate key with key = (5 % 2 << 17) | 8 = 131080.
      • Check the Map for key 131080 and increment ans by counter[131080] which is 0.
      • Update the Map: counter[131080] = 1.
  3. Return the Result:

    • The total number of beautiful substrings found is ans = 2.

In this walk-through, we derived 2 beautiful substrings from the existing setup. This explanation demonstrates how the algorithm efficiently scopes through each character, updates its internal state, and counts substrings that meet the specified criteria using prefix sums and hash mapping.

Solution Implementation

1def beautifulSubstrings(s: str, k: int) -> int:
2    l = pSqrt(k * 4)  # Calculate the modified period length factor
3    n = len(s)  # Get the length of the input string
4    sum_val = n  # Initialize sum_val with the length of the string
5    answer = 0  # Counter for beautiful substrings
6    counter = {}  # Dictionary to track occurrences of states
7    counter[((l - 1) << 17) | sum_val] = 1  # Initialize the dictionary with the start state
8
9    # AEIOU_MASK is a bitmask where a bit is set for each vowel (a, e, i, o, u)
10    AEIOU_MASK = 1065233
11
12    for i in range(n):
13        char = s[i]  # Current character of the string
14        # Calculate the bit value: 1 if char is a vowel, else 0
15        bit = (AEIOU_MASK >> (ord(char) - ord('a'))) & 1
16        sum_val += bit * 2 - 1  # Adjust sum_val: 1 for vowels, -1 for non-vowels
17
18        # Calculate the key based on the current index and sum_val
19        key = ((i % l) << 17) | sum_val
20
21        # Increment the answer by the number of times this state has been encountered
22        answer += counter.get(key, 0)
23
24        # Update the counter dictionary with the current key's occurrence
25        counter[key] = counter.get(key, 0) + 1
26
27    return answer  # Return the total count of beautiful substrings
28
29def pSqrt(n: int) -> int:
30    res = 1  # Initialize the result as 1
31    i = 2
32
33    while i * i <= n:
34        i2 = i * i  # Square of the current factor
35
36        # Divide out all factors of i^2 as much as possible
37        while n % i2 == 0:
38            res *= i  # Multiply result by i
39            n //= i2  # Reduce n by factor of i^2
40
41        # Divide out a single factor of i if n is still divisible by i
42        if n % i == 0:
43            res *= i  # Multiply result by i
44            n //= i  # Reduce n by factor of i
45
46        i += 1
47
48    # If n is greater than 1, multiply the result by n (handling remaining prime factor)
49    if n > 1:
50        res *= n
51
52    return res  # Return the final calculated product
53
1import java.util.HashMap;
2import java.util.Map;
3
4public class BeautifulSubstrings {
5
6    // AEIOU_MASK is a bitmask where a bit is set for each vowel (a, e, i, o, u)
7    private static final int AEIOU_MASK = 1065233;
8
9    public static int beautifulSubstrings(String s, int k) {
10        int l = pSqrt(k * 4); // Calculate the modified period length factor
11        int n = s.length(); // Get the length of the input string
12        int sum = n; // Initialize sum with the length of the string
13        int ans = 0; // Counter for beautiful substrings
14        Map<Integer, Integer> counter = new HashMap<>(); // Map to track occurrences of states
15        counter.put(((l - 1) << 17) | sum, 1); // Initialize the map with the start state
16
17        for (int i = 0; i < n; i++) {
18            char c = s.charAt(i); // Current character of the string
19            // Calculate the bit value: 1 if char is a vowel (in AEIOU_MASK), else 0
20            int bit = (AEIOU_MASK >> (c - 'a')) & 1;
21            sum += bit * 2 - 1; // Adjust sum: 1 for vowels, -1 for non-vowels
22
23            // Calculate the key based on the current index and sum
24            int key = ((i % l) << 17) | sum;
25
26            // Increment the answer by the number of times this state has been encountered
27            ans += counter.getOrDefault(key, 0);
28
29            // Update the counter map with the current key's occurrence
30            counter.put(key, counter.getOrDefault(key, 0) + 1);
31        }
32        return ans; // Return the total count of beautiful substrings
33    }
34
35    /**
36     * Calculates the largest factor of n that is a perfect square.
37     *
38     * @param n The number to be factored.
39     * @return The product of prime factors raised to an appropriate power.
40     */
41    public static int pSqrt(int n) {
42        int res = 1; // Initialize the result as 1
43        for (int i = 2; i * i <= n; i++) {
44            int i2 = i * i; // Square of the current factor
45
46            // Divide out all factors of i^2 as much as possible
47            while (n % i2 == 0) {
48                res *= i; // Multiply result by i
49                n /= i2; // Reduce n by factor of i^2
50            }
51
52            // Divide out a single factor of i if n is still divisible by i
53            if (n % i == 0) {
54                res *= i; // Multiply result by i
55                n /= i; // Reduce n by factor of i
56            }
57        }
58
59        // If n is greater than 1, multiply the result by n (handling remaining prime factor)
60        if (n > 1) {
61            res *= n;
62        }
63
64        return res; // Return the final calculated product
65    }
66}
67
1#include <iostream>
2#include <unordered_map>
3#include <string>
4
5using namespace std;
6
7const int AEIOU_MASK = 1065233; // Bitmask where bits for vowels (a, e, i, o, u) are set
8
9/**
10 * Calculates the largest factor of n that is a perfect square.
11 * 
12 * @param n The number to be factored.
13 * @returns The product of prime factors raised to an appropriate power.
14 */
15int pSqrt(int n) {
16    int res = 1; // Initialize result as 1
17    for (int i = 2; i * i <= n; i++) {
18        int i2 = i * i; // Square of current factor
19      
20        // Divide out all factors of i^2 as much as possible
21        while (n % i2 == 0) {
22            res *= i; // Multiply result by i
23            n /= i2; // Reduce n by factor of i^2
24        }
25
26        // Divide out a single factor of i if n is still divisible by i
27        if (n % i == 0) {
28            res *= i; // Multiply result by i
29            n /= i; // Reduce n by a factor of i
30        }
31    }
32
33    // If n is greater than 1, multiply the result by n (handling remaining prime factor)
34    if (n > 1) {
35        res *= n;
36    }
37
38    return res; // Return the final calculated product
39}
40
41/**
42 * Counts the number of beautiful substrings in a given string.
43 * 
44 * @param s The input string.
45 * @param k A parameter to adjust period length.
46 * @returns The total count of beautiful substrings.
47 */
48int beautifulSubstrings(const string& s, int k) {
49    int l = pSqrt(k * 4); // Calculate the modified period length factor
50    int n = s.length(); // Get the length of the input string
51    int sum = n; // Initialize sum with the length of the string
52    int ans = 0; // Counter for beautiful substrings
53    unordered_map<int, int> counter; // Map to track occurrences of states
54    counter[((l - 1) << 17) | sum] = 1; // Initialize the map with the start state
55
56    for (int i = 0; i < n; i++) {
57        char char = s[i]; // Current character of the string
58        // Calculate the bit value: 1 if char is a vowel (in AEIOU_MASK), else 0
59        int bit = (AEIOU_MASK >> (char - 'a')) & 1;
60        sum += bit * 2 - 1; // Adjust sum: 1 for vowels, -1 for non-vowels
61
62        // Calculate the key based on the current index and sum
63        int key = ((i % l) << 17) | sum;
64
65        // Increment the answer by the number of times this state has been encountered
66        ans += counter[key];
67
68        // Update the counter map with the current key's occurrence
69        counter[key]++;
70    }
71    return ans; // Return the total count of beautiful substrings
72}
73
74int main() {
75    // Example usage
76    string s = "aeiou";
77    int k = 3;
78    cout << "Beautiful Substrings: " << beautifulSubstrings(s, k) << endl;
79    return 0;
80}
81
1function beautifulSubstrings(s: string, k: number): number {
2    const l = pSqrt(k * 4); // Calculate the modified period length factor
3    const n = s.length; // Get the length of the input string
4    let sum = n; // Initialize sum with the length of the string
5    let ans = 0; // Counter for beautiful substrings
6    const counter = new Map<number, number>(); // Map to track occurrences of states
7    counter.set(((l - 1) << 17) | sum, 1); // Initialize the map with the start state
8
9    for (let i = 0; i < n; i++) {
10        const char = s[i]; // Current character of the string
11        // Calculate the bit value: 1 if char is a vowel (in AEIOU_MASK), else 0
12        const bit = (AEIOU_MASK >> (char.charCodeAt(0) - 'a'.charCodeAt(0))) & 1;
13        sum += bit * 2 - 1; // Adjust sum: 1 for vowels, -1 for non-vowels
14
15        // Calculate the key based on the current index and sum
16        const key = ((i % l) << 17) | sum;
17
18        // Increment the answer by the number of times this state has been encountered
19        ans += counter.get(key) || 0;
20
21        // Update the counter map with the current key's occurrence
22        counter.set(key, (counter.get(key) ?? 0) + 1);
23    }
24    return ans; // Return the total count of beautiful substrings
25}
26
27// AEIOU_MASK is a bitmask where a bit is set for each vowel (a, e, i, o, u)
28const AEIOU_MASK = 1065233;
29
30/**
31 * Calculates the largest factor of n that is a perfect square.
32 *
33 * @param n The number to be factored.
34 * @returns The product of prime factors raised to an appropriate power.
35 */
36function pSqrt(n: number): number {
37    let res = 1; // Initialize the result as 1
38    for (let i = 2; i * i <= n; i++) {
39        let i2 = i * i; // Square of the current factor
40
41        // Divide out all factors of i^2 as much as possible
42        while (n % i2 === 0) {
43            res *= i; // Multiply result by i
44            n /= i2; // Reduce n by factor of i^2
45        }
46
47        // Divide out a single factor of i if n is still divisible by i
48        if (n % i === 0) {
49            res *= i; // Multiply result by i
50            n /= i; // Reduce n by factor of i
51        }
52    }
53
54    // If n is greater than 1, multiply the result by n (handling remaining prime factor)
55    if (n > 1) {
56        res *= n;
57    }
58
59    return res; // Return the final calculated product
60}
61

Time and Space Complexity

The time complexity of the beautifulSubstrings function is O(n), where n is the length of the input string s. This is because the function iterates over the string with a single loop running n times. Each operation inside the loop, including accessing and updating the Map, takes constant time on average.

The pSqrt function runs in O(sqrt(k)) time complexity, which is the time taken to find the perfect square root decomposition using trial division up to the square root of k.

Overall, the time complexity of the combined code is O(n + sqrt(k)).

The space complexity is O(l + n), where l is the result of pSqrt(k * 4). This includes the space used by the counter Map which can store up to O(n) entries, each representing a unique state combination of (i % l << 17) | sum.

Learn more about how to find time and space complexity quickly.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

Which of these properties could exist for a graph but not a tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More