Partition String Into Minimum Beautiful Substrings

MEDIUM

Description

Given a binary string s, partition the string into one or more substrings such that each substring is beautiful.

A string is beautiful if:

  • It doesn't contain leading zeros.
  • It's the binary representation of a number that is a power of 5.

Return the minimum number of substrings in such partition. If it is impossible to partition the string s into beautiful substrings, return -1.

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

 

Example 1:

Input: s = "1011"
Output: 2
Explanation: We can paritition the given string into ["101", "1"].
- The string "101" does not contain leading zeros and is the binary representation of integer 51 = 5.
- The string "1" does not contain leading zeros and is the binary representation of integer 50 = 1.
It can be shown that 2 is the minimum number of beautiful substrings that s can be partitioned into.

Example 2:

Input: s = "111"
Output: 3
Explanation: We can paritition the given string into ["1", "1", "1"].
- The string "1" does not contain leading zeros and is the binary representation of integer 50 = 1.
It can be shown that 3 is the minimum number of beautiful substrings that s can be partitioned into.

Example 3:

Input: s = "0"
Output: -1
Explanation: We can not partition the given string into beautiful substrings.

 

Constraints:

  • 1 <= s.length <= 15
  • s[i] is either '0' or '1'.

Approaches

Checkout 2 different approaches to solve Partition String Into Minimum Beautiful Substrings. Click on different approaches to view the approach and algorithm in detail.

Dynamic Programming (Memoization / Iterative)

This approach uses dynamic programming to solve the problem efficiently by breaking it down into overlapping subproblems and solving each subproblem only once. We can implement this using either a top-down (memoization) or a bottom-up (iterative) method. Both have the same time complexity and are optimal for this problem.

Algorithm

  • The core idea is to define a state dp[i] representing the minimum number of beautiful partitions for the suffix of the string, s[i..n-1].
  • The goal is to find dp[0].
  • Base Case: dp[n] = 0, as an empty suffix requires 0 partitions.
  • Recurrence Relation: dp[i] = min(1 + dp[j+1]) over all j from i to n-1 such that the substring s[i..j] is beautiful. If no such j exists, dp[i] is infinity.
  • This can be implemented in two ways:
    • Top-Down (Memoization): Use a recursive function with a memoization table memo to store results of dp[i]. Before computing, check memo[i]. After computing, store the result in memo[i].
    • Bottom-Up (Iterative): Use an array dp of size n+1. Iterate i from n-1 down to 0. For each i, calculate dp[i] using the already computed values dp[j+1].

The brute-force approach is inefficient because it repeatedly solves the same subproblems. We can optimize this by storing the results of subproblems. Let dp[i] be the minimum number of beautiful partitions for the suffix s[i..n-1]. Our goal is to find dp[0].

Top-Down DP (Memoization) This is a direct optimization of the recursive solution. We use a memoization array (e.g., memo) to store the results of solve(i). Before computing solve(i), we check if the result is already in memo. If so, we return it. Otherwise, we compute it, store it in memo, and then return it. This ensures each state is computed only once.

// Top-Down DP with Memoization
class Solution {
    int[] memo;
    public int beautifulPartitions(String s) {
        int n = s.length();
        memo = new int[n + 1];
        java.util.Arrays.fill(memo, -1);
        int result = solve(s, 0);
        return result >= 1_000_000 ? -1 : result;
    }

    private int solve(String s, int i) {
        if (i == s.length()) return 0;
        if (memo[i] != -1) return memo[i];
        if (s.charAt(i) == '0') return memo[i] = 1_000_000;

        int res = 1_000_000;
        long val = 0;
        for (int j = i; j < s.length(); j++) {
            val = val * 2 + (s.charAt(j) - '0');
            if (isPowerOfFive(val)) {
                res = Math.min(res, 1 + solve(s, j + 1));
            }
        }
        return memo[i] = res;
    }

    private boolean isPowerOfFive(long n) { /* ... */ }
}

Bottom-Up DP (Iterative) This approach builds the solution iteratively, eliminating recursion. We create a dp array of size n+1, initialize dp[n] = 0 and the rest to infinity. We loop from i = n-1 down to 0. For each i, we calculate dp[i] using the values of dp[j+1] that have already been computed.

// Bottom-Up Iterative DP
class Solution {
    public int beautifulPartitions(String s) {
        int n = s.length();
        int[] dp = new int[n + 1];
        java.util.Arrays.fill(dp, 1_000_000);
        dp[n] = 0;

        for (int i = n - 1; i >= 0; i--) {
            if (s.charAt(i) == '0') continue;
            long val = 0;
            for (int j = i; j < n; j++) {
                val = val * 2 + (s.charAt(j) - '0');
                if (isPowerOfFive(val)) {
                    if (dp[j + 1] < 1_000_000) {
                        dp[i] = Math.min(dp[i], 1 + dp[j + 1]);
                    }
                }
            }
        }
        return dp[0] >= 1_000_000 ? -1 : dp[0];
    }

    private boolean isPowerOfFive(long n) { /* ... */ }
}

Complexity Analysis

Time Complexity: O(n^2). There are n states (subproblems) to solve. For each state `i`, we iterate from `i` to `n-1`, which is an O(n) loop. The operations inside the loop are constant time.Space Complexity: O(n), where n is the length of the string. We use an array of size n for the DP table. The recursive version also uses O(n) space for the call stack.

Pros and Cons

Pros:
  • Highly efficient and optimal for this problem.
  • Guarantees that each subproblem is solved only once.
  • The iterative version avoids recursion overhead and potential stack overflow issues on larger inputs.
Cons:
  • Requires extra space for the DP table/memoization cache.
  • The logic can be slightly more complex to formulate compared to a simple brute-force approach.

Code Solutions

Checking out 3 solutions in different languages for Partition String Into Minimum Beautiful Substrings. Click on different languages to view the code.

class Solution {
private
  Integer[] f;
private
  String s;
private
  Set<Long> ss = new HashSet<>();
private
  int n;
public
  int minimumBeautifulSubstrings(String s) {
    n = s.length();
    this.s = s;
    f = new Integer[n];
    long x = 1;
    for (int i = 0; i <= n; ++i) {
      ss.add(x);
      x *= 5;
    }
    int ans = dfs(0);
    return ans > n ? -1 : ans;
  }
private
  int dfs(int i) {
    if (i >= n) {
      return 0;
    }
    if (s.charAt(i) == '0') {
      return n + 1;
    }
    if (f[i] != null) {
      return f[i];
    }
    long x = 0;
    int ans = n + 1;
    for (int j = i; j < n; ++j) {
      x = x << 1 | (s.charAt(j) - '0');
      if (ss.contains(x)) {
        ans = Math.min(ans, 1 + dfs(j + 1));
      }
    }
    return f[i] = ans;
  }
}

Video Solution

Watch the video walkthrough for Partition String Into Minimum Beautiful Substrings



Patterns:

Dynamic ProgrammingBacktracking

Data Structures:

Hash TableString

Subscribe to Scale Engineer newsletter

Learn about System Design, Software Engineering, and interview experiences every week.

No spam, unsubscribe at any time.