Find Beautiful Indices in the Given Array I
MedPrompt
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 <= 1051 <= a.length, b.length <= 10s,a, andbcontain only lowercase English letters.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach is a straightforward, brute-force solution. It first identifies all occurrences of the substrings a and b within the main string s and stores their starting indices in two separate lists. Afterwards, it employs a pair of nested loops to compare every index from the first list against every index from the second. If any pair of indices (i, j) satisfies the condition |i - j| <= k, the index i is marked as beautiful and added to the result.
Algorithm
- Create two lists,
indicesAandindicesB, to store the starting indices of all occurrences of stringsaandbins. - Populate
indicesAby iterating throughsand checking foraat each position. - Populate
indicesBsimilarly by checking forb. - Initialize an empty list
resultto store the beautiful indices. - Iterate through each index
iinindicesAwith an outer loop. - Inside, use a nested loop to iterate through each index
jinindicesB. - If
Math.abs(i - j) <= k, it meansiis a beautiful index. Additoresultand break the inner loop to proceed to the nexti. - Return the
resultlist.
Walkthrough
The implementation involves two main steps. First, we scan the string s twice to find all starting positions for a and b. A simple way to do this is by iterating from 0 to s.length() - pattern.length() and using s.startsWith(pattern, i). The found indices are stored in indicesA and indicesB.
Second, we iterate through each index i in indicesA. For each i, we iterate through all indices j in indicesB. We calculate the absolute difference |i - j| and check if it's less than or equal to k. If the condition holds, we add i to our result list and use break to stop searching for other j's for the current i, as one is sufficient.
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<>(); for (int i = 0; i <= n - lenA; i++) { if (s.startsWith(a, i)) { indicesA.add(i); } } List<Integer> indicesB = new ArrayList<>(); for (int i = 0; i <= n - lenB; i++) { if (s.startsWith(b, i)) { indicesB.add(i); } } List<Integer> result = new ArrayList<>(); if (indicesA.isEmpty() || indicesB.isEmpty()) { return result; } for (int i : indicesA) { for (int j : indicesB) { if (Math.abs(i - j) <= k) { result.add(i); break; // Found a valid j, move to the next i } } } return result; }}Complexity
Time
O(N*M + P*Q), where N is `s.length()`, M is `max(a.length(), b.length())`, P is the count of `a`'s occurrences, and Q is the count of `b`'s occurrences. In the worst case, P and Q can be O(N), making the complexity O(N^2).
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
Easy to understand and implement.
Works correctly for small inputs.
Cons
The nested loop for checking the distance condition leads to a quadratic time complexity in the worst case, which is highly inefficient for large inputs.
It is likely to result in a 'Time Limit Exceeded' error on platforms with strict time constraints.
This approach improves upon the brute-force method by optimizing the search for a valid index j. After finding all occurrences of a and b and storing their indices in sorted lists, it iterates through each index i from a's occurrences. For each i, instead of a linear scan, it performs a binary search on the list of b's indices. This allows for a much faster check (logarithmic time) to see if there's any index j within the desired range [i - k, i + k].
Algorithm
- First, find all starting indices of
aandbinsand store them in two sorted lists,indicesAandindicesB. - Initialize an empty list
result. - Iterate through each index
iinindicesA. - For each
i, the condition|i - j| <= kis equivalent to finding ajinindicesBsuch thati - k <= j <= i + k. - Use binary search on the sorted list
indicesBto check for the existence of such aj. - To do this, search for the first element in
indicesBthat is greater than or equal toi - k. Let's call this elementj_candidate. - If
j_candidateexists andj_candidate <= i + k, theniis a beautiful index. Additoresult. - Return
result.
Walkthrough
The first step remains the same: populate indicesA and indicesB with the starting indices of a and b. Since we find them by iterating from left to right, these lists will be naturally sorted.
Next, for each index i in indicesA, we need to find if there's an index j in indicesB in the range [i - k, i + k]. We can do this efficiently with binary search. We search for the lower bound of i - k in indicesB. This gives us the first potential j that is greater than or equal to i - k. If this potential j exists and is also less than or equal to i + k, we have found a valid partner for i, so i is beautiful.
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(); List<Integer> indicesA = new ArrayList<>(); for (int i = 0; i <= n - a.length(); i++) { if (s.startsWith(a, i)) { indicesA.add(i); } } List<Integer> indicesB = new ArrayList<>(); for (int i = 0; i <= n - b.length(); i++) { if (s.startsWith(b, i)) { indicesB.add(i); } } List<Integer> result = new ArrayList<>(); if (indicesA.isEmpty() || indicesB.isEmpty()) { return result; } for (int i : indicesA) { int searchStart = i - k; // Binary search for the first j >= searchStart int low = 0, high = indicesB.size() - 1; int insertionPoint = indicesB.size(); while (low <= high) { int mid = low + (high - low) / 2; if (indicesB.get(mid) >= searchStart) { insertionPoint = mid; high = mid - 1; } else { low = mid + 1; } } // Check if the found j is within the valid range [i-k, i+k] if (insertionPoint < indicesB.size()) { int j = indicesB.get(insertionPoint); if (Math.abs(i - j) <= k) { result.add(i); } } } return result; }}Complexity
Time
O(N*M + P*log(Q)). Finding indices takes O(N*M). The main loop runs P times, with each iteration taking O(log Q) for binary search. In the worst case, this is O(N*log(N)).
Space
O(P + Q), which can be O(N) in the worst case, for storing the indices.
Trade-offs
Pros
Significantly more efficient than the brute-force approach.
Passes for larger inputs where the brute-force approach would time out.
Cons
While much better than brute-force, it's not the most optimal solution as it repeatedly performs a binary search for each occurrence of
a.
This is the most optimal approach, achieving linear time complexity. It has two main parts. First, it uses the Knuth-Morris-Pratt (KMP) algorithm to find all occurrences of a and b in O(N) time, which is asymptotically faster than simple string matching. Second, with the two sorted lists of indices, it uses a two-pointer technique. As we iterate through a's indices with one pointer, we intelligently advance the second pointer for b's indices. This avoids re-scanning and allows us to check the distance condition for all indices in a single, linear pass.
Algorithm
- Use an efficient string searching algorithm like KMP (Knuth-Morris-Pratt) to find all occurrences of
aandbins. This populatesindicesAandindicesBin O(N) time. - Initialize an empty list
result. - Use a two-pointer approach. Initialize one pointer
idxAto 0 forindicesAand another pointeridxBto 0 forindicesB. - Iterate with
idxAthroughindicesA. For each indexi = indicesA.get(idxA):- Advance the
idxBpointer as long asindicesB.get(idxB)is too small (i.e.,indicesB.get(idxB) < i - k). This bringsidxBto the beginning of the valid window fori. - After advancing
idxB, check ifidxBis within the bounds ofindicesBand if the currentj = indicesB.get(idxB)satisfies|i - j| <= k. - If it does,
iis a beautiful index, so add it toresult. - Increment
idxAto check the next potential beautiful index.
- Advance the
- Since both
idxAandidxBonly move forward, this check is done in a single pass over the index lists. - Return
result.
Walkthrough
This approach optimizes both phases of the problem.
-
Finding Occurrences with KMP: Instead of a simple loop, we use the KMP algorithm. KMP preprocesses the pattern to create a Longest Proper Prefix Suffix (LPS) array. This array helps to avoid redundant comparisons, allowing the search for all occurrences to complete in O(N + M) time, where N is the text length and M is the pattern length.
-
Two-Pointer Check: With the sorted
indicesAandindicesBlists, we use two pointers,idxAandidxB, both starting at 0. We iterate throughindicesAwithidxA. For eachi = indicesA[idxA], we advanceidxBto find the firstj = indicesB[idxB]that could potentially be in the range[i-k, i+k]. We do this by skipping allj's that are smaller thani-k. Then, we check if the currentjis within the range. If it is, we've found a beautiful index. We then move to the nexti. BecauseidxBnever moves backward, the total work for this phase is proportional to the sum of the lengths of the two index lists, O(P + Q).
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 = kmpSearch(s, a); List<Integer> indicesB = kmpSearch(s, b); List<Integer> result = new ArrayList<>(); if (indicesA.isEmpty() || indicesB.isEmpty()) { return result; } int idxA = 0, idxB = 0; while (idxA < indicesA.size()) { int i = indicesA.get(idxA); // Move idxB to the start of the window for i while (idxB < indicesB.size() && indicesB.get(idxB) < i - k) { idxB++; } // Check if there's a j in the window [i-k, i+k] if (idxB < indicesB.size() && Math.abs(indicesB.get(idxB) - i) <= k) { result.add(i); } idxA++; } return result; } private List<Integer> kmpSearch(String text, String pattern) { List<Integer> occurrences = new ArrayList<>(); int n = text.length(), m = pattern.length(); if (m == 0) return occurrences; if (n < m) return occurrences; int[] lps = computeLPS(pattern); int i = 0, j = 0; while (i < n) { if (pattern.charAt(j) == text.charAt(i)) { i++; j++; } if (j == m) { occurrences.add(i - j); j = lps[j - 1]; } else if (i < n && pattern.charAt(j) != text.charAt(i)) { if (j != 0) { j = lps[j - 1]; } else { i++; } } } return occurrences; } private int[] computeLPS(String pattern) { int m = pattern.length(); int[] lps = new int[m]; int length = 0; int i = 1; while (i < m) { if (pattern.charAt(i) == pattern.charAt(length)) { length++; lps[i] = length; i++; } else { if (length != 0) { length = lps[length - 1]; } else { lps[i] = 0; i++; } } } return lps; }}Complexity
Time
O(N + M). The KMP searches take O(N + |a|) and O(N + |b|). The two-pointer scan takes O(P + Q). Since P, Q <= N, the total complexity is dominated by the string searching, resulting in O(N + M).
Space
O(N + M) where N is `s.length()` and M is `max(a.length(), b.length())`. This is for storing the indices and the KMP helper arrays.
Trade-offs
Pros
Achieves optimal linear time complexity.
Most efficient solution, guaranteed to pass even for very large inputs within the given constraints.
Cons
The implementation is more complex, requiring knowledge of advanced algorithms like KMP.
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.