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.
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
midwith cost ≤maxCost, thenmidis valid and we should search for potentially larger lengths - If no substring of length
midsatisfies 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
371class 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}
511class 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};
411function 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}
41Solution 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 EvaluatorExample 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
- Substring [0,1]: cost =
- 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
- Substring [0,2]: cost =
- 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
- Substring [0,3]: cost =
- 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
fwhich storesn + 1elements (including the initial 0), requiringO(n)space - A few variables (
l,r,mid,n) which useO(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_indexwill be 1, returning 0 - Set default
first_true_index = n + 1to handle case where all lengths are achievable
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
Recommended Readings
https assets algo monster cover_photos Binary_Search svg Binary Search Intuition Binary search is an efficient array search algorithm It works by narrowing down the search range by half each time If you have looked up a word in a physical dictionary you've already used binary search in real life Let's
Prefix Sum Given an array of integers arr and a target value find a subarray that sums to the target Return the start and end indices of the subarray Input arr 1 20 3 30 5 4 target 7 Output 1 4 Explanation The subarray 20 3 30 from index 1 inclusive
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Want a Structured Path to Master System Design Too? Don’t Miss This!