Skip to content

Commit ddaaf74

Browse files
committed
LeetCode 838. Push Dominoes
1 parent b5ea92c commit ddaaf74

File tree

3 files changed

+229
-1
lines changed

3 files changed

+229
-1
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ Solutions to LeetCode problems. The first column links to the problem in LeetCod
195195
| [815. Bus Routes][lc815] | 🔴 Hard | [![python](res/py.png)][lc815py] |
196196
| [820. Short Encoding of Words][lc820] | 🟠 Medium | [![python](res/py.png)][lc820py] |
197197
| [823. Binary Trees With Factors][lc823] | 🟠 Medium | [![python](res/py.png)][lc823py] |
198+
| [838. Push Dominoes][lc838] | 🟠 Medium | [![python](res/py.png)][lc838py] |
198199
| [844. Backspace String Compare][lc844] | 🟢 Easy | [![python](res/py.png)][lc844py] |
199200
| [846. Hand of Straights][lc846] | 🟠 Medium | [![python](res/py.png)][lc846py] |
200201
| [853. Car Fleet][lc853] | 🟠 Medium | [![python](res/py.png)][lc853py] |
@@ -603,6 +604,8 @@ Solutions to LeetCode problems. The first column links to the problem in LeetCod
603604
[lc820py]: leetcode/short-encoding-of-words.py
604605
[lc823]: https://leetcode.com/problems/binary-trees-with-factors/
605606
[lc823py]: leetcode/binary-trees-with-factors.py
607+
[lc838]: https://leetcode.com/problems/push-dominoes/
608+
[lc838py]: leetcode/push-dominoes.py
606609
[lc844]: https://leetcode.com/problems/backspace-string-compare/
607610
[lc844py]: leetcode/backspace-string-compare.py
608611
[lc846]: https://leetcode.com/problems/hand-of-straights/

leetcode/lists/dp.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ tracking your own progress.
500500
| | 🟠 Medium | [221. Maximal Square][lc221] | |
501501
|| 🟠 Medium | [304. Range Sum Query 2D - Immutable][lc304] | [![python](../../res/py.png)][lc304py] |
502502
| | 🟠 Medium | [764. Largest Plus Sign][lc764] | |
503-
| | 🟠 Medium | [838. Push Dominoes][lc838] | |
503+
| | 🟠 Medium | [838. Push Dominoes][lc838] | [![python](../../res/py.png)][lc838py] |
504504
| | 🟠 Medium | [1139. Largest 1-Bordered Square][lc1139] | |
505505
| | 🟠 Medium | [1277. Count Square Submatrices with All Ones][lc1277] | |
506506
| | 🟠 Medium | [1314. Matrix Block Sum][lc1314] | |
@@ -521,6 +521,7 @@ tracking your own progress.
521521
[lc304py]: ../range-sum-query-2d-immutable.py
522522
[lc764]: https://leetcode.com/problems/largest-plus-sign/
523523
[lc838]: https://leetcode.com/problems/push-dominoes/
524+
[lc838py]: ../push-dominoes.py
524525
[lc1139]: https://leetcode.com/problems/largest-1-bordered-square/
525526
[lc1277]: https://leetcode.com/problems/count-square-submatrices-with-all-ones/
526527
[lc1314]: https://leetcode.com/problems/matrix-block-sum/

leetcode/push-dominoes.py

+224
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
# 838. Push Dominoes
2+
# 🟠 Medium
3+
#
4+
# https://leetcode.com/problems/push-dominoes/
5+
#
6+
# Tags: Two Pointers - String - Dynamic Programming
7+
8+
import timeit
9+
from collections import deque
10+
11+
# 1000 calls
12+
# » TwoPointers 0.01016 seconds
13+
# » BFS 0.01630 seconds
14+
# » DP 0.02575 seconds
15+
16+
# Iterate over the dominoes checking the current value. When we find a
17+
# "L" or "R" value, handle that portion of the string. We keep a pointer
18+
# to the leftmost value that has been processed. When the iterator finds
19+
# a "L" value, it will check the value under the leftmost pointer, if it
20+
# is a "R" it will use a shrinking window to push left dominoes to the
21+
# right and right dominoes to the left until the pushes cancel each
22+
# other in the middle. If a neutral "." is under the left pointer, then
23+
# all the dominoes left of the iterator get pushed to the left. Once we
24+
# process that section, we update the left pointer. When the iterator
25+
# finds a "R" value, it checks the element under the left pointer, if
26+
# it finds a "R", it updates all values between the pointers to "R" and
27+
# adjusts the left pointer. If it finds a ".", it will adjust the
28+
# pointer without updating any values.
29+
#
30+
# Time complexity: O(n) - Any value is visited at most twice.
31+
# Space complexity: O(n) - The input is cast to a list to work with the
32+
# values, then parsed to string to return the expected format.
33+
#
34+
# Runtime: 478 ms, faster than 61.69%
35+
# Memory Usage: 15.8 MB, less than 83.88%
36+
class TwoPointers:
37+
def pushDominoes(self, dominoes: str) -> str:
38+
# Make a mutable copy of the input.
39+
dom = list(dominoes)
40+
# Initialize a left pointer.
41+
l = 0
42+
# Iterate over the positions.
43+
for i in range(len(dom)):
44+
if dom[i] == ".":
45+
continue
46+
# Check what happens when we push that section to the left.
47+
if dom[i] == "L":
48+
# If the left domino was not pushed.
49+
if dom[l] == ".":
50+
# If the leftmost domino was not pushed, they all
51+
# fall to the left.
52+
for j in range(l, i):
53+
dom[j] = "L"
54+
else:
55+
# The left domino is a "R" two pointers.
56+
r = i
57+
while l < r:
58+
dom[l] = "R"
59+
dom[r] = "L"
60+
l += 1
61+
r -= 1
62+
# Central dominoes in uneven length chains do
63+
# not get pushed.
64+
# Adjust the left pointer to the start of the next
65+
# sequence
66+
l = i + 1
67+
else:
68+
# Current domino is an "R"
69+
if dom[l] == "R":
70+
for j in range(l, i):
71+
dom[j] = "R"
72+
# Else do nothing, there was no right push.
73+
# Adjust the left pointer to the current "R"
74+
l = i
75+
# If the last right pushed didn't find a left pushed domino,
76+
# push all the dominoes to the right.
77+
if l < len(dominoes) and dominoes[l] == "R":
78+
for j in range(l + 1, len(dominoes)):
79+
dom[j] = "R"
80+
return "".join(dom)
81+
82+
83+
# The dynamic programming solution stores the forces of the left and
84+
# right pushes and adds them to decide if a domino falls right, left, or
85+
# stands on its own.
86+
#
87+
# Time complexity: O(n) - We visit each domino twice.
88+
# Space complexity: O(n) - The pushes array has the same length as the
89+
# input string.
90+
#
91+
# Runtime: 430 ms, faster than 70.56%
92+
# Memory Usage: 19.8 MB, less than 34.11%
93+
class DP:
94+
def pushDominoes(self, dominoes: str) -> str:
95+
# Store the force of the pushes received by a single domino.
96+
push = [0] * len(dominoes)
97+
# Current push values.
98+
lp = rp = 0
99+
# Iterate over start and end simultaneously.
100+
for i in range(len(dominoes)):
101+
# Update the left and right pointers.
102+
l, r = i, -i - 1
103+
# Compute the right push from the left.
104+
# If the position contains an "R" reset to max push.
105+
if dominoes[l] == "R":
106+
rp = len(dominoes)
107+
# If the position contains an "L" reset to no push.
108+
elif dominoes[l] == "L":
109+
rp = 0
110+
# Otherwise, if there was a current push, reduce by 1.
111+
elif dominoes[l] == "." and rp > 0:
112+
rp -= 1
113+
# Add the computed push to the cumulative.
114+
push[l] += rp
115+
# Compute the left push from the right.
116+
# If the position contains an "L" reset to max push.
117+
if dominoes[r] == "L":
118+
lp = len(dominoes)
119+
# If the position contains an "R" reset to no push.
120+
elif dominoes[r] == "R":
121+
lp = 0
122+
# Otherwise, if there was a current push, reduce by 1.
123+
elif dominoes[r] == "." and lp > 0:
124+
lp -= 1
125+
# Subtract (left push) the computed push from the cumulative.
126+
push[r] -= lp
127+
# Iterate over the cumulative array, for each position, return
128+
# the value of where the domino fell.
129+
return "".join("." if p == 0 else "L" if p < 0 else "R" for p in push)
130+
131+
132+
# This is a neat idea that I saw in the NeetCode YouTube channel at:
133+
#
134+
#
135+
# We can look at the time, seconds they call them in the description, as
136+
# the levels of a BFS algorithm, each second, nodes that are falling
137+
# right or left get processed, we compute how they will affect their
138+
# neighbors, and any neighbor that is caused to fall is added to the
139+
# queue to be processed as part of the next level.
140+
#
141+
# Time complexity: O(n) - We iterate over all the nodes initially, to
142+
# find which nodes we need to process. Once we start processing nodes,
143+
# we visit each one a maximum of one time.
144+
# Space complexity: O(n) - The queue that we use to process nodes, and
145+
# the list that we use to store intermediate states, take, or can take
146+
# up to, O(n)
147+
#
148+
# Runtime: 444 ms, faster than 68.93%
149+
# Memory Usage: 18.4 MB, less than 53.04%
150+
class BFS:
151+
def pushDominoes(self, dominoes: str) -> str:
152+
# Cast to a list to have a mutable data structure.
153+
dom = list(dominoes)
154+
# Create a double ended queue populated with the indices of all
155+
# nodes that are initially pushed.
156+
q = deque([i for i in range(len(dominoes)) if dominoes[i] != "."])
157+
# Keep processing nodes while we have moving dominoes.
158+
while q:
159+
# Process nodes left to right.
160+
idx = q.popleft()
161+
# This is the most complex case, we need to check if it will
162+
# push the node next to it.
163+
if dom[idx] == "R":
164+
# If the next domino is standing, check the one after.
165+
if idx + 1 < len(dom) and dom[idx + 1] == ".":
166+
# If the one after is falling left, they will
167+
# balance each other.
168+
if idx + 2 < len(dom) and dom[idx + 2] == "L":
169+
q.popleft()
170+
# If the one after is not falling left, the next
171+
# domino will be pushed right.
172+
else:
173+
q.append(idx + 1)
174+
dom[idx + 1] = "R"
175+
# The left case is easier because we already handled the
176+
# contiguous R <=> L case.
177+
elif dom[idx] == "L" and idx > 0 and dom[idx - 1] == ".":
178+
dom[idx - 1] = "L"
179+
q.append(idx - 1)
180+
return "".join(dom)
181+
182+
183+
def test():
184+
executors = [
185+
TwoPointers,
186+
DP,
187+
BFS,
188+
]
189+
tests = [
190+
[".", "."],
191+
["R", "R"],
192+
["L", "L"],
193+
[".L", "LL"],
194+
["R.", "RR"],
195+
[".R", ".R"],
196+
["L.", "L."],
197+
["RR.L", "RR.L"],
198+
["....", "...."],
199+
["..L.", "LLL."],
200+
["..R.", "..RR"],
201+
["RRRRRRL...", "RRRRRRL..."],
202+
[".L.R...LR.....", "LL.RR.LLRRRRRR"],
203+
["...R...LR.....", "...RR.LLRRRRRR"],
204+
[".L.R...LR..L..", "LL.RR.LLRRLL.."],
205+
]
206+
for executor in executors:
207+
start = timeit.default_timer()
208+
for _ in range(1):
209+
for col, t in enumerate(tests):
210+
sol = executor()
211+
result = sol.pushDominoes(t[0])
212+
exp = t[1]
213+
assert result == exp, (
214+
f"\033[93m» {result} <> {exp}\033[91m for"
215+
+ f" test {col} using \033[1m{executor.__name__}"
216+
)
217+
stop = timeit.default_timer()
218+
used = str(round(stop - start, 5))
219+
cols = "{0:20}{1:10}{2:10}"
220+
res = cols.format(executor.__name__, used, "seconds")
221+
print(f"\033[92m» {res}\033[0m")
222+
223+
224+
test()

0 commit comments

Comments
 (0)