Facebook Pixel

1781. Sum of Beauty of All Substrings

MediumHash TableStringCounting
Leetcode Link

Problem Description

The problem asks us to find the sum of beauty values for all possible substrings of a given string s.

The beauty of a string is defined as the difference between the frequency of the most frequent character and the frequency of the least frequent character in that string.

For example, in the string "abaacc":

  • Character 'a' appears 3 times (most frequent)
  • Character 'b' appears 1 time (least frequent)
  • Character 'c' appears 2 times
  • Beauty = 3 - 1 = 2

The task is to:

  1. Find all possible substrings of the input string s
  2. Calculate the beauty value for each substring
  3. Return the sum of all these beauty values

The solution uses a nested loop approach where:

  • The outer loop (variable i) marks the starting position of each substring
  • The inner loop (variable j) extends from position i to the end of the string, considering all substrings starting at position i
  • A counter tracks the frequency of each character as we extend the substring
  • For each substring formed, we calculate its beauty by finding the difference between the maximum and minimum frequency values
  • All beauty values are accumulated and returned as the final answer
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that we need to examine every possible substring of the given string to calculate its beauty value. Since we need all substrings, a brute force approach makes sense here.

Think about how substrings are formed - any substring can be defined by two positions: a starting index and an ending index. This naturally leads to a nested loop structure where we fix a starting position and then explore all possible ending positions from that start.

Why do we build substrings incrementally? Instead of generating each substring from scratch and counting its character frequencies separately, we can be more efficient. When we extend a substring by one character (moving from substring s[i:j] to s[i:j+1]), we only need to update our frequency count for that one new character. This allows us to maintain a running frequency counter that gets updated as we extend the substring.

The beauty calculation becomes straightforward once we have the frequency counts. At each step, after adding a new character to our current substring, we can immediately calculate the beauty by finding the difference between max(cnt.values()) and min(cnt.values()). This gives us the beauty of the current substring s[i:j+1].

The approach essentially answers: "For each possible starting position, what are all the substrings I can form, and what is their beauty?" By systematically exploring all starting positions (outer loop) and all ending positions from each start (inner loop), we ensure we don't miss any substring.

Solution Approach

The solution uses an enumeration approach with counting to solve the problem efficiently.

We enumerate each starting position i in the string, treating it as the left endpoint of our substrings. For each starting position, we find all substrings that begin at this position by extending to different ending positions.

The implementation follows these steps:

  1. Initialize variables: Set ans = 0 to store the total beauty sum and n = len(s) to get the string length.

  2. Outer loop - Fix starting position: Iterate through each index i from 0 to n-1. This represents the starting position of our substrings.

  3. Initialize counter for each starting position: Create a new Counter() object (a dictionary that tracks character frequencies) for each starting position. This counter will be reused and updated as we extend the substring.

  4. Inner loop - Extend substring: For each starting position i, iterate j from i to n-1. This represents the ending position of the substring s[i:j+1].

  5. Update frequency count: Add the character at position j to our counter: cnt[s[j]] += 1. This incrementally builds the frequency map as we extend the substring.

  6. Calculate and accumulate beauty: After adding each new character, calculate the beauty of the current substring as max(cnt.values()) - min(cnt.values()) and add it to our answer.

  7. Return result: After processing all possible substrings, return the accumulated sum.

The time complexity is O(n² × k) where n is the length of the string and k is the number of unique characters in each substring (for finding max and min values). The space complexity is O(k) for the counter, where k is at most 26 for lowercase English letters.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through the solution with a small example: s = "aab"

Step 1: Initialize

  • ans = 0 (to accumulate total beauty)
  • n = 3 (length of string)

Step 2: Process starting position i = 0

Initialize cnt = Counter() for substrings starting at index 0.

  • j = 0: Substring is "a"

    • Update: cnt['a'] = 1
    • Frequencies: {'a': 1}
    • Beauty = max(1) - min(1) = 0
    • ans = 0 + 0 = 0
  • j = 1: Substring is "aa"

    • Update: cnt['a'] = 2
    • Frequencies: {'a': 2}
    • Beauty = max(2) - min(2) = 0
    • ans = 0 + 0 = 0
  • j = 2: Substring is "aab"

    • Update: cnt['b'] = 1
    • Frequencies: {'a': 2, 'b': 1}
    • Beauty = max(2, 1) - min(2, 1) = 2 - 1 = 1
    • ans = 0 + 1 = 1

Step 3: Process starting position i = 1

Initialize new cnt = Counter() for substrings starting at index 1.

  • j = 1: Substring is "a"

    • Update: cnt['a'] = 1
    • Frequencies: {'a': 1}
    • Beauty = max(1) - min(1) = 0
    • ans = 1 + 0 = 1
  • j = 2: Substring is "ab"

    • Update: cnt['b'] = 1
    • Frequencies: {'a': 1, 'b': 1}
    • Beauty = max(1, 1) - min(1, 1) = 1 - 1 = 0
    • ans = 1 + 0 = 1

Step 4: Process starting position i = 2

Initialize new cnt = Counter() for substrings starting at index 2.

  • j = 2: Substring is "b"
    • Update: cnt['b'] = 1
    • Frequencies: {'b': 1}
    • Beauty = max(1) - min(1) = 0
    • ans = 1 + 0 = 1

Final Result: The sum of all beauty values is 1.

The key insight demonstrated here is how we incrementally build each substring by extending it one character at a time, updating our frequency counter as we go, rather than recalculating frequencies from scratch for each substring.

Solution Implementation

1from collections import Counter
2
3class Solution:
4    def beautySum(self, s: str) -> int:
5        """
6        Calculate the sum of beauty values for all substrings of s.
7        Beauty is defined as the difference between the maximum and minimum 
8        frequency of characters in a substring.
9      
10        Args:
11            s: Input string
12          
13        Returns:
14            Sum of beauty values for all substrings
15        """
16        total_beauty = 0
17        string_length = len(s)
18      
19        # Iterate through all possible starting positions
20        for start_index in range(string_length):
21            # Character frequency counter for current substring
22            char_frequency = Counter()
23          
24            # Extend substring from start_index to end of string
25            for end_index in range(start_index, string_length):
26                # Add current character to frequency counter
27                char_frequency[s[end_index]] += 1
28              
29                # Calculate beauty: max frequency - min frequency
30                # Beauty represents the difference between most and least frequent characters
31                max_frequency = max(char_frequency.values())
32                min_frequency = min(char_frequency.values())
33                total_beauty += max_frequency - min_frequency
34      
35        return total_beauty
36
1class Solution {
2    public int beautySum(String s) {
3        int totalBeauty = 0;
4        int stringLength = s.length();
5      
6        // Iterate through all possible starting positions of substrings
7        for (int startIndex = 0; startIndex < stringLength; ++startIndex) {
8            // Frequency array to count occurrences of each character (a-z)
9            int[] charFrequency = new int[26];
10          
11            // Extend the substring from startIndex to each possible ending position
12            for (int endIndex = startIndex; endIndex < stringLength; ++endIndex) {
13                // Update frequency for the current character
14                ++charFrequency[s.charAt(endIndex) - 'a'];
15              
16                // Find minimum and maximum frequencies among characters that appear at least once
17                int minFrequency = 1000;  // Initialize to a large value
18                int maxFrequency = 0;      // Initialize to smallest possible value
19              
20                // Iterate through all character frequencies
21                for (int frequency : charFrequency) {
22                    // Only consider characters that appear in the current substring
23                    if (frequency > 0) {
24                        minFrequency = Math.min(minFrequency, frequency);
25                        maxFrequency = Math.max(maxFrequency, frequency);
26                    }
27                }
28              
29                // Add beauty of current substring (difference between max and min frequencies)
30                totalBeauty += maxFrequency - minFrequency;
31            }
32        }
33      
34        return totalBeauty;
35    }
36}
37
1class Solution {
2public:
3    int beautySum(string s) {
4        int totalBeauty = 0;
5        int stringLength = s.size();
6      
7        // Iterate through all possible starting positions
8        for (int start = 0; start < stringLength; ++start) {
9            // Frequency array for 26 lowercase letters
10            int charFrequency[26] = {0};
11          
12            // Iterate through all possible ending positions from current start
13            for (int end = start; end < stringLength; ++end) {
14                // Increment frequency of current character
15                ++charFrequency[s[end] - 'a'];
16              
17                // Find minimum and maximum frequencies among present characters
18                int minFreq = INT_MAX;
19                int maxFreq = 0;
20              
21                for (int freq : charFrequency) {
22                    if (freq > 0) {
23                        minFreq = min(minFreq, freq);
24                        maxFreq = max(maxFreq, freq);
25                    }
26                }
27              
28                // Add beauty (difference between max and min frequencies) to total
29                totalBeauty += maxFreq - minFreq;
30            }
31        }
32      
33        return totalBeauty;
34    }
35};
36
1/**
2 * Calculates the sum of beauty of all substrings of a given string.
3 * Beauty is defined as the difference between the most frequent and least frequent characters in a substring.
4 * 
5 * @param s - The input string to process
6 * @returns The sum of beauty values for all possible substrings
7 */
8function beautySum(s: string): number {
9    let totalBeautySum: number = 0;
10  
11    // Iterate through each starting position for substrings
12    for (let startIndex: number = 0; startIndex < s.length; startIndex++) {
13        // Map to store character frequency for current substring
14        const charFrequencyMap: Map<string, number> = new Map<string, number>();
15      
16        // Extend substring from current starting position
17        for (let endIndex: number = startIndex; endIndex < s.length; endIndex++) {
18            // Update frequency count for current character
19            const currentChar: string = s[endIndex];
20            const currentFrequency: number = charFrequencyMap.get(currentChar) || 0;
21            charFrequencyMap.set(currentChar, currentFrequency + 1);
22          
23            // Extract all frequency values from the map
24            const frequencyValues: number[] = Array.from(charFrequencyMap.values());
25          
26            // Calculate beauty as difference between max and min frequencies
27            const maxFrequency: number = Math.max(...frequencyValues);
28            const minFrequency: number = Math.min(...frequencyValues);
29            const beauty: number = maxFrequency - minFrequency;
30          
31            // Add current substring's beauty to total sum
32            totalBeautySum += beauty;
33        }
34    }
35  
36    return totalBeautySum;
37}
38

Time and Space Complexity

The time complexity is O(n² × C), where n is the length of the string and C is the size of the character set (26 for lowercase English letters).

  • The outer loop runs n times (from i = 0 to n-1)
  • The inner loop runs n-i times for each iteration of the outer loop
  • Total iterations: n + (n-1) + (n-2) + ... + 1 = n(n+1)/2 = O(n²)
  • Inside the inner loop, finding max(cnt.values()) and min(cnt.values()) each takes O(C) time since we need to iterate through all character counts in the Counter
  • Therefore, the overall time complexity is O(n² × C)

The space complexity is O(C), where C = 26.

  • The Counter object cnt stores at most C distinct characters (26 lowercase English letters)
  • Even though we create a new Counter for each starting position i, we only maintain one Counter at a time
  • The space used by the Counter is bounded by the size of the character set, not the length of the string

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

Common Pitfalls

1. Inefficient Max/Min Calculation for Every Substring

The current solution calls max(char_frequency.values()) and min(char_frequency.values()) for every substring, which iterates through all frequency values each time. This becomes inefficient when dealing with longer strings.

Why it's a problem: For each substring s[i:j+1], we're iterating through up to 26 values (for lowercase English letters) to find max and min. With O(n²) substrings, this adds unnecessary overhead.

Better approach: Track max and min frequencies incrementally or use a more efficient data structure:

from collections import Counter, defaultdict

class Solution:
    def beautySum(self, s: str) -> int:
        total_beauty = 0
        n = len(s)
      
        for i in range(n):
            char_freq = defaultdict(int)
            freq_count = defaultdict(int)  # Track how many chars have each frequency
            max_freq = 0
          
            for j in range(i, n):
                char = s[j]
                old_freq = char_freq[char]
                new_freq = old_freq + 1
                char_freq[char] = new_freq
              
                # Update frequency tracking
                if old_freq > 0:
                    freq_count[old_freq] -= 1
                    if freq_count[old_freq] == 0:
                        del freq_count[old_freq]
                freq_count[new_freq] += 1
              
                # Update max frequency
                max_freq = max(max_freq, new_freq)
              
                # Find min frequency (smallest key in freq_count)
                if freq_count:
                    min_freq = min(freq_count.keys())
                    total_beauty += max_freq - min_freq
      
        return total_beauty

2. Unnecessary Beauty Calculation for Single Character Substrings

When start_index == end_index, the substring contains only one character, so all frequencies are 1, making beauty = 1 - 1 = 0. These calculations don't contribute to the sum but still consume resources.

Optimization: Skip beauty calculation for single-character substrings:

for end_index in range(start_index, string_length):
    char_frequency[s[end_index]] += 1
  
    # Skip single character substrings (beauty is always 0)
    if end_index > start_index:
        max_frequency = max(char_frequency.values())
        min_frequency = min(char_frequency.values())
        total_beauty += max_frequency - min_frequency

3. Memory Overhead from Counter Object Creation

Creating a new Counter() object for each starting position can cause memory allocation overhead, especially for large strings.

Alternative: Reuse a single dictionary and reset it manually:

char_frequency = {}
for start_index in range(string_length):
    char_frequency.clear()  # Reset for new starting position
  
    for end_index in range(start_index, string_length):
        char = s[end_index]
        char_frequency[char] = char_frequency.get(char, 0) + 1
        # ... rest of logic

4. Missing Early Termination Opportunities

The algorithm doesn't leverage the fact that beauty can only increase or stay the same as we extend a substring (max can only increase or stay same, min can only decrease or stay same).

Potential optimization: Track when beauty values stabilize for certain patterns, though this is more complex to implement correctly and may not always provide significant benefits.

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

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

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

Load More