tyristik's blog

By tyristik, history, 13 months ago, In English

Imagine doing bfs on a 2D $$$n \times n$$$ grid. Your code may look something like this:

import collections
NOT_VISITED, VISITED = 0, 1
n = int(input())
grid = [[NOT_VISITED] * n for i in range(n)]
queue = collections.deque([(0, 0)])
while len(queue):
    x, y = queue.popleft()
    for nx, ny in [(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)]:
        if 0 <= nx < n and 0 <= ny < n and grid[nx][ny] == NOT_VISITED:
            queue.append((nx, ny))
            grid[nx][ny] = VISITED
print(grid)

It works in $$$O(n^2)$$$, nothing interesting. But if we write it like this:

import collections
NOT_VISITED, VISITED = 0, 1
n = int(input())
grid = [[NOT_VISITED] * n for i in range(n)]
queue = collections.deque([(0, 0)])
while len(queue):
    x, y = queue.popleft()
    grid[x][y] = VISITED
    for nx, ny in [(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)]:
        if 0 <= nx < n and 0 <= ny < n and grid[nx][ny] == NOT_VISITED:
            queue.append((nx, ny))
print(grid)

The code now works in $$$O\left(\dfrac{4^n}{\sqrt{n}}\right)$$$ :skull: :skull: :skull:

Can you figure out why?

  • Vote: I like it
  • +29
  • Vote: I do not like it

| Write comment?
»
13 months ago, hide # |
 
Vote: I like it 0 Vote: I do not like it

In the second code, the cell is marked as visited, but its neighbors are not. This allows the same cell to be visited multiple times, leading to an exponential blow-up.

»
13 months ago, hide # |
 
Vote: I like it +1 Vote: I do not like it

In second version, you mark the current cell as visited before you check the neighbours.

That would mean that you might re visit the same cell multiple times cause the cell is being put back into queue for checking later even though it is marked as visited.

Also, can you explain how you got the time complexity for second code? i am still working on my skills and would be happy to know

  • »
    »
    13 months ago, hide # ^ |
     
    Vote: I like it +14 Vote: I do not like it

    The number of operations spent by the algorithm is $$$c \cdot \text{number of things added to the queue}$$$. You can try to convince yourself that the number of times cell $$$(x, y)$$$ gets added to the queue is the number of shortest paths from $$$(0, 0)$$$ to $$$(x, y)$$$, which is $$$\binom{x + y}{x}$$$.

    Therefore, the number of things added to the queue is $$$\sum_{x=0}^{n-1} \sum_{y = 0}^{n-1} \binom{x + y}{x}$$$. Applying hockey-stick once yields $$$\sum_{x=1}^n \binom{x + n - 1}{x} = \sum_{x=1}^n \binom{x + n - 1}{n - 1}$$$. Applying hockey-stick again yields $$$\binom{2n}{n} - 1$$$.

    One can use Stirling's approximation, $$$n! \sim \sqrt{2\pi n} (\frac{n}{e})^n$$$, to show that $$$O(\binom{2n}{n}) = O(\frac{4^n}{\sqrt{n}})$$$. I learned the last part just now thanks to Google AI lmao.

»
13 months ago, hide # |
 
Vote: I like it 0 Vote: I do not like it

Interesting, is there a similar issue for DFS using stack?

»
13 months ago, hide # |
 
Vote: I like it +2 Vote: I do not like it

tyristik The user is disabled.

»
13 months ago, hide # |
 
Vote: I like it -6 Vote: I do not like it

fix the second code by checking if the cell is visited before

 import collections
 NOT_VISITED, VISITED = 0, 1
 n = int(input())
 grid = [[NOT_VISITED] * n for i in range(n)]
 queue = collections.deque([(0, 0)])
 while len(queue):
     x, y = queue.popleft()
+    if grid[x][y] == VISITED:
+        continue
     grid[x][y] = VISITED
     for nx, ny in [(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)]:
         if 0 <= nx < n and 0 <= ny < n and grid[nx][ny] == NOT_VISITED:
             queue.append((nx, ny))
 print(grid)