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