Facebook Pixel

1208. Get Equal Substrings Within Budget

Problem Description

You are given two strings s and t that have the same length, along with an integer maxCost.

Your goal is to transform string s into string t. The cost to change the i-th character of s to the i-th character of t is calculated as |s[i] - t[i]|, which represents the absolute difference between the ASCII values of the two characters.

You need to find the maximum length of a substring from s that can be changed to match the corresponding substring in t while keeping the total transformation cost less than or equal to maxCost.

For example, if you select a substring from position i to position j in string s, the total cost to transform this substring would be the sum of |s[k] - t[k]| for all k from i to j. This total cost must not exceed maxCost.

The function should return the maximum possible length of such a substring. If no substring can be transformed within the cost limit (even a single character transformation exceeds maxCost), return 0.

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

Intuition

The key insight is recognizing that if we can transform a substring of length x within the cost limit, then we can definitely transform any substring of length x-1 or smaller within that same limit (we can simply use a portion of the original substring). This property is called monotonicity - as the substring length decreases, it becomes easier to stay within the cost constraint.

This monotonic property immediately suggests using binary search on the answer. Instead of checking every possible substring length from 1 to n, we can binary search for the maximum valid length.

To efficiently calculate the cost of transforming any substring, we can use prefix sums. If we precompute an array f where f[i] stores the cumulative cost of transforming characters from index 0 to i-1, then the cost of transforming a substring from index i to j can be calculated in O(1) time as f[j+1] - f[i].

The binary search works by repeatedly checking if a certain length mid is achievable:

  • If we can find at least one substring of length mid with cost ≤ maxCost, then mid is valid and we should search for potentially larger lengths
  • If no substring of length mid satisfies the constraint, then we need to search for smaller lengths

For each candidate length during binary search, we slide a window of that length across the string and check if any position gives us a valid transformation cost using our prefix sum array. This allows us to efficiently determine whether a given length is achievable.

The combination of binary search (O(log n)) with the sliding window check (O(n) per check) gives us an overall time complexity of O(n log n), which is much better than the brute force approach of checking all possible substrings.

Learn more about Binary Search, Prefix Sum and Sliding Window patterns.

Solution Implementation

1class Solution:
2    def equalSubstring(self, s: str, t: str, maxCost: int) -> int:
3        string_length = len(s)
4
5        # Build prefix sum array for costs
6        costs = (abs(ord(char_s) - ord(char_t)) for char_s, char_t in zip(s, t))
7        prefix_sum = list(accumulate(costs, initial=0))
8
9        def feasible(length: int) -> bool:
10            """
11            Returns True if we CANNOT achieve this length (all windows exceed budget).
12            This inverts the problem so we can use the standard binary search template.
13            """
14            for start_idx in range(string_length - length + 1):
15                window_cost = prefix_sum[start_idx + length] - prefix_sum[start_idx]
16                if window_cost <= maxCost:
17                    return False  # Found a valid window, so NOT infeasible
18            return True  # No valid window found, this length is infeasible
19
20        # Binary search for the first length that is NOT achievable
21        # Pattern: [false, false, false, true, true, true]
22        # We want the last false, which is first_true_index - 1
23        left, right = 1, string_length + 1
24        first_true_index = string_length + 1  # Default: all lengths achievable
25
26        while left <= right:
27            mid = (left + right) // 2
28
29            if feasible(mid):
30                first_true_index = mid
31                right = mid - 1
32            else:
33                left = mid + 1
34
35        # The answer is the largest achievable length = first_true_index - 1
36        return first_true_index - 1
37
1class Solution {
2    private int maxCost;
3    private int[] prefixSum;
4    private int stringLength;
5
6    public int equalSubstring(String s, String t, int maxCost) {
7        stringLength = s.length();
8        prefixSum = new int[stringLength + 1];
9        this.maxCost = maxCost;
10
11        // Build prefix sum array
12        for (int i = 0; i < stringLength; i++) {
13            int cost = Math.abs(s.charAt(i) - t.charAt(i));
14            prefixSum[i + 1] = prefixSum[i] + cost;
15        }
16
17        // Binary search for the first length that is NOT achievable
18        int left = 1;
19        int right = stringLength + 1;
20        int firstTrueIndex = stringLength + 1;
21
22        while (left <= right) {
23            int mid = left + (right - left) / 2;
24
25            if (feasible(mid)) {
26                firstTrueIndex = mid;
27                right = mid - 1;
28            } else {
29                left = mid + 1;
30            }
31        }
32
33        // The answer is the largest achievable length
34        return firstTrueIndex - 1;
35    }
36
37    /**
38     * Returns true if we CANNOT achieve this length (all windows exceed budget).
39     */
40    private boolean feasible(int length) {
41        if (length > stringLength) return true;
42        for (int start = 0; start + length <= stringLength; start++) {
43            int cost = prefixSum[start + length] - prefixSum[start];
44            if (cost <= maxCost) {
45                return false;  // Found valid window, not infeasible
46            }
47        }
48        return true;  // No valid window, this length is infeasible
49    }
50}
51
1class Solution {
2public:
3    int equalSubstring(string s, string t, int maxCost) {
4        int n = s.size();
5
6        // Build prefix sum array
7        vector<int> prefixSum(n + 1, 0);
8        for (int i = 0; i < n; ++i) {
9            prefixSum[i + 1] = prefixSum[i] + abs(s[i] - t[i]);
10        }
11
12        // Feasible: returns true if we CANNOT achieve this length
13        auto feasible = [&](int length) -> bool {
14            if (length > n) return true;
15            for (int start = 0; start + length <= n; ++start) {
16                int cost = prefixSum[start + length] - prefixSum[start];
17                if (cost <= maxCost) {
18                    return false;  // Found valid window
19                }
20            }
21            return true;  // No valid window
22        };
23
24        // Binary search for the first infeasible length
25        int left = 1, right = n + 1;
26        int firstTrueIndex = n + 1;
27
28        while (left <= right) {
29            int mid = left + (right - left) / 2;
30            if (feasible(mid)) {
31                firstTrueIndex = mid;
32                right = mid - 1;
33            } else {
34                left = mid + 1;
35            }
36        }
37
38        return firstTrueIndex - 1;
39    }
40};
41
1function equalSubstring(s: string, t: string, maxCost: number): number {
2    const n: number = s.length;
3
4    // Build prefix sum array
5    const prefixSum: number[] = Array(n + 1).fill(0);
6    for (let i = 0; i < n; i++) {
7        const changeCost: number = Math.abs(s.charCodeAt(i) - t.charCodeAt(i));
8        prefixSum[i + 1] = prefixSum[i] + changeCost;
9    }
10
11    // Feasible: returns true if we CANNOT achieve this length
12    const feasible = (length: number): boolean => {
13        if (length > n) return true;
14        for (let start = 0; start + length <= n; start++) {
15            const cost = prefixSum[start + length] - prefixSum[start];
16            if (cost <= maxCost) {
17                return false;  // Found valid window
18            }
19        }
20        return true;  // No valid window
21    };
22
23    // Binary search for the first infeasible length
24    let left: number = 1;
25    let right: number = n + 1;
26    let firstTrueIndex: number = n + 1;
27
28    while (left <= right) {
29        const mid: number = Math.floor((left + right) / 2);
30
31        if (feasible(mid)) {
32            firstTrueIndex = mid;
33            right = mid - 1;
34        } else {
35            left = mid + 1;
36        }
37    }
38
39    return firstTrueIndex - 1;
40}
41

Solution Approach

The solution uses the binary search template with prefix sum for efficient cost calculation.

1. Building the Prefix Sum Array:

prefix_sum = list(accumulate(costs, initial=0))

This creates an array where prefix_sum[i] represents the total cost of transforming the first i characters. For any substring from index i to j, the cost can be calculated as prefix_sum[j+1] - prefix_sum[i].

2. Define the Feasible Function (Inverted for Maximum Search): Since we're searching for the maximum valid length, we invert the feasible function. Instead of "can we achieve length X?", we define feasible(x) as "can we NOT achieve length X?":

def feasible(length):
    for start in range(n - length + 1):
        cost = prefix_sum[start + length] - prefix_sum[start]
        if cost <= maxCost:
            return False  # Found valid window, NOT infeasible
    return True  # No valid window, this length IS infeasible

This creates a pattern like: [false, false, false, true, true] where false means "achievable" and true means "not achievable".

3. Binary Search Using the Template:

left, right = 1, n + 1
first_true_index = n + 1  # Default if all lengths achievable

while left <= right:
    mid = (left + right) // 2
    if feasible(mid):
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1

return first_true_index - 1

The template finds the first infeasible length. The answer is first_true_index - 1 (the last achievable length).

Why this works:

  • The inverted feasible function creates a monotonic [false, ..., true, ...] pattern
  • The template finds the first true (first length we can't achieve)
  • The maximum achievable length is one less than the first infeasible length

The time complexity is O(n log n) and space complexity is O(n).

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 a concrete example to illustrate the solution approach.

Given:

  • s = "abcd"
  • t = "bcdf"
  • maxCost = 3

Step 1: Calculate transformation costs and build prefix sum array

First, let's calculate the cost for each character:

  • Position 0: |'a' - 'b'| = |97 - 98| = 1
  • Position 1: |'b' - 'c'| = |98 - 99| = 1
  • Position 2: |'c' - 'd'| = |99 - 100| = 1
  • Position 3: |'d' - 'f'| = |100 - 102| = 2

Prefix sum array f = [0, 1, 2, 3, 5]:

  • f[0] = 0 (no characters)
  • f[1] = 0 + 1 = 1 (cost of first character)
  • f[2] = 1 + 1 = 2 (cost of first two characters)
  • f[3] = 2 + 1 = 3 (cost of first three characters)
  • f[4] = 3 + 2 = 5 (cost of all four characters)

Step 2: Binary Search

Initialize: l = 0, r = 4 (length of string)

Iteration 1:

  • mid = (0 + 4 + 1) >> 1 = 2
  • Check if length 2 is valid:
    • Substring [0,1]: cost = f[2] - f[0] = 2 - 0 = 2 ≤ 3
    • Found valid substring, so length 2 is achievable
  • Update: l = 2 (search for larger lengths)

Iteration 2:

  • mid = (2 + 4 + 1) >> 1 = 3
  • Check if length 3 is valid:
    • Substring [0,2]: cost = f[3] - f[0] = 3 - 0 = 3 ≤ 3
    • Found valid substring, so length 3 is achievable
  • Update: l = 3 (search for larger lengths)

Iteration 3:

  • mid = (3 + 4 + 1) >> 1 = 4
  • Check if length 4 is valid:
    • Substring [0,3]: cost = f[4] - f[0] = 5 - 0 = 5 > 3
    • No valid substring of length 4
  • Update: r = 3 (search for smaller lengths)

Termination:

  • l = 3, r = 3 (converged)
  • Return 3

Answer: 3

The maximum length substring that can be transformed within cost 3 is length 3. We can transform "abc" to "bcd" with total cost 3.

Time and Space Complexity

Time Complexity: O(n × log n)

The algorithm uses binary search on the answer space (the length of the substring). The binary search runs for O(log n) iterations since we're searching in the range [0, n] where n is the length of the string.

For each binary search iteration, the check(mid) function is called. Inside check(), there's a loop that iterates through all possible starting positions i from 0 to n-1, performing O(1) operations in each iteration (checking if j < n and if f[j + 1] - f[i] <= maxCost). Therefore, check() has a time complexity of O(n).

Additionally, before the binary search, we compute the prefix sum array f using accumulate(), which takes O(n) time.

Overall time complexity: O(n) for preprocessing + O(log n) binary search iterations × O(n) per check = O(n × log n)

Space Complexity: O(n)

The algorithm uses additional space for:

  • The prefix sum array f which stores n + 1 elements (including the initial 0), requiring O(n) space
  • A few variables (l, r, mid, n) which use O(1) space

Overall space complexity: O(n)

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

Common Pitfalls

1. Using Wrong Binary Search Template Variant

The Pitfall: For "find maximum" problems, a common mistake is using the non-standard while left < right with left = mid variant, which requires careful midpoint calculation to avoid infinite loops.

Example of the Problem:

# Non-standard variant - prone to infinite loops
while left < right:
    mid = (left + right + 1) >> 1  # Must use +1 to avoid infinite loop
    if can_achieve_length(mid):
        left = mid
    else:
        right = mid - 1

The Solution: Invert the feasible function and use the standard template:

def feasible(length):
    # Returns True if we CANNOT achieve this length
    ...

first_true_index = n + 1
while left <= right:
    mid = (left + right) // 2
    if feasible(mid):
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1
return first_true_index - 1

2. Incorrect Prefix Sum Index Mapping

The Pitfall: Confusing the indexing when calculating substring costs. Remember that prefix_sum[i] represents the sum of elements from index 0 to i-1.

Example of the Problem:

# Incorrect calculation
window_cost = prefix_sum[end_idx] - prefix_sum[start_idx + 1]  # Wrong!

The Solution: For a substring from index start to start + length - 1:

window_cost = prefix_sum[start + length] - prefix_sum[start]

3. Not Inverting the Feasible Function for Maximum Search

The Pitfall: Trying to directly search for the maximum using the standard template without inverting the feasible function.

The Solution: For "find maximum" problems, define feasible(x) as "x is NOT achievable" so the pattern becomes [false, false, true, true]. The answer is first_true_index - 1.

4. Forgetting Edge Cases

The Pitfall: Not handling cases where even a single character transformation exceeds maxCost, or when maxCost is 0.

The Solution: The template naturally handles these:

  • If no valid substring exists, first_true_index will be 1, returning 0
  • Set default first_true_index = n + 1 to handle case where all lengths are achievable
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?


Recommended Readings

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

Load More