Facebook Pixel

2904. Shortest and Lexicographically Smallest Beautiful String

Problem Description

You are given a binary string s (containing only '0's and '1's) and a positive integer k.

A substring is called beautiful if it contains exactly k ones ('1's).

Your task is to find the shortest beautiful substring. If there are multiple beautiful substrings with the same shortest length, return the lexicographically smallest one among them.

Key requirements:

  • The substring must have exactly k ones
  • Among all such substrings, find the shortest one(s)
  • If multiple shortest substrings exist, return the lexicographically smallest

Lexicographical comparison: A string a is lexicographically smaller than string b if at the first position where they differ, a has a smaller character than b. For binary strings, '0' is smaller than '1'.

For example, if s = "100011001" and k = 3:

  • Some beautiful substrings with 3 ones: "100011", "00011001", "10001100", "000110", "00110", "001100", "01100", "1100", "11001"
  • The shortest ones have length 5: "00110", "01100", "11001"
  • Among these, "00110" is lexicographically smallest

If no beautiful substring exists (when the total number of '1's in s is less than k), return an empty string "".

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Since we need to find the shortest beautiful substring with exactly k ones, and among all shortest ones we need the lexicographically smallest, we can think about checking all possible substrings.

The key insight is that any beautiful substring must have at least k characters (since it needs k ones). So for each starting position i, we only need to check substrings starting from length k onwards.

For each starting position i in the string, we can examine substrings s[i:j] where j ranges from i+k to the end of the string. This ensures we're looking at substrings that are at least k characters long.

For each substring, we need to:

  1. Count if it has exactly k ones
  2. If it does, compare it with our current answer

The comparison logic follows these priorities:

  • First, prefer shorter substrings (smaller length wins)
  • If lengths are equal, prefer the lexicographically smaller one

By systematically checking all possible substrings and keeping track of the best one found so far, we're guaranteed to find the optimal answer. The brute force nature of this approach is acceptable because we need to potentially examine all substrings anyway to ensure we find the lexicographically smallest among the shortest beautiful substrings.

The beauty of this approach is its simplicity - we don't need complex data structures or algorithms. We just enumerate all possibilities and keep the best one according to our criteria.

Learn more about Sliding Window patterns.

Solution Approach

The solution uses a nested loop enumeration approach to find all possible substrings and check if they meet our criteria.

Implementation walkthrough:

  1. Initialize variables:

    • n = len(s) to store the length of the string
    • ans = "" to keep track of the best beautiful substring found so far
  2. Outer loop - Starting position:

    • Iterate through each position i from 0 to n-1 as potential starting points for substrings
  3. Inner loop - Ending position:

    • For each starting position i, iterate j from i+k to n+1
    • This ensures we only check substrings with at least k characters (since a beautiful substring needs at least k characters to contain k ones)
    • Extract substring t = s[i:j]
  4. Check if substring is beautiful and better:

    • First, check if t.count("1") == k to verify it's a beautiful substring
    • If it is beautiful, check if it's better than our current answer using three conditions:
      • not ans: No answer found yet, so this is our first beautiful substring
      • j - i < len(ans): Current substring is shorter than the best one found so far
      • j - i == len(ans) and t < ans: Same length as current best, but lexicographically smaller
  5. Update answer:

    • If all conditions are met, update ans = t
  6. Return result:

    • After checking all possible substrings, return ans
    • If no beautiful substring was found, ans remains empty string ""

The time complexity is O(n³) where n is the length of string s:

  • O(n²) for the two nested loops
  • O(n) for counting ones in each substring and string comparison

The space complexity is O(n) for storing the substring t and answer ans.

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 s = "11000111" and k = 2.

We need to find the shortest substring with exactly 2 ones, and if there are multiple, choose the lexicographically smallest.

Step 1: Initialize

  • n = 8 (length of string)
  • ans = "" (no beautiful substring found yet)

Step 2: Check substrings starting at position 0

  • i = 0, check substrings of length at least k = 2:
    • s[0:2] = "11" → count("1") = 2 ✓ Beautiful!
      • Since ans = "", update ans = "11"
    • s[0:3] = "110" → count("1") = 2 ✓ Beautiful!
      • Length 3 > length 2 of current ans, skip
    • Continue checking longer substrings from position 0, but they're all longer than our current best

Step 3: Check substrings starting at position 1

  • i = 1, check substrings:
    • s[1:3] = "10" → count("1") = 1 ✗ Not beautiful
    • s[1:4] = "100" → count("1") = 1 ✗ Not beautiful
    • s[1:5] = "1000" → count("1") = 1 ✗ Not beautiful
    • s[1:6] = "10001" → count("1") = 2 ✓ Beautiful!
      • Length 5 > length 2 of current ans, skip

Step 4: Check substrings starting at position 2

  • i = 2, check substrings:
    • s[2:4] = "00" → count("1") = 0 ✗ Not beautiful
    • Continue... no beautiful substrings of length 2 found

