3365. Rearrange K Substrings to Form Target String
Problem Description
You are given two strings s
and t
that are anagrams of each other (they contain the same characters with the same frequencies). You are also given an integer k
.
Your task is to determine if you can:
- Split string
s
into exactlyk
equal-sized substrings - Rearrange these substrings in any order
- Concatenate them to form string
t
The key points to understand:
- Both strings must have a length divisible by
k
(since we need equal-sized substrings) - Each substring will have length
n/k
wheren
is the length of the strings - The substrings are contiguous sequences from the original string
s
- You can only rearrange the order of the substrings, not the characters within each substring
For example, if s = "abcd"
, t = "cdab"
, and k = 2
:
- Split
s
into 2 equal parts:["ab", "cd"]
- Rearrange to
["cd", "ab"]
- Concatenate to get
"cdab"
which equalst
- Return
true
The solution uses a hash table to count occurrences of each substring of length m = n/k
in both strings. For string s
, it increments the count, and for string t
, it decrements the count. If all counts end up at zero, it means the same set of substrings appears in both strings, making the rearrangement possible.
Intuition
When we split string s
into k
equal parts and rearrange them to form t
, we're essentially asking: do both strings contain the same collection of substrings of length m = n/k
?
Think of it like having two boxes of puzzle pieces. If we cut string s
into pieces, we get one set of puzzle pieces. If we cut string t
into pieces the same way, we get another set. For the rearrangement to be possible, both boxes must contain exactly the same pieces (though possibly in different orders).
The key insight is that we don't actually need to try all possible rearrangements. We just need to verify that when both strings are cut into chunks of size m
, they produce the same multiset of substrings.
Why does this work? Because if s
can be rearranged to form t
, then:
- The substrings we get from cutting
s
must be exactly the substrings that appear int
when cut the same way - Each unique substring must appear the same number of times in both cuts
This leads us to a counting approach:
- For each substring of length
m
ins
, we add 1 to its count - For each substring of length
m
int
, we subtract 1 from its count - If all counts end up at zero, it means every substring appears equally often in both strings
This is much more efficient than trying to generate and check all possible permutations of the k
substrings, which would be k!
possibilities.
Learn more about Sorting patterns.
Solution Approach
The solution uses a hash table to efficiently track and compare the substrings from both strings.
Implementation Steps:
-
Calculate substring length: First, we determine the length of each substring as
m = n // k
, wheren
is the length of the string. This ensures we split the string intok
equal parts. -
Initialize a counter: We use a
Counter
(hash table) calledcnt
to track the difference in occurrences of substrings betweens
andt
. -
Process both strings simultaneously: We iterate through both strings with a step size of
m
:for i in range(0, n, m): cnt[s[i : i + m]] += 1 # Add substring from s cnt[t[i : i + m]] -= 1 # Subtract substring from t
- For each position
i
(0, m, 2m, ...), we extract a substring of lengthm
- We increment the count for substrings from
s
- We decrement the count for substrings from
t
- This effectively computes the difference in substring occurrences
- For each position
-
Verify the result: After processing all substrings, we check if all values in the counter are zero:
return all(v == 0 for v in cnt.values())
If all values are zero, it means:
- Every substring that appears in
s
also appears int
- They appear with the same frequency
- Therefore, we can rearrange the substrings from
s
to formt
- Every substring that appears in
Time Complexity: O(n)
where n
is the length of the strings, as we iterate through each string once and substring extraction is O(m)
for each of the k
substrings.
Space Complexity: O(k * m)
for storing at most k
different substrings of length m
in the hash table.
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.
Example: s = "abcdef"
, t = "cdefab"
, k = 3
Step 1: Calculate substring length
- String length
n = 6
- Substring length
m = n // k = 6 // 3 = 2
- So we'll split both strings into substrings of length 2
Step 2: Initialize counter
- Create an empty hash table
cnt = {}
Step 3: Process both strings
We iterate with i = 0, 2, 4
(step size of m = 2
):
When i = 0:
- Extract
s[0:2] = "ab"
βcnt["ab"] += 1
βcnt = {"ab": 1}
- Extract
t[0:2] = "cd"
βcnt["cd"] -= 1
βcnt = {"ab": 1, "cd": -1}
When i = 2:
- Extract
s[2:4] = "cd"
βcnt["cd"] += 1
βcnt = {"ab": 1, "cd": 0}
- Extract
t[2:4] = "ef"
βcnt["ef"] -= 1
βcnt = {"ab": 1, "cd": 0, "ef": -1}
When i = 4:
- Extract
s[4:6] = "ef"
βcnt["ef"] += 1
βcnt = {"ab": 1, "cd": 0, "ef": 0}
- Extract
t[4:6] = "ab"
βcnt["ab"] -= 1
βcnt = {"ab": 0, "cd": 0, "ef": 0}
Step 4: Verify result
- Check all values in
cnt
: all are 0 β - Return
true
This confirms that s
split into ["ab", "cd", "ef"]
can be rearranged to ["cd", "ef", "ab"]
to form t = "cdefab"
.
The beauty of this approach is that we never explicitly tried different arrangements. We simply verified that both strings contain the same collection of substrings when cut into pieces of size 2.
Solution Implementation
1from collections import Counter
2
3class Solution:
4 def isPossibleToRearrange(self, s: str, t: str, k: int) -> bool:
5 """
6 Determines if string s can be rearranged to form string t by dividing
7 both strings into k equal-length segments and rearranging those segments.
8
9 Args:
10 s: Source string to be rearranged
11 t: Target string to match
12 k: Number of equal segments to divide the strings into
13
14 Returns:
15 True if rearrangement is possible, False otherwise
16 """
17 # Initialize a counter to track segment frequencies
18 segment_counter = Counter()
19
20 # Calculate the length of the source string
21 string_length = len(s)
22
23 # Calculate the length of each segment
24 segment_length = string_length // k
25
26 # Iterate through both strings in segments
27 for start_index in range(0, string_length, segment_length):
28 # Extract current segment from string s and increment its count
29 s_segment = s[start_index:start_index + segment_length]
30 segment_counter[s_segment] += 1
31
32 # Extract current segment from string t and decrement its count
33 t_segment = t[start_index:start_index + segment_length]
34 segment_counter[t_segment] -= 1
35
36 # Check if all segment counts are zero (meaning perfect matching)
37 # This ensures each segment in s has a corresponding segment in t
38 return all(count == 0 for count in segment_counter.values())
39
1class Solution {
2 public boolean isPossibleToRearrange(String s, String t, int k) {
3 // Create a HashMap to count occurrences of substrings
4 Map<String, Integer> substringCount = new HashMap<>(k);
5
6 // Calculate the total length and the length of each substring block
7 int totalLength = s.length();
8 int blockSize = totalLength / k;
9
10 // Iterate through both strings in blocks of size blockSize
11 for (int startIndex = 0; startIndex < totalLength; startIndex += blockSize) {
12 // Extract substring from s and increment its count
13 String substringFromS = s.substring(startIndex, startIndex + blockSize);
14 substringCount.merge(substringFromS, 1, Integer::sum);
15
16 // Extract substring from t and decrement its count
17 String substringFromT = t.substring(startIndex, startIndex + blockSize);
18 substringCount.merge(substringFromT, -1, Integer::sum);
19 }
20
21 // Check if all counts are zero (meaning s and t have the same substrings)
22 for (int count : substringCount.values()) {
23 if (count != 0) {
24 // If any count is non-zero, rearrangement is not possible
25 return false;
26 }
27 }
28
29 // All substrings match, rearrangement is possible
30 return true;
31 }
32}
33
1class Solution {
2public:
3 bool isPossibleToRearrange(string s, string t, int k) {
4 // Map to track frequency difference between substrings in s and t
5 unordered_map<string, int> frequencyDiff;
6
7 // Calculate total string length and substring length
8 int totalLength = s.size();
9 int substringLength = totalLength / k;
10
11 // Iterate through both strings in chunks of substringLength
12 for (int i = 0; i < totalLength; i += substringLength) {
13 // Extract substring from current position
14 string sSubstring = s.substr(i, substringLength);
15 string tSubstring = t.substr(i, substringLength);
16
17 // Increment count for substring from s, decrement for substring from t
18 frequencyDiff[sSubstring]++;
19 frequencyDiff[tSubstring]--;
20 }
21
22 // Check if all frequency differences are zero
23 // If zero, substrings from s can be rearranged to form t
24 for (const auto& [substring, count] : frequencyDiff) {
25 if (count != 0) {
26 return false;
27 }
28 }
29
30 return true;
31 }
32};
33
1/**
2 * Determines if string s can be rearranged to form string t by dividing both strings
3 * into k equal-length substrings and rearranging them.
4 *
5 * @param s - The source string to be rearranged
6 * @param t - The target string to match after rearrangement
7 * @param k - The number of equal parts to divide the strings into
8 * @returns true if rearrangement is possible, false otherwise
9 */
10function isPossibleToRearrange(s: string, t: string, k: number): boolean {
11 // Map to track frequency difference of substrings between s and t
12 const substringFrequencyMap: Record<string, number> = {};
13
14 // Total length of the input string
15 const stringLength: number = s.length;
16
17 // Length of each substring when divided into k parts
18 const substringLength: number = Math.floor(stringLength / k);
19
20 // Iterate through both strings in chunks of substringLength
21 for (let startIndex = 0; startIndex < stringLength; startIndex += substringLength) {
22 // Extract substring from source string s
23 const sourceSubstring: string = s.slice(startIndex, startIndex + substringLength);
24 // Increment count for this substring from source
25 substringFrequencyMap[sourceSubstring] = (substringFrequencyMap[sourceSubstring] || 0) + 1;
26
27 // Extract corresponding substring from target string t
28 const targetSubstring: string = t.slice(startIndex, startIndex + substringLength);
29 // Decrement count for this substring from target
30 substringFrequencyMap[targetSubstring] = (substringFrequencyMap[targetSubstring] || 0) - 1;
31 }
32
33 // Check if all frequency counts are zero (meaning perfect match of substrings)
34 return Object.values(substringFrequencyMap).every((frequency: number) => frequency === 0);
35}
36
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the string s
. The algorithm iterates through both strings s
and t
once, creating substrings of length m = n/k
. The loop runs k
times (from 0
to n
with step m
), and each iteration performs substring extraction in O(m)
time. Since we process all n
characters total across all iterations (k * m = k * (n/k) = n
), the overall time complexity is O(n)
.
The space complexity is O(n)
. The Counter dictionary cnt
stores at most 2k
unique substrings (up to k
different substrings from s
and k
from t
), where each substring has length m = n/k
. In the worst case where all substrings are unique, the total space used is O(k * m) = O(k * n/k) = O(n)
. Additionally, the substring operations create temporary strings that contribute to the space complexity, resulting in O(n)
total space.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Validating Input Divisibility
The most critical pitfall is assuming that the string length is always divisible by k
. If len(s) % k != 0
, the division into equal-sized substrings becomes impossible, and the code might produce incorrect results or unexpected behavior.
Issue Example:
- If
s = "abcde"
(length 5) andk = 2
, we getsegment_length = 5 // 2 = 2
- The last segment extraction
s[4:6]
would only get"e"
instead of a 2-character substring - This leads to comparing unequal-length segments and incorrect results
Solution: Add validation at the beginning of the function:
def isPossibleToRearrange(self, s: str, t: str, k: int) -> bool:
string_length = len(s)
# Validate that the string can be evenly divided
if string_length % k != 0:
return False
segment_length = string_length // k
# ... rest of the code
2. Using String Slicing as Dictionary Keys Without Consideration
While Python handles string slices as immutable objects suitable for dictionary keys, in some languages or scenarios, developers might accidentally use mutable objects (like lists) for substring representation, which cannot be used as dictionary keys.
Issue Example: If someone modifies the code to work with character lists instead of strings:
# Wrong approach if working with lists
s_segment = list(s[start_index:start_index + segment_length])
segment_counter[s_segment] += 1 # TypeError: unhashable type: 'list'
Solution: Always ensure substring representations are immutable (strings, tuples) when using them as dictionary keys:
# If working with lists, convert to tuple or string
s_segment = tuple(s[start_index:start_index + segment_length])
# or
s_segment = ''.join(s[start_index:start_index + segment_length])
3. Incorrect Assumption About Empty Strings
The code doesn't handle edge cases where strings might be empty or k = 0
.
Issue Example:
- If
s = ""
,t = ""
, andk = 0
, division by zero would occur - If
k = 0
, the range iteration would be infinite
Solution: Add edge case handling:
def isPossibleToRearrange(self, s: str, t: str, k: int) -> bool:
# Handle edge cases
if k == 0:
return False
if len(s) == 0:
return True # Empty strings are considered equal
string_length = len(s)
if string_length % k != 0:
return False
# ... rest of the code
Which of the following is a min heap?
Recommended Readings
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!