Push Dominoes
MedPrompt
There are n dominoes in a line, and we place each domino vertically upright. In the beginning, we simultaneously push some of the dominoes either to the left or to the right.
After each second, each domino that is falling to the left pushes the adjacent domino on the left. Similarly, the dominoes falling to the right push their adjacent dominoes standing on the right.
When a vertical domino has dominoes falling on it from both sides, it stays still due to the balance of the forces.
For the purposes of this question, we will consider that a falling domino expends no additional force to a falling or already fallen domino.
You are given a string dominoes representing the initial state where:
dominoes[i] = 'L', if theithdomino has been pushed to the left,dominoes[i] = 'R', if theithdomino has been pushed to the right, anddominoes[i] = '.', if theithdomino has not been pushed.
Return a string representing the final state.
Example 1:
Input: dominoes = "RR.L"
Output: "RR.L"
Explanation: The first domino expends no additional force on the second domino.Example 2:
Input: dominoes = ".L.R...LR..L.."
Output: "LL.RR.LLRRLL.."
Constraints:
n == dominoes.length1 <= n <= 105dominoes[i]is either'L','R', or'.'.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach directly simulates the process of dominoes falling second by second. We repeatedly scan the line of dominoes and update their states based on their neighbors. A standing domino falls if pushed from one side but not the other. This process continues until no more dominoes can fall, and the configuration becomes stable.
Algorithm
- Initialize a
chararraycurrentStatefrom the inputdominoesstring. - Start a loop that continues as long as changes are made to the dominoes' state in an iteration.
- Inside the loop, create a
nextStatecharacter array, initially a copy ofcurrentState. - Set a boolean flag
changedtofalse. - Iterate from
i = 0ton-1through thecurrentState. - If
currentState[i]is '.':- Check for a right push from the left neighbor:
pushR = (i > 0 && currentState[i-1] == 'R'). - Check for a left push from the right neighbor:
pushL = (i < n-1 && currentState[i+1] == 'L'). - If
pushRis true andpushLis false, setnextState[i] = 'R'andchanged = true. - If
pushLis true andpushRis false, setnextState[i] = 'L'andchanged = true.
- Check for a right push from the left neighbor:
- After the inner loop, update
currentStatewithnextState. - If
changedisfalse, break the outer loop as the state is stable. - Convert the final
currentStatearray back to a string and return it.
Walkthrough
The brute-force simulation mimics the physical process described in the problem. We treat each pass over the dominoes array as one second of time. In each pass, we determine the next state of all dominoes simultaneously. A standing domino . at index i will fall to the right if its left neighbor i-1 is an R, and it will fall to the left if its right neighbor i+1 is an L. If it's pushed from both sides, the forces balance, and it remains standing. We use a temporary array to store the state for the next second to ensure all changes are based on the state from the current second. We repeat this process until a full pass results in no changes, indicating a final, stable state.
class Solution { public String pushDominoes(String dominoes) { int n = dominoes.length(); char[] current = dominoes.toCharArray(); char[] next = new char[n]; boolean changed = true; while (changed) { changed = false; System.arraycopy(current, 0, next, 0, n); for (int i = 0; i < n; i++) { if (current[i] == '.') { boolean pushR = (i > 0 && current[i - 1] == 'R'); boolean pushL = (i < n - 1 && current[i + 1] == 'L'); if (pushR && !pushL) { next[i] = 'R'; changed = true; } else if (pushL && !pushR) { next[i] = 'L'; changed = true; } } } System.arraycopy(next, 0, current, 0, n); } return new String(current); }}Complexity
Time
O(N^2), where N is the number of dominoes. In the worst-case scenario (e.g., `R.......`), a force propagates one step at a time, requiring N iterations. Each iteration involves a scan of the entire array, taking O(N) time.
Space
O(N), where N is the number of dominoes. This is for the auxiliary array used to store the state of the dominoes in the next second.
Trade-offs
Pros
Intuitive and easy to understand as it directly models the problem statement.
Relatively simple to implement.
Cons
Highly inefficient for large inputs, likely to result in a 'Time Limit Exceeded' error.
The number of iterations can be up to N, making the overall complexity quadratic.
This approach determines the final state of each domino by calculating the net force acting upon it. We can think of an 'R' domino as exerting a rightward force that diminishes with distance, and an 'L' domino exerting a leftward force. By making two passes over the dominoes, we can compute the total force at each position and decide its final state.
Algorithm
- Create an integer array
forcesof sizen, initialized to zeros. - First Pass (Left to Right): Calculate rightward forces.
- Initialize a variable
force = 0. - Iterate from
i = 0ton-1:- If
dominoes[i] == 'R', setforce = n. - If
dominoes[i] == 'L', resetforce = 0(as 'L' blocks any rightward force from its left). - If
dominoes[i] == '.', decrementforce(i.e.,force = Math.max(0, force - 1)). - Add the current
forcetoforces[i].
- If
- Initialize a variable
- Second Pass (Right to Left): Calculate leftward forces.
- Reset
force = 0. - Iterate from
i = n-1to0:- If
dominoes[i] == 'L', setforce = n. - If
dominoes[i] == 'R', resetforce = 0. - If
dominoes[i] == '.', decrementforce. - Subtract the current
forcefromforces[i].
- If
- Reset
- Build Result:
- Create a
StringBuilder. - Iterate through the
forcesarray:- If
forces[i] > 0, append 'R'. - If
forces[i] < 0, append 'L'. - If
forces[i] == 0, append '.'.
- If
- Create a
- Return the final string.
Walkthrough
The core idea is that the state of any standing domino . is decided by the nearest R to its left and the nearest L to its right. We can quantify this by calculating forces. We use an auxiliary array, forces, to store the net force at each position.
First, we iterate from left to right. When we encounter an 'R', we start a 'rightward force' of magnitude n. This force decreases by 1 for each step we move to the right. An 'L' acts as a wall, stopping any rightward force from its left, so we reset the force to 0. We add this calculated force to our forces array.
Next, we do a similar pass from right to left to calculate 'leftward forces'. An 'L' starts a force, and an 'R' stops it. We subtract this leftward force from the forces array.
After both passes, the forces array contains the net force at each position. A positive value means the rightward force is stronger, a negative value means the leftward force is stronger, and zero means the forces are balanced or no force is present. We can then build the final string based on the sign of the force at each position.
class Solution { public String pushDominoes(String dominoes) { int n = dominoes.length(); int[] forces = new int[n]; // Rightward forces from left to right int force = 0; for (int i = 0; i < n; i++) { if (dominoes.charAt(i) == 'R') { force = n; } else if (dominoes.charAt(i) == 'L') { force = 0; } else { force = Math.max(0, force - 1); } forces[i] += force; } // Leftward forces from right to left force = 0; for (int i = n - 1; i >= 0; i--) { if (dominoes.charAt(i) == 'L') { force = n; } else if (dominoes.charAt(i) == 'R') { force = 0; } else { force = Math.max(0, force - 1); } forces[i] -= force; } StringBuilder result = new StringBuilder(); for (int f : forces) { if (f > 0) { result.append('R'); } else if (f < 0) { result.append('L'); } else { result.append('.'); } } return result.toString(); }}Complexity
Time
O(N), as the algorithm consists of two linear passes to calculate forces and one pass to build the result string.
Space
O(N) to store the `forces` array.
Trade-offs
Pros
Efficient O(N) time complexity.
Systematically handles all interactions between 'L' and 'R' forces.
Cons
Requires extra O(N) space for the forces array.
Needs multiple passes over the data, which might be slightly less performant than a single-pass solution due to cache effects.
This is the most efficient approach, which processes the dominoes in a single pass. The key observation is that the string can be divided into independent segments of standing dominoes ('.') bordered by falling dominoes ('L' or 'R') or the ends of the string. The final state of the dominoes within each segment only depends on its two boundaries.
Algorithm
- Convert the input string to a
chararrayresfor in-place modification. - Use two pointers,
iandj. Letibe the index of the last seen 'L' or 'R', initialized to -1. - Iterate
jfrom0ton(inclusive, to handle the last segment). - When
jreachesnorres[j]is not '.', we have identified a segment of '.'s betweeniandj. - Get the boundary characters:
leftCharisres[i](or 'L' ifiis -1) andrightCharisres[j](or 'R' ifjisn). - Apply rules based on
leftCharandrightChar:- If
leftChar == rightChar == 'R', fill the segmentres[i+1...j-1]with 'R'. - If
leftChar == rightChar == 'L', fill the segmentres[i+1...j-1]with 'L'. - If
leftChar == 'R'andrightChar == 'L', fill the segment from both ends towards the middle. Use two more pointers,l=i+1andr=j-1. In a loop whilel < r, setres[l++] = 'R'andres[r--] = 'L'. - If
leftChar == 'L'andrightChar == 'R', do nothing, as the dominoes in between are not pushed.
- If
- After processing the segment, update
i = jto start searching for the next segment. - Convert the modified
chararray back to a string.
Walkthrough
We can iterate through the string using a pointer j to find segments of .s. Another pointer, i, keeps track of the index of the previous non-. character (the left boundary of the current segment). When res[j] is 'L' or 'R', we have found a segment of .s from index i+1 to j-1.
The behavior of this segment is determined entirely by its boundaries, res[i] and res[j].
- R...R: All
.s becomeR. - L...L: All
.s becomeL. - L...R: The
.s are not pushed by these boundaries, so they remain.. - R...L: The forces collide. We fill the first half of the
.s withRand the second half withL. If there's an odd number of.s, the middle one remains standing.
We also need to handle the edge cases where a segment is at the beginning or end of the string. We can imagine a virtual 'L' at index -1 and a virtual 'R' at index n. For example, a segment ...R at the beginning is treated as L...R, so the .s don't fall. A segment L... at the end is treated as L...R, so again the .s don't fall. This logic allows us to solve the problem in a single, efficient pass.
class Solution { public String pushDominoes(String dominoes) { char[] res = dominoes.toCharArray(); int n = res.length; int i = -1; // Index of the last seen 'L' or 'R' for (int j = 0; j < n; ++j) { if (res[j] == '.') { continue; } if (i == -1) { // Segment starts at the beginning if (res[j] == 'L') { for (int k = 0; k < j; ++k) { res[k] = 'L'; } } } else { // Segment is between res[i] and res[j] if (res[i] == res[j]) { // R...R or L...L for (int k = i + 1; k < j; ++k) { res[k] = res[i]; } } else if (res[i] == 'R' && res[j] == 'L') { // R...L int l = i + 1, r = j - 1; while (l < r) { res[l++] = 'R'; res[r--] = 'L'; } } // Case L...R is implicitly handled, as '.'s remain '.' } i = j; } if (i != -1 && res[i] == 'R') { // Segment ends at the end for (int k = i + 1; k < n; ++k) { res[k] = 'R'; } } return new String(res); }}Complexity
Time
O(N), as we iterate through the array with pointer `j` once. The inner loops for filling segments also contribute linearly to the total time, as each character is written to at most once.
Space
O(N). In Java, strings are immutable, so we need a `char` array or `StringBuilder` of size N to build the result. In languages with mutable strings, this could be an O(1) space solution.
Trade-offs
Pros
Most efficient time complexity, O(N), with a single pass.
Processes the string in-place (on a char array), which can be memory efficient.
Cons
The logic for handling the different segment types and boundary conditions can be slightly more complex to write correctly compared to other approaches.
Solutions
Solution
public class Solution { public String pushDominoes ( String dominoes ) { int n = dominoes . length (); Deque < Integer > q = new ArrayDeque <>(); int [] time = new int [ n ]; Arrays . fill ( time , - 1 ); List < Character >[] force = new List [ n ]; for ( int i = 0 ; i < n ; ++ i ) { force [ i ] = new ArrayList <>(); } for ( int i = 0 ; i < n ; ++ i ) { char f = dominoes . charAt ( i ); if ( f != '.' ) { q . offer ( i ); time [ i ] = 0 ; force [ i ]. add ( f ); } } char [] ans = new char [ n ]; Arrays . fill ( ans , '.' ); while (! q . isEmpty ()) { int i = q . poll (); if ( force [ i ]. size () == 1 ) { ans [ i ] = force [ i ]. get ( 0 ); char f = ans [ i ]; int j = f == 'L' ? i - 1 : i + 1 ; if ( j >= 0 && j < n ) { int t = time [ i ]; if ( time [ j ] == - 1 ) { q . offer ( j ); time [ j ] = t + 1 ; force [ j ]. add ( f ); } else if ( time [ j ] == t + 1 ) { force [ j ]. add ( f ); } } } } return new String ( ans ); } } class Solution {}class Solution {public: string pushDominoes(string dominoes) { int n = dominoes.size(); queue<int> q; vector<int> time(n, -1); vector<string> force(n); for (int i = 0; i < n; i++) { if (dominoes[i] == '.') continue; q.emplace(i); time[i] = 0; force[i].push_back(dominoes[i]); } string ans(n, '.'); while (!q.empty()) { int i = q.front(); q.pop(); if (force[i].size() == 1) { char f = force[i][0]; ans[i] = f; int j = (f == 'L') ? (i - 1) : (i + 1); if (j >= 0 && j < n) { int t = time[i]; if (time[j] == -1) { q.emplace(j); time[j] = t + 1; force[j].push_back(f); } else if (time[j] == t + 1) force[j].push_back(f); } } } return ans; }};class Solution: def pushDominoes(self, dominoes: str) -> str: n = len(dominoes) q = deque() time = [- 1] * n force = defaultdict(list) for i, f in enumerate(dominoes): if f != '.': q . append(i) time[i] = 0 force[i]. append(f) ans = ['.'] * n while q: i = q . popleft() if len(force[i]) == 1: ans[i] = f = force[i][0] j = i - 1 if f == 'L' else i + 1 if 0 <= j < n: t = time[i] if time[j] == - 1: q . append(j) time[j] = t + 1 force[j]. append(f) elif time[j] == t + 1: force[j]. append(f) return '' . join(ans)Video 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.