2337. Move Pieces to Obtain a String
Problem Description
You have two strings start
and target
, both with the same length n
. These strings contain only three types of characters: 'L'
, 'R'
, and '_'
.
The characters represent:
'L'
- a piece that can only move left'R'
- a piece that can only move right'_'
- an empty/blank space
Movement rules:
- An
'L'
piece can move one position to the left only if there's a blank space ('_'
) immediately to its left - An
'R'
piece can move one position to the right only if there's a blank space ('_'
) immediately to its right - Pieces cannot jump over other pieces - they can only move into adjacent blank spaces
Your task is to determine if you can transform the start
string into the target
string by moving the pieces according to these rules. You can make any number of moves.
For example:
"_L"
can become"L_"
(L moves left into the blank)"R_"
can become"_R"
(R moves right into the blank)"RL"
cannot change (pieces are blocked by each other)
Return true
if it's possible to transform start
into target
, otherwise return false
.
Intuition
The key insight is that while pieces can move, they cannot pass through each other. This means the relative order of 'L'
and 'R'
pieces must remain the same in both strings.
Think about it this way: if we have "R_L"
, the R
can never get past the L
because R
moves right and L
moves left - they would collide. Similarly, two R
s or two L
s maintain their relative order since they move in the same direction and can't jump over each other.
This leads us to the first condition: if we remove all blanks from both strings, the sequence of 'L'
and 'R'
characters must be identical. If start
has "RLL"
after removing blanks, then target
must also have "RLL"
after removing blanks.
The second insight involves the movement constraints:
- An
'L'
can only move left, so if an'L'
is at positioni
instart
and needs to be at positionj
intarget
, theni >= j
must be true (it can stay in place or move left, but never right) - An
'R'
can only move right, so if an'R'
is at positioni
instart
and needs to be at positionj
intarget
, theni <= j
must be true (it can stay in place or move right, but never left)
By checking these two conditions - matching sequence of pieces and valid movement directions for each piece - we can determine if the transformation is possible without actually simulating the moves. We extract the non-blank characters with their positions from both strings and verify that each piece can legally move from its starting position to its target position.
Learn more about Two Pointers patterns.
Solution Approach
The solution implements the intuition by extracting and comparing the non-blank characters from both strings:
-
Extract pieces with positions: Create two lists
a
andb
that store tuples of(character, index)
for all non-blank characters instart
andtarget
respectively:a = [(v, i) for i, v in enumerate(start) if v != '_'] b = [(v, i) for i, v in enumerate(target) if v != '_']
This gives us the sequence of pieces and their positions, filtering out all blanks.
-
Check piece count: First verify that both strings have the same number of pieces:
if len(a) != len(b): return False
If the counts don't match, transformation is impossible.
-
Validate each piece: Use
zip
to pair up corresponding pieces from both lists and check:for (c, i), (d, j) in zip(a, b):
For each paired piece:
-
Check piece type matches:
if c != d: return False
- The i-th piece instart
must be the same type as the i-th piece intarget
-
Check movement validity for 'L':
if c == 'L' and i < j: return False
- An 'L' at positioni
cannot move right to positionj
(wherej > i
) -
Check movement validity for 'R':
if c == 'R' and i > j: return False
- An 'R' at positioni
cannot move left to positionj
(wherej < i
)
-
-
Return result: If all checks pass, return
True
.
The algorithm runs in O(n)
time where n
is the length of the strings, as we iterate through each string once to extract pieces and then iterate through the pieces once to validate them. The space complexity is also O(n)
for storing the extracted pieces.
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 the solution with start = "R__L"
and target = "_RL_"
.
Step 1: Extract pieces with positions
- From
start = "R__L"
:- 'R' at index 0, 'L' at index 3
a = [('R', 0), ('L', 3)]
- From
target = "_RL_"
:- 'R' at index 1, 'L' at index 2
b = [('R', 1), ('L', 2)]
Step 2: Check piece count
- Both lists have 2 pieces β
Step 3: Validate each piece
-
First pair:
('R', 0)
from start and('R', 1)
from target- Piece types match: 'R' == 'R' β
- Movement check for 'R': Can it move from index 0 to index 1?
- 'R' moves right, so we need
i <= j
: 0 <= 1 β
- 'R' moves right, so we need
-
Second pair:
('L', 3)
from start and('L', 2)
from target- Piece types match: 'L' == 'L' β
- Movement check for 'L': Can it move from index 3 to index 2?
- 'L' moves left, so we need
i >= j
: 3 >= 2 β
- 'L' moves left, so we need
Step 4: Return result
All checks passed, so return True
.
To verify this makes sense, let's trace the actual moves:
"R__L"
β"_R_L"
(R moves right)"_R_L"
β"_RL_"
(L moves left)
The transformation is indeed possible!
Solution Implementation
1class Solution:
2 def canChange(self, start: str, target: str) -> bool:
3 # Extract non-underscore characters with their positions from start string
4 # Each tuple contains (character, original_index)
5 start_chars = [(char, index) for index, char in enumerate(start) if char != '_']
6
7 # Extract non-underscore characters with their positions from target string
8 target_chars = [(char, index) for index, char in enumerate(target) if char != '_']
9
10 # If the number of non-underscore characters differs, transformation is impossible
11 if len(start_chars) != len(target_chars):
12 return False
13
14 # Check each corresponding pair of characters
15 for (start_char, start_pos), (target_char, target_pos) in zip(start_chars, target_chars):
16 # Characters must match at corresponding positions (order preservation)
17 if start_char != target_char:
18 return False
19
20 # 'L' can only move left, so its target position must be <= start position
21 if start_char == 'L' and start_pos < target_pos:
22 return False
23
24 # 'R' can only move right, so its target position must be >= start position
25 if start_char == 'R' and start_pos > target_pos:
26 return False
27
28 # All conditions satisfied, transformation is possible
29 return True
30
1class Solution {
2 /**
3 * Determines if string 'start' can be transformed to 'target' by moving 'L' and 'R' characters.
4 * Rules: 'L' can only move left, 'R' can only move right, and they cannot cross each other.
5 *
6 * @param start the initial string
7 * @param target the target string to transform to
8 * @return true if transformation is possible, false otherwise
9 */
10 public boolean canChange(String start, String target) {
11 // Extract positions and types of non-blank characters from both strings
12 List<int[]> startPositions = extractNonBlankPositions(start);
13 List<int[]> targetPositions = extractNonBlankPositions(target);
14
15 // If the number of non-blank characters differs, transformation is impossible
16 if (startPositions.size() != targetPositions.size()) {
17 return false;
18 }
19
20 // Validate each character's movement constraints
21 for (int i = 0; i < startPositions.size(); i++) {
22 int[] startChar = startPositions.get(i);
23 int[] targetChar = targetPositions.get(i);
24
25 // Characters must be of the same type (both 'L' or both 'R')
26 if (startChar[0] != targetChar[0]) {
27 return false;
28 }
29
30 // 'L' can only move left (start position >= target position)
31 if (startChar[0] == 1 && startChar[1] < targetChar[1]) {
32 return false;
33 }
34
35 // 'R' can only move right (start position <= target position)
36 if (startChar[0] == 2 && startChar[1] > targetChar[1]) {
37 return false;
38 }
39 }
40
41 return true;
42 }
43
44 /**
45 * Extracts positions and types of 'L' and 'R' characters from the string.
46 *
47 * @param s the input string
48 * @return list of [type, position] pairs where type: 1='L', 2='R'
49 */
50 private List<int[]> extractNonBlankPositions(String s) {
51 List<int[]> positions = new ArrayList<>();
52
53 for (int i = 0; i < s.length(); i++) {
54 char currentChar = s.charAt(i);
55
56 if (currentChar == 'L') {
57 // Store 'L' as type 1 with its position
58 positions.add(new int[] {1, i});
59 } else if (currentChar == 'R') {
60 // Store 'R' as type 2 with its position
61 positions.add(new int[] {2, i});
62 }
63 // Skip blank/underscore characters
64 }
65
66 return positions;
67 }
68}
69
1using pii = pair<int, int>;
2
3class Solution {
4public:
5 bool canChange(string start, string target) {
6 // Extract positions of 'L' and 'R' characters from both strings
7 auto startPositions = extractNonBlankPositions(start);
8 auto targetPositions = extractNonBlankPositions(target);
9
10 // If the number of 'L' and 'R' characters don't match, transformation is impossible
11 if (startPositions.size() != targetPositions.size()) {
12 return false;
13 }
14
15 // Check each corresponding pair of characters
16 for (int i = 0; i < startPositions.size(); ++i) {
17 auto startChar = startPositions[i];
18 auto targetChar = targetPositions[i];
19
20 // Characters must be the same type (both 'L' or both 'R')
21 if (startChar.first != targetChar.first) {
22 return false;
23 }
24
25 // 'L' can only move left, so start position must be >= target position
26 if (startChar.first == 1 && startChar.second < targetChar.second) {
27 return false;
28 }
29
30 // 'R' can only move right, so start position must be <= target position
31 if (startChar.first == 2 && startChar.second > targetChar.second) {
32 return false;
33 }
34 }
35
36 return true;
37 }
38
39private:
40 // Extract positions of 'L' and 'R' characters from the string
41 // Returns vector of pairs: (character_type, position)
42 // where character_type: 1 for 'L', 2 for 'R'
43 vector<pair<int, int>> extractNonBlankPositions(string s) {
44 vector<pii> positions;
45
46 for (int i = 0; i < s.size(); ++i) {
47 if (s[i] == 'L') {
48 positions.push_back({1, i}); // 'L' is encoded as 1
49 }
50 else if (s[i] == 'R') {
51 positions.push_back({2, i}); // 'R' is encoded as 2
52 }
53 // Skip blank spaces (underscore characters)
54 }
55
56 return positions;
57 }
58};
59
1/**
2 * Determines if the start string can be transformed into the target string
3 * by moving 'L' pieces left and 'R' pieces right, but not past each other.
4 * Empty spaces are represented by '_'.
5 *
6 * @param start - The initial string configuration
7 * @param target - The target string configuration
8 * @returns true if transformation is possible, false otherwise
9 */
10function canChange(start: string, target: string): boolean {
11 // Check if both strings have the same non-blank characters in the same order
12 // This ensures the relative order of 'L' and 'R' pieces is preserved
13 const startPieces: string = [...start].filter(char => char !== '_').join('');
14 const targetPieces: string = [...target].filter(char => char !== '_').join('');
15
16 if (startPieces !== targetPieces) {
17 return false;
18 }
19
20 const stringLength: number = start.length;
21 let startIndex: number = 0;
22 let targetIndex: number = 0;
23
24 // Traverse both strings simultaneously
25 while (startIndex < stringLength || targetIndex < stringLength) {
26 // Skip blank spaces in start string
27 while (startIndex < stringLength && start[startIndex] === '_') {
28 startIndex++;
29 }
30
31 // Skip blank spaces in target string
32 while (targetIndex < stringLength && target[targetIndex] === '_') {
33 targetIndex++;
34 }
35
36 // Check if we've reached the end of both strings
37 if (startIndex >= stringLength && targetIndex >= stringLength) {
38 break;
39 }
40
41 // 'R' pieces can only move right, so start position must be <= target position
42 if (startIndex < stringLength && start[startIndex] === 'R') {
43 if (startIndex > targetIndex) {
44 return false;
45 }
46 }
47
48 // 'L' pieces can only move left, so start position must be >= target position
49 if (startIndex < stringLength && start[startIndex] === 'L') {
50 if (startIndex < targetIndex) {
51 return false;
52 }
53 }
54
55 // Move to next character in both strings
56 startIndex++;
57 targetIndex++;
58 }
59
60 return true;
61}
62
Time and Space Complexity
Time Complexity: O(n)
where n
is the length of the input strings.
- Creating list
a
requires iterating through all characters instart
:O(n)
- Creating list
b
requires iterating through all characters intarget
:O(n)
- The
zip
operation with the subsequent loop iterates through the filtered lists, which in the worst case contains alln
characters:O(n)
- Overall time complexity:
O(n) + O(n) + O(n) = O(n)
Space Complexity: O(n)
where n
is the length of the input strings.
- List
a
stores tuples of characters and indices fromstart
, excluding underscores. In the worst case where there are no underscores, this storesn
tuples:O(n)
- List
b
stores tuples of characters and indices fromtarget
, excluding underscores. In the worst case, this also storesn
tuples:O(n)
- The
zip
operation creates an iterator but doesn't create additional space proportional to input size - Overall space complexity:
O(n) + O(n) = O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Forgetting that pieces cannot jump over each other
A common mistake is only checking if L pieces move left and R pieces move right, without considering that pieces block each other's movement. For example:
# Incorrect approach - only checks direction constraints
def canChange_wrong(start: str, target: str):
for i in range(len(start)):
if start[i] == 'L' and target[i] == 'L':
# Just check if L is not moving right
continue
elif start[i] == 'R' and target[i] == 'R':
# Just check if R is not moving left
continue
return True
This would incorrectly return True
for cases like:
start = "R_L"
,target = "L_R"
- The R cannot jump over L to reach the right position, and L cannot jump over R to reach the left position
Solution: The correct approach extracts pieces in order and ensures the relative ordering of all pieces remains the same. By comparing pieces sequentially using their extraction order, we implicitly ensure no jumping occurs.
Pitfall 2: Trying to simulate actual movements
Another mistake is attempting to simulate the actual movement process step by step:
# Inefficient and error-prone approach
def canChange_simulation(start: str, target: str):
current = list(start)
visited = set()
queue = [current]
while queue:
state = queue.pop(0)
if ''.join(state) == target:
return True
# Try all possible moves...
# This becomes very complex and inefficient
This approach is inefficient (potentially exponential time complexity) and prone to implementation errors.
Solution: The key insight is that we don't need to simulate movements. We only need to verify:
- The sequence of pieces (ignoring blanks) is identical
- Each piece can reach its target position given its movement constraints
Pitfall 3: Incorrect handling of multiple pieces of the same type
Consider checking positions without maintaining order:
# Wrong approach
def canChange_wrong2(start: str, target: str):
l_positions_start = [i for i, c in enumerate(start) if c == 'L']
l_positions_target = [i for i, c in enumerate(target) if c == 'L']
# Just checking if each L can reach some target position
# This ignores which specific L should go where
For example, with start = "L_L"
and target = "LL_"
, this might incorrectly try to move the second L to the first position.
Solution: By using zip
to pair pieces in order, we ensure each piece in start
is matched with its corresponding piece in target
, maintaining the correct mapping.
How would you design a stack which has a function min
that returns the minimum element in the stack, in addition to push
and pop
? All push
, pop
, min
should have running time O(1)
.
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
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!