Find Beautiful Indices in the Given Array II
HardPrompt
You are given a 0-indexed string s, a string a, a string b, and an integer k.
An index i is beautiful if:
0 <= i <= s.length - a.lengths[i..(i + a.length - 1)] == a- There exists an index
jsuch that:0 <= j <= s.length - b.lengths[j..(j + b.length - 1)] == b|j - i| <= k
Return the array that contains beautiful indices in sorted order from smallest to largest.
Example 1:
Input: s = "isawsquirrelnearmysquirrelhouseohmy", a = "my", b = "squirrel", k = 15
Output: [16,33]
Explanation: There are 2 beautiful indices: [16,33].
- The index 16 is beautiful as s[16..17] == "my" and there exists an index 4 with s[4..11] == "squirrel" and |16 - 4| <= 15.
- The index 33 is beautiful as s[33..34] == "my" and there exists an index 18 with s[18..25] == "squirrel" and |33 - 18| <= 15.
Thus we return [16,33] as the result.Example 2:
Input: s = "abcd", a = "a", b = "a", k = 4
Output: [0]
Explanation: There is 1 beautiful index: [0].
- The index 0 is beautiful as s[0..0] == "a" and there exists an index 0 with s[0..0] == "a" and |0 - 0| <= 4.
Thus we return [0] as the result.
Constraints:
1 <= k <= s.length <= 5 * 1051 <= a.length, b.length <= 5 * 105s,a, andbcontain only lowercase English letters.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach is a direct, straightforward implementation based on the problem description. It first finds all starting indices of substrings a and b in s using a naive string matching method. Then, for every occurrence of a at index i, it performs a linear scan through all occurrences of b at index j to check if the distance condition |i - j| <= k is satisfied.
Algorithm
- Initialize two empty lists,
indicesAandindicesB. - Find all occurrences of string
ains:- Iterate from
i = 0tos.length - a.length. - If
s.substring(i, i + a.length)equalsa, additoindicesA.
- Iterate from
- Find all occurrences of string
bins:- Iterate from
j = 0tos.length - b.length. - If
s.substring(j, j + b.length)equalsb, addjtoindicesB.
- Iterate from
- Initialize an empty list
result. - For each index
iinindicesA:- For each index
jinindicesB:- If
|i - j| <= k, additoresultand break the inner loop to check the nexti.
- If
- For each index
- Return
result.
Walkthrough
The method involves three main steps. First, we create a list indicesA by iterating through the string s and checking for a at every possible starting position using substring comparisons. Second, we do the same for string b to populate a list indicesB. Finally, we use nested loops to iterate through indicesA and indicesB. For each pair of indices (i, j), we check if their absolute difference is at most k. If the condition holds, we add i to our result list and move to the next index in indicesA to avoid duplicates.
import java.util.ArrayList;import java.util.List; class Solution { public List<Integer> beautifulIndices(String s, String a, String b, int k) { int n = s.length(); int lenA = a.length(); int lenB = b.length(); List<Integer> indicesA = new ArrayList<>(); List<Integer> indicesB = new ArrayList<>(); for (int i = 0; i <= n - lenA; i++) { if (s.substring(i, i + lenA).equals(a)) { indicesA.add(i); } } for (int i = 0; i <= n - lenB; i++) { if (s.substring(i, i + lenB).equals(b)) { indicesB.add(i); } } List<Integer> result = new ArrayList<>(); for (int i : indicesA) { for (int j : indicesB) { if (Math.abs(i - j) <= k) { result.add(i); break; } } } return result; }}Complexity
Time
O(N*M_a + N*M_b + P*Q), where N is `s.length()`, M_a is `a.length()`, M_b is `b.length()`, P is `|indicesA|`, and Q is `|indicesB|`. In the worst case, this can be O(N^2), which is very inefficient.
Space
O(P + Q), where P and Q are the number of occurrences of `a` and `b` respectively. In the worst case, this can be O(N), where N is the length of `s`.
Trade-offs
Pros
Simple to understand and implement.
Cons
Extremely slow due to nested loops and inefficient string searching.
Will cause a 'Time Limit Exceeded' (TLE) error on platforms with large test cases.
This approach significantly improves performance by using more sophisticated algorithms. It replaces the naive O(N*M) string search with the Knuth-Morris-Pratt (KMP) algorithm, which finds all occurrences in O(N+M) time. After obtaining the sorted lists of indices for a and b, it then uses binary search for each index of a to efficiently find a nearby index of b, reducing the search complexity from quadratic to log-linear.
Algorithm
- Implement a helper function using the Knuth-Morris-Pratt (KMP) algorithm to find all occurrences of a pattern in a text. This involves pre-computing an LPS (Longest Proper Prefix which is also Suffix) array.
- Use the KMP helper to find all starting indices of
ainsand store them in a sorted listindicesA. - Similarly, find all starting indices of
band store them inindicesB. - Initialize an empty list
result. - For each index
iinindicesA:- Define the search range for an index
jfromindicesBas[i - k, i + k]. - Perform a binary search on
indicesBto find if any element exists in this range. A good way is to find the lower bound fori - k. - If an index
jis found inindicesBsuch thati - k <= j <= i + k, additoresult.
- Define the search range for an index
- Return
result.
Walkthrough
The core of this approach is optimization. First, we find all occurrences of a and b using KMP, which is a linear-time string searching algorithm. This gives us two sorted lists, indicesA and indicesB. Then, for each index i in indicesA, we need to check if there's an index j in indicesB within the range [i - k, i + k]. Instead of a linear scan, we can leverage the sorted nature of indicesB and use binary search. Specifically, for each i, we search for the smallest j in indicesB that is at least i - k. If this j exists and is also no more than i + k, we've found a match, and i is a beautiful index.
import java.util.ArrayList;import java.util.List; class Solution { public List<Integer> beautifulIndices(String s, String a, String b, int k) { List<Integer> indicesA = search(s, a); List<Integer> indicesB = search(s, b); List<Integer> result = new ArrayList<>(); if (indicesA.isEmpty() || indicesB.isEmpty()) { return result; } for (int i : indicesA) { int lowerBound = i - k; int upperBound = i + k; int low = 0, high = indicesB.size() - 1; int insertionPoint = indicesB.size(); while (low <= high) { int mid = low + (high - low) / 2; if (indicesB.get(mid) >= lowerBound) { insertionPoint = mid; high = mid - 1; } else { low = mid + 1; } } if (insertionPoint < indicesB.size()) { int j = indicesB.get(insertionPoint); if (j <= upperBound) { result.add(i); } } } return result; } private List<Integer> search(String text, String pattern) { int n = text.length(), m = pattern.length(); List<Integer> result = new ArrayList<>(); if (m == 0 || m > n) return result; int[] lps = computeLPS(pattern); for (int i = 0, j = 0; i < n; ) { if (text.charAt(i) == pattern.charAt(j)) { i++; j++; } if (j == m) { result.add(i - j); j = lps[j - 1]; } else if (i < n && text.charAt(i) != pattern.charAt(j)) { if (j != 0) j = lps[j - 1]; else i++; } } return result; } private int[] computeLPS(String pattern) { int m = pattern.length(); int[] lps = new int[m]; for (int i = 1, length = 0; i < m; ) { if (pattern.charAt(i) == pattern.charAt(length)) { length++; lps[i] = length; i++; } else { if (length != 0) length = lps[length - 1]; else i++; } } return lps; }}Complexity
Time
O(N + M_a + M_b + P * log(Q)), where P is `|indicesA|` and Q is `|indicesB|`. This is dominated by the search part, resulting in an effective complexity of O(N log N).
Space
O(N + M_a + M_b) to store the indices and the LPS arrays for KMP.
Trade-offs
Pros
Much more efficient than the brute-force approach.
Passes most test cases due to the O(N log N) time complexity.
Cons
While much faster than brute force, the
O(N log N)complexity might still be too slow for the tightest time limits.The binary search part can be further optimized to achieve linear time.
This is the most optimal approach, achieving linear time complexity. It uses the KMP algorithm for efficient string searching, just like the previous approach. However, it replaces the binary search step with a more efficient two-pointer technique. Since both indicesA and indicesB are sorted, we can iterate through them in a synchronized manner. For each index i from indicesA, we only need to check a small, sliding window of indices in indicesB. The two-pointer method avoids re-scanning parts of indicesB, leading to a linear time check.
Algorithm
- Use the KMP algorithm to find all occurrences of
aandb, storing them in sorted listsindicesAandindicesB. - Initialize an empty list
result. - Initialize a pointer for
indicesB,ptrB = 0. - Iterate through each index
iinindicesA:- Advance
ptrBforward as long asindicesB[ptrB]is too small, i.e.,indicesB[ptrB] < i - k. - After advancing
ptrB, check if it's still within the bounds ofindicesB. - If it is, check if the current
indicesB[ptrB]is within the valid window, i.e.,|indicesB[ptrB] - i| <= k. - If the condition is met, add
itoresult.
- Advance
- Do not reset
ptrBbetween iterations fori. It will continue from where it left off. - Return
result.
Walkthrough
The overall strategy is to first find all occurrences of a and b using KMP, yielding sorted lists indicesA and indicesB. The key improvement is in how we check the distance condition. We use two pointers, one for indicesA (implicitly via the for-each loop) and one for indicesB (explicitly, ptrB). As we iterate through each i in indicesA, we advance ptrB to the first position where indicesB[ptrB] could potentially be in the valid range [i - k, i + k]. Because i is always increasing, ptrB never needs to move backward. We then just need to check if indicesB[ptrB] is indeed in the range. This single pass over both lists makes the checking phase O(P + Q), leading to an overall linear time solution.
import java.util.ArrayList;import java.util.List; class Solution { public List<Integer> beautifulIndices(String s, String a, String b, int k) { List<Integer> indicesA = search(s, a); List<Integer> indicesB = search(s, b); List<Integer> result = new ArrayList<>(); if (indicesA.isEmpty() || indicesB.isEmpty()) { return result; } int ptrB = 0; for (int i : indicesA) { while (ptrB < indicesB.size() && indicesB.get(ptrB) < i - k) { ptrB++; } if (ptrB < indicesB.size() && Math.abs(indicesB.get(ptrB) - i) <= k) { result.add(i); } } return result; } private List<Integer> search(String text, String pattern) { int n = text.length(), m = pattern.length(); List<Integer> result = new ArrayList<>(); if (m == 0 || m > n) return result; int[] lps = computeLPS(pattern); for (int i = 0, j = 0; i < n; ) { if (text.charAt(i) == pattern.charAt(j)) { i++; j++; } if (j == m) { result.add(i - j); j = lps[j - 1]; } else if (i < n && text.charAt(i) != pattern.charAt(j)) { if (j != 0) j = lps[j - 1]; else i++; } } return result; } private int[] computeLPS(String pattern) { int m = pattern.length(); int[] lps = new int[m]; for (int i = 1, length = 0; i < m; ) { if (pattern.charAt(i) == pattern.charAt(length)) { length++; lps[i] = length; i++; } else { if (length != 0) length = lps[length - 1]; else i++; } } return lps; }}Complexity
Time
O(N + M_a + M_b). The KMP part is O(N + M_a + M_b) and the two-pointer scan is O(P + Q). Since P, Q <= N, the total time is linear.
Space
O(N + M_a + M_b) to store the indices and the LPS arrays for KMP.
Trade-offs
Pros
Optimal time complexity, making it very fast and efficient.
Guaranteed to pass within the time limits for the given constraints.
Cons
The implementation is more complex, requiring a solid understanding of both KMP and the two-pointer technique.
Solutions
Solution
public class Solution { public void computeLPS ( String pattern , int [] lps ) { int M = pattern . length (); int len = 0 ; lps [ 0 ] = 0 ; int i = 1 ; while ( i < M ) { if ( pattern . charAt ( i ) == pattern . charAt ( len )) { len ++; lps [ i ] = len ; i ++; } else { if ( len != 0 ) { len = lps [ len - 1 ]; } else { lps [ i ] = 0 ; i ++; } } } } public List < Integer > KMP_codestorywithMIK ( String pat , String txt ) { int N = txt . length (); int M = pat . length (); List < Integer > result = new ArrayList <>(); int [] lps = new int [ M ]; computeLPS ( pat , lps ); int i = 0 ; // Index for text int j = 0 ; // Index for pattern while ( i < N ) { if ( pat . charAt ( j ) == txt . charAt ( i )) { i ++; j ++; } if ( j == M ) { result . add ( i - j ); // Pattern found at index i-j+1 (If you have to return 1 Based // indexing, that's why added + 1) j = lps [ j - 1 ]; } else if ( i < N && pat . charAt ( j ) != txt . charAt ( i )) { if ( j != 0 ) { j = lps [ j - 1 ]; } else { i ++; } } } return result ; } private int lowerBound ( List < Integer > list , int target ) { int left = 0 , right = list . size () - 1 , result = list . size (); while ( left <= right ) { int mid = left + ( right - left ) / 2 ; if ( list . get ( mid ) >= target ) { result = mid ; right = mid - 1 ; } else { left = mid + 1 ; } } return result ; } public List < Integer > beautifulIndices ( String s , String a , String b , int k ) { int n = s . length (); List < Integer > i_indices = KMP_codestorywithMIK ( a , s ); List < Integer > j_indices = KMP_codestorywithMIK ( b , s ); List < Integer > result = new ArrayList <>(); for ( int i : i_indices ) { int left_limit = Math . max ( 0 , i - k ); // To avoid out of bound -> I used max(0, i-k) int right_limit = Math . min ( n - 1 , i + k ); // To avoid out of bound -> I used min(n-1, i+k) int lowerBoundIndex = lowerBound ( j_indices , left_limit ); if ( lowerBoundIndex < j_indices . size () && j_indices . get ( lowerBoundIndex ) <= right_limit ) { result . add ( i ); } } return result ; } }class Solution {public: vector<int> beautifulIndices(string s, string patternA, string patternB, int k) { vector<int> beautifulIndicesA = kmpSearch(s, patternA); vector<int> beautifulIndicesB = kmpSearch(s, patternB); sort(beautifulIndicesB.begin(), beautifulIndicesB.end()); vector<int> result; for (int indexA : beautifulIndicesA) { int left = lower_bound(beautifulIndicesB.begin(), beautifulIndicesB.end(), indexA - k) - beautifulIndicesB.begin(); int right = lower_bound(beautifulIndicesB.begin(), beautifulIndicesB.end(), indexA + k + patternB.length()) - beautifulIndicesB.begin(); left = (left >= 0) ? left : -(left + 1); right = (right >= 0) ? right : -(right + 1); for (int indexB = left; indexB < right; indexB++) { if (abs(beautifulIndicesB[indexB] - indexA) <= k) { result.push_back(indexA); break; } } } return result; } private: vector<int> kmpSearch(string text, string pattern) { vector<int> indices; vector<int> pi = computePrefixFunction(pattern); int q = 0; for (int i = 0; i < text.length(); i++) { while (q > 0 && pattern[q] != text[i]) { q = pi[q - 1]; } if (pattern[q] == text[i]) { q++; } if (q == pattern.length()) { indices.push_back(i - q + 1); q = pi[q - 1]; } } return indices; } vector<int> computePrefixFunction(string pattern) { int m = pattern.length(); vector<int> pi(m, 0); int k = 0; for (int q = 1; q < m; q++) { while (k > 0 && pattern[k] != pattern[q]) { k = pi[k - 1]; } if (pattern[k] == pattern[q]) { k++; } pi[q] = k; } return pi; }};class Solution: def beautifulIndices(self, s: str, a: str, b: str, k: int) -> List[int]: def build_prefix_function(pattern): prefix_function = [0] * len(pattern) j = 0 for i in range(1, len(pattern)): while j > 0 and pattern[i] != pattern[j]: j = prefix_function[j - 1] if pattern[i] == pattern[j]: j += 1 prefix_function[i] = j return prefix_function def kmp_search(pattern, text, prefix_function): occurrences = [] j = 0 for i in range(len(text)): while j > 0 and text[i] != pattern[j]: j = prefix_function[j - 1] if text[i] == pattern[j]: j += 1 if j == len(pattern): occurrences . append(i - j + 1) j = prefix_function[j - 1] return occurrences prefix_a = build_prefix_function(a) prefix_b = build_prefix_function(b) resa = kmp_search(a, s, prefix_a) resb = kmp_search(b, s, prefix_b) res = [] print(resa, resb) i = 0 j = 0 while i < len(resa): while j < len(resb): if abs(resb[j] - resa[i]) <= k: res . append(resa[i]) break elif j + 1 < len(resb) and abs(resb[j + 1] - resa[i]) < abs(resb[j] - resa[i]): j += 1 else: break i += 1 return resVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.