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
mid
with cost ≤maxCost
, thenmid
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 Approach
The solution implements the binary search with prefix sum strategy discussed earlier. Let's walk through the implementation step by step:
1. Building the Prefix Sum Array:
f = list(accumulate((abs(ord(a) - ord(b)) for a, b in zip(s, t)), initial=0))
This creates an array f
where f[i]
represents the total cost of transforming the first i
characters. The accumulate
function with initial=0
ensures f[0] = 0
, and each subsequent element is the cumulative sum of transformation costs. For any substring from index i
to j
, the transformation cost can be calculated as f[j+1] - f[i]
.
2. Binary Search Setup:
l, r = 0, n
We initialize the search range where l = 0
(minimum possible length) and r = n
(maximum possible length, which is the entire string).
3. The Check Function:
def check(x):
for i in range(n):
j = i + mid - 1
if j < n and f[j + 1] - f[i] <= maxCost:
return True
return False
This function determines if there exists any substring of length x
that can be transformed within maxCost
. It iterates through all possible starting positions i
and checks if the substring from i
to i + x - 1
has a transformation cost ≤ maxCost
. The cost is calculated using the prefix sum: f[j+1] - f[i]
where j = i + x - 1
.
4. Binary Search Loop:
while l < r: mid = (l + r + 1) >> 1 if check(mid): l = mid else: r = mid - 1
The binary search continues while l < r
:
- Calculate
mid = (l + r + 1) >> 1
(equivalent to(l + r + 1) // 2
). The+1
ensures we round up to avoid infinite loops whenl
andr
are adjacent. - If
check(mid)
returnsTrue
, a substring of lengthmid
is valid, so we search for potentially larger lengths by settingl = mid
. - If
check(mid)
returnsFalse
, lengthmid
is too large, so we search for smaller lengths by settingr = mid - 1
.
5. Return the Result:
After the binary search completes, l
contains the maximum valid substring length, which is returned as the answer.
The time complexity is O(n log n) where the binary search takes O(log n) iterations, and each check
function call takes O(n) time. The space complexity is O(n) for storing the prefix sum array.
Ready to land your dream job?
Unlock your dream job with a 3-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.
Solution Implementation
1class Solution:
2 def equalSubstring(self, s: str, t: str, maxCost: int) -> int:
3 """
4 Find the maximum length of a substring that can be changed from s to t
5 within the given maxCost budget.
6
7 Args:
8 s: Source string
9 t: Target string
10 maxCost: Maximum cost allowed for character changes
11
12 Returns:
13 Maximum length of substring that can be changed within budget
14 """
15
16 def can_achieve_length(length):
17 """
18 Check if we can find a substring of given length that costs <= maxCost.
19
20 Args:
21 length: The substring length to check
22
23 Returns:
24 True if such substring exists, False otherwise
25 """
26 # Check all possible windows of size 'length'
27 for start_idx in range(string_length):
28 end_idx = start_idx + length - 1
29
30 # Check if window is within bounds and cost is within budget
31 if end_idx < string_length:
32 # Calculate cost for substring [start_idx, end_idx]
33 window_cost = prefix_sum[end_idx + 1] - prefix_sum[start_idx]
34 if window_cost <= maxCost:
35 return True
36 return False
37
38 # Initialize variables
39 string_length = len(s)
40
41 # Build prefix sum array for costs
42 # prefix_sum[i] = sum of costs from index 0 to i-1
43 costs = (abs(ord(char_s) - ord(char_t)) for char_s, char_t in zip(s, t))
44 prefix_sum = list(accumulate(costs, initial=0))
45
46 # Binary search for maximum valid substring length
47 left, right = 0, string_length
48
49 while left < right:
50 # Try the larger half (ceiling division)
51 mid = (left + right + 1) >> 1
52
53 if can_achieve_length(mid):
54 # If current length is achievable, try larger lengths
55 left = mid
56 else:
57 # If current length is not achievable, try smaller lengths
58 right = mid - 1
59
60 return left
61
1class Solution {
2 private int maxCost;
3 private int[] prefixSum; // Cumulative cost array
4 private int stringLength;
5
6 public int equalSubstring(String s, String t, int maxCost) {
7 // Initialize instance variables
8 stringLength = s.length();
9 prefixSum = new int[stringLength + 1];
10 this.maxCost = maxCost;
11
12 // Build prefix sum array where prefixSum[i+1] stores the cumulative cost
13 // of changing characters from index 0 to i
14 for (int i = 0; i < stringLength; i++) {
15 int cost = Math.abs(s.charAt(i) - t.charAt(i));
16 prefixSum[i + 1] = prefixSum[i] + cost;
17 }
18
19 // Binary search for the maximum valid substring length
20 int left = 0;
21 int right = stringLength;
22
23 while (left < right) {
24 // Calculate mid point, avoiding overflow
25 int mid = (left + right + 1) >>> 1;
26
27 // If a substring of length mid exists within budget, search for longer
28 if (canFindValidSubstring(mid)) {
29 left = mid;
30 } else {
31 // Otherwise, search for shorter substring
32 right = mid - 1;
33 }
34 }
35
36 return left;
37 }
38
39 /**
40 * Checks if there exists a substring of given length that can be changed
41 * within the maxCost budget
42 *
43 * @param substringLength the length of substring to check
44 * @return true if such substring exists, false otherwise
45 */
46 private boolean canFindValidSubstring(int substringLength) {
47 // Try all possible starting positions for a substring of given length
48 for (int startIndex = 0; startIndex + substringLength - 1 < stringLength; startIndex++) {
49 int endIndex = startIndex + substringLength - 1;
50
51 // Calculate cost using prefix sum: cost = prefixSum[end+1] - prefixSum[start]
52 int totalCost = prefixSum[endIndex + 1] - prefixSum[startIndex];
53
54 // If cost is within budget, we found a valid substring
55 if (totalCost <= maxCost) {
56 return true;
57 }
58 }
59
60 return false;
61 }
62}
63
1class Solution {
2public:
3 int equalSubstring(string s, string t, int maxCost) {
4 int n = s.size();
5
6 // Build prefix sum array to store cumulative costs
7 // prefixSum[i] = total cost to change s[0...i-1] to t[0...i-1]
8 int prefixSum[n + 1];
9 prefixSum[0] = 0;
10 for (int i = 0; i < n; ++i) {
11 prefixSum[i + 1] = prefixSum[i] + abs(s[i] - t[i]);
12 }
13
14 // Check if a substring of given length can be made equal within maxCost
15 auto canMakeEqual = [&](int length) -> bool {
16 // Try all possible substrings of the given length
17 for (int start = 0; start + length - 1 < n; ++start) {
18 int end = start + length - 1;
19 // Calculate cost for substring [start, end] using prefix sum
20 int cost = prefixSum[end + 1] - prefixSum[start];
21 if (cost <= maxCost) {
22 return true;
23 }
24 }
25 return false;
26 };
27
28 // Binary search for the maximum valid substring length
29 int left = 0, right = n;
30 while (left < right) {
31 // Use upper mid to avoid infinite loop when left = right - 1
32 int mid = (left + right + 1) >> 1;
33 if (canMakeEqual(mid)) {
34 // If current length is valid, try longer substrings
35 left = mid;
36 } else {
37 // If current length is invalid, try shorter substrings
38 right = mid - 1;
39 }
40 }
41
42 return left;
43 }
44};
45
1/**
2 * Find the maximum length of a substring that can be changed with a given cost budget
3 * @param s - Source string
4 * @param t - Target string
5 * @param maxCost - Maximum cost allowed for character changes
6 * @returns Maximum length of substring that can be changed within budget
7 */
8function equalSubstring(s: string, t: string, maxCost: number): number {
9 const length: number = s.length;
10
11 // Build prefix sum array to store cumulative costs
12 // prefixSum[i] represents total cost to change characters from index 0 to i-1
13 const prefixSum: number[] = Array(length + 1).fill(0);
14
15 // Calculate prefix sums of character change costs
16 for (let i = 0; i < length; i++) {
17 const changeCost: number = Math.abs(s.charCodeAt(i) - t.charCodeAt(i));
18 prefixSum[i + 1] = prefixSum[i] + changeCost;
19 }
20
21 /**
22 * Check if a substring of given length can be changed within budget
23 * @param substringLength - Length of substring to check
24 * @returns True if any substring of this length fits within budget
25 */
26 const canChangeSubstring = (substringLength: number): boolean => {
27 // Try all possible starting positions for substring of given length
28 for (let startIndex = 0; startIndex + substringLength - 1 < length; startIndex++) {
29 const endIndex: number = startIndex + substringLength;
30 const totalCost: number = prefixSum[endIndex] - prefixSum[startIndex];
31
32 if (totalCost <= maxCost) {
33 return true;
34 }
35 }
36 return false;
37 };
38
39 // Binary search for maximum valid substring length
40 let left: number = 0;
41 let right: number = length;
42
43 while (left < right) {
44 // Calculate mid point (ceil division to favor larger values)
45 const mid: number = (left + right + 1) >> 1;
46
47 if (canChangeSubstring(mid)) {
48 // If mid length is valid, search for potentially longer substrings
49 left = mid;
50 } else {
51 // If mid length exceeds budget, search for shorter substrings
52 right = mid - 1;
53 }
54 }
55
56 return left;
57}
58
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 storesn + 1
elements (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. Off-by-One Error in Binary Search Midpoint Calculation
The Pitfall:
A common mistake is using mid = (left + right) // 2
instead of mid = (left + right + 1) // 2
when the update logic sets left = mid
. This can cause an infinite loop when left
and right
differ by 1.
Example of the Problem:
# Incorrect version that causes infinite loop while left < right: mid = (left + right) // 2 # Wrong! if can_achieve_length(mid): left = mid # When left=3, right=4, mid will always be 3 else: right = mid - 1
When left = 3
and right = 4
, mid
will always equal 3, and if the condition is true, left
stays at 3 forever.
The Solution:
Always use mid = (left + right + 1) // 2
(or (left + right + 1) >> 1
) when your binary search updates with left = mid
. This ensures the midpoint rounds up, preventing infinite loops.
2. Incorrect Prefix Sum Index Mapping
The Pitfall:
Confusing the indexing when calculating substring costs using prefix sums. Remember that prefix_sum[i]
represents the sum of elements from index 0 to i-1 (exclusive of i).
Example of the Problem:
# Incorrect calculation window_cost = prefix_sum[end_idx] - prefix_sum[start_idx + 1] # Wrong! # Or forgetting the bounds check window_cost = prefix_sum[end_idx + 1] - prefix_sum[start_idx] # No bounds check!
The Solution:
For a substring from index start
to end
(inclusive), the cost should be:
window_cost = prefix_sum[end + 1] - prefix_sum[start]
And always verify that end + 1
doesn't exceed the prefix sum array length.
3. Using Sliding Window Instead of Binary Search
The Pitfall: While sliding window seems intuitive for this problem, directly applying it can miss the optimal solution because the window might need to "skip" certain high-cost positions.
Example of the Problem:
# Incorrect sliding window approach
left = 0
max_length = 0
current_cost = 0
for right in range(len(s)):
current_cost += abs(ord(s[right]) - ord(t[right]))
while current_cost > maxCost:
current_cost -= abs(ord(s[left]) - ord(t[left]))
left += 1
max_length = max(max_length, right - left + 1)
This actually works correctly! The confusion arises because both sliding window and binary search are valid approaches for this problem. However, the binary search approach is more generalizable to similar problems where a simple sliding window might not work.
The Solution: Both approaches are valid for this specific problem. Choose based on:
- Sliding window: O(n) time, simpler to implement
- Binary search with prefix sum: O(n log n) time, more generalizable pattern
4. Forgetting Edge Cases
The Pitfall:
Not handling cases where even a single character transformation exceeds maxCost
, or when maxCost
is 0.
Example of the Problem:
# Missing edge case handling if maxCost == 0: # Should check if any position has s[i] == t[i] pass
The Solution: The binary search approach naturally handles these edge cases because:
- If no valid substring exists, the binary search will converge to
left = 0
- The prefix sum approach correctly handles
maxCost = 0
by checking if any substring has zero transformation cost
What's the output of running the following function using the following tree as input?
1def serialize(root):
2 res = []
3 def dfs(root):
4 if not root:
5 res.append('x')
6 return
7 res.append(root.val)
8 dfs(root.left)
9 dfs(root.right)
10 dfs(root)
11 return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4 StringJoiner res = new StringJoiner(" ");
5 serializeDFS(root, res);
6 return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10 if (root == null) {
11 result.add("x");
12 return;
13 }
14 result.add(Integer.toString(root.val));
15 serializeDFS(root.left, result);
16 serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2 let res = [];
3 serialize_dfs(root, res);
4 return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8 if (!root) {
9 res.push("x");
10 return;
11 }
12 res.push(root.val);
13 serialize_dfs(root.left, res);
14 serialize_dfs(root.right, res);
15}
16
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
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!