Step 5: Continue checking positions 3, 4, 5

  • Position 3: s[3:5] = "00" → not beautiful
  • Position 4: s[4:6] = "01" → count("1") = 1 ✗ Not beautiful
  • Position 5: s[5:7] = "11" → count("1") = 2 ✓ Beautiful!
    • Length 2 = length of current ans = "11"
    • Compare lexicographically: "11" == "11", no update needed

Step 6: Check position 6

  • i = 6, check substrings:
    • s[6:8] = "11" → count("1") = 2 ✓ Beautiful!
      • Same as current answer, no update

Final Result: ans = "11"

The algorithm found all beautiful substrings with exactly 2 ones:

  • "11" (positions 0-2, length 2)
  • "110" (positions 0-3, length 3)
  • "10001" (positions 1-6, length 5)
  • "11" (positions 5-7, length 2)
  • "11" (positions 6-8, length 2)

Among these, the shortest ones have length 2. There are multiple occurrences of "11", but they're all the same string, so we return "11".

Solution Implementation

1class Solution:
2    def shortestBeautifulSubstring(self, s: str, k: int) -> str:
3        """
4        Find the shortest substring containing exactly k ones.
5        If multiple exist with same length, return the lexicographically smallest.
6      
7        Args:
8            s: Binary string containing only '0' and '1'
9            k: Target number of '1's in the substring
10      
11        Returns:
12            Shortest beautiful substring or empty string if none exists
13        """
14        n = len(s)
15        result = ""
16      
17        # Try all possible starting positions
18        for start in range(n):
19            # Try all possible ending positions (minimum length k to contain k ones)
20            for end in range(start + k, n + 1):
21                # Extract current substring
22                current_substring = s[start:end]
23              
24                # Check if current substring has exactly k ones
25                if current_substring.count("1") == k:
26                    # Update result if:
27                    # 1. Result is empty (first valid substring found)
28                    # 2. Current substring is shorter than result
29                    # 3. Same length but lexicographically smaller
30                    if (not result or 
31                        end - start < len(result) or 
32                        (end - start == len(result) and current_substring < result)):
33                        result = current_substring
34      
35        return result
36
1class Solution {
2    public String shortestBeautifulSubstring(String s, int k) {
3        int stringLength = s.length();
4        String result = "";
5      
6        // Iterate through all possible starting positions
7        for (int startIndex = 0; startIndex < stringLength; ++startIndex) {
8            // Try all possible ending positions (minimum length k to contain k ones)
9            for (int endIndex = startIndex + k; endIndex <= stringLength; ++endIndex) {
10                // Extract the current substring
11                String currentSubstring = s.substring(startIndex, endIndex);
12              
13                // Count the number of '1's in the current substring
14                int onesCount = 0;
15                for (char character : currentSubstring.toCharArray()) {
16                    onesCount += character - '0';  // Convert '1' to 1, '0' to 0
17                }
18              
19                // Check if this substring has exactly k ones and is better than current result
20                if (onesCount == k && 
21                    (result.isEmpty() ||                                    // First valid substring found
22                     endIndex - startIndex < result.length() ||             // Shorter than current result
23                     (endIndex - startIndex == result.length() &&          // Same length but
24                      currentSubstring.compareTo(result) < 0))) {           // lexicographically smaller
25                    result = currentSubstring;
26                }
27            }
28        }
29      
30        return result;
31    }
32}
33
1class Solution {
2public:
3    string shortestBeautifulSubstring(string s, int k) {
4        int stringLength = s.size();
5        string result = "";
6      
7        // Iterate through all possible starting positions
8        for (int startPos = 0; startPos < stringLength; ++startPos) {
9            // Try all possible ending positions (minimum length is k to have k ones)
10            for (int endPos = startPos + k; endPos <= stringLength; ++endPos) {
11                // Extract the current substring
12                string currentSubstring = s.substr(startPos, endPos - startPos);
13              
14                // Count the number of '1's in the current substring
15                int onesCount = count(currentSubstring.begin(), currentSubstring.end(), '1');
16              
17                // Check if this substring is a valid beautiful substring
18                if (onesCount == k) {
19                    // Update result if:
20                    // 1. No result found yet (result is empty)
21                    // 2. Current substring is shorter than the existing result
22                    // 3. Same length but lexicographically smaller
23                    if (result == "" || 
24                        endPos - startPos < result.size() || 
25                        (endPos - startPos == result.size() && currentSubstring < result)) {
26                        result = currentSubstring;
27                    }
28                }
29            }
30        }
31      
32        return result;
33    }
34};
35
1/**
2 * Finds the shortest beautiful substring containing exactly k '1's.
3 * If multiple substrings have the same length, returns the lexicographically smallest one.
4 * @param s - The input binary string
5 * @param k - The required number of '1's in the substring
6 * @returns The shortest beautiful substring, or empty string if none exists
7 */
8function shortestBeautifulSubstring(s: string, k: number): string {
9    const stringLength: number = s.length;
10    let shortestSubstring: string = '';
11  
12    // Iterate through all possible starting positions
13    for (let startIndex: number = 0; startIndex < stringLength; startIndex++) {
14        // Check all substrings starting from startIndex with minimum length k
15        for (let endIndex: number = startIndex + k; endIndex <= stringLength; endIndex++) {
16            // Extract the current substring
17            const currentSubstring: string = s.slice(startIndex, endIndex);
18          
19            // Count the number of '1's in the current substring
20            const onesCount: number = currentSubstring
21                .split('')
22                .filter((char: string) => char === '1')
23                .length;
24          
25            // Check if current substring is valid and better than the current answer
26            const isValidSubstring: boolean = onesCount === k;
27            const isFirstValid: boolean = shortestSubstring === '';
28            const isShorter: boolean = endIndex - startIndex < shortestSubstring.length;
29            const isSameLengthButSmaller: boolean = 
30                endIndex - startIndex === shortestSubstring.length && 
31                currentSubstring < shortestSubstring;
32          
33            if (isValidSubstring && (isFirstValid || isShorter || isSameLengthButSmaller)) {
34                shortestSubstring = currentSubstring;
35            }
36        }
37    }
38  
39    return shortestSubstring;
40}
41

Time and Space Complexity

Time Complexity: O(n^3)

The algorithm uses two nested loops to generate all possible substrings:

  • The outer loop runs from i = 0 to n-1, contributing O(n)
  • The inner loop runs from j = i+k to n, contributing O(n) in the worst case
  • For each substring s[i:j], we perform:
    • String slicing operation s[i:j]: O(j-i) which is O(n) in the worst case
    • Counting "1"s with t.count("1"): O(j-i) which is O(n) in the worst case
    • String comparison t < ans: O(min(len(t), len(ans))) which is O(n) in the worst case

Therefore, the total time complexity is O(n) × O(n) × O(n) = O(n^3).

Space Complexity: O(n)

The space usage includes:

  • Variable ans stores a substring of maximum length n: O(n)
  • Variable t temporarily stores substrings during iteration: O(n) in the worst case
  • Other variables (n, i, j) use constant space: O(1)

The dominant space usage is O(n) for storing the substrings, resulting in an overall space complexity of O(n).

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

Common Pitfalls

1. Inefficient Early Termination

The current solution checks all possible substrings even after finding one with exactly k ones at a given starting position. Once we find a beautiful substring starting at position i, we could optimize by breaking the inner loop since any longer substring from the same starting position cannot be better (it won't be shorter).

Problem Code:

for start in range(n):
    for end in range(start + k, n + 1):
        current_substring = s[start:end]
        if current_substring.count("1") == k:
            # Updates result but continues checking longer substrings

Improved Solution:

for start in range(n):
    for end in range(start + k, n + 1):
        current_substring = s[start:end]
        if current_substring.count("1") == k:
            if (not result or 
                end - start < len(result) or 
                (end - start == len(result) and current_substring < result)):
                result = current_substring
            break  # No need to check longer substrings from this start position

2. Redundant String Operations

The solution creates substring objects and counts ones repeatedly, which can be optimized using a running count approach.

Problem: Creating substrings and counting '1's for each substring is expensive.

Optimized Approach Using Sliding Window:

class Solution:
    def shortestBeautifulSubstring(self, s: str, k: int) -> str:
        n = len(s)
        result = ""
        min_length = float('inf')
      
        # Use sliding window for better efficiency
        for start in range(n):
            ones_count = 0
            for end in range(start, n):
                if s[end] == '1':
                    ones_count += 1
              
                # When we have exactly k ones
                if ones_count == k:
                    current_length = end - start + 1
                    current_substring = s[start:end + 1]
                  
                    # Update result based on length and lexicographical order
                    if (current_length < min_length or 
                        (current_length == min_length and 
                         (not result or current_substring < result))):
                        result = current_substring
                        min_length = current_length
                    break  # Found k ones, no need to extend further
              
                # Early termination if we exceed k ones
                elif ones_count > k:
                    break
      
        return result

3. Memory Inefficiency with Substring Storage

Storing all candidate substrings before comparison wastes memory. The solution should only keep track of the best substring found so far.

Problem: Some implementations might store all beautiful substrings first, then find the minimum:

# Inefficient approach
beautiful_substrings = []
for start in range(n):
    for end in range(start + k, n + 1):
        substring = s[start:end]
        if substring.count("1") == k:
            beautiful_substrings.append(substring)

# Then find minimum - wastes memory
if beautiful_substrings:
    return min(beautiful_substrings, key=lambda x: (len(x), x))

The original solution correctly avoids this by maintaining only the best answer found so far.

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

Which data structure is used to implement priority queue?


Recommended Readings

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

Load More