Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 Rs or two Ls 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 position i in start and needs to be at position j in target, then i >= 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 position i in start and needs to be at position j in target, then i <= 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:

  1. Extract pieces with positions: Create two lists a and b that store tuples of (character, index) for all non-blank characters in start and target 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.

  2. 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.

  3. 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 in start must be the same type as the i-th piece in target

    • Check movement validity for 'L': if c == 'L' and i < j: return False - An 'L' at position i cannot move right to position j (where j > i)

    • Check movement validity for 'R': if c == 'R' and i > j: return False - An 'R' at position i cannot move left to position j (where j < i)

  4. 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 Evaluator

Example 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 βœ“
  • 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 βœ“

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 in start: O(n)
  • Creating list b requires iterating through all characters in target: O(n)
  • The zip operation with the subsequent loop iterates through the filtered lists, which in the worst case contains all n 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 from start, excluding underscores. In the worst case where there are no underscores, this stores n tuples: O(n)
  • List b stores tuples of characters and indices from target, excluding underscores. In the worst case, this also stores n 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:

  1. The sequence of pieces (ignoring blanks) is identical
  2. 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More