Real-omn-dp-dfsbfs-top-solutions-are-not-omn

Approach 2 - Dynamic Programming

So far I hope you guys have observed that the answer, distance between one cell to another cell, is dependent. It's better to visualize it with the example below.

Let's assume we are in a cinema. The theater is very dark. You can't see how far you are sitting from Zeroes. (They have popcorn!). You are sitting at "here". And you are trying to find a Zero that can pass you the popcorn (lol what?).

The only way to find out how far you are from Zeroes is to whisper to the person in your front or left, "hey do you have popcorn?". Luckily they both answer yes and you know the shortest distance from them is 0 + 1 = 1.

Now with the same logic, if someone is sitting at "here" and he is whispering to his front and left "hey do you have popcorn?". Now they will say "I don't. But I know I'm one distance far away from Zeroes who have popcorns. And you immediately know you are 1 + 1 = 2 distances far away to Zeroes that can give you popcorn! As a result , the result should look like

Observation

Basically the theater example illustrates the idea of recursion. But we also observed that it can be done using an m x n array with prefilled values in it. A particular value in the array depends of their minimum values from its top, left right and bottom + 1.

From the above example, result[1][1] = min( result[0][1], result[1][0] ) + 1.

Introducing DP - Bottom Up

This technique is called Bottom-up Dynamic Programming. Instead of starting from the beginning and calling the function recursively to the bottom until the base case is hit, we start from the bottom and ramp up.

  1. We will create a dp array with prefilled values in it. In this problem, I will set 0 for each cell because the distance between 0 and 0 is 0.

2. Check if each cell of mat is 1. If so, get the location of that cell. Then get the values from the left and top cell of that location at dp. Assign the location at dp to be the minimum values +1.

3. Repeat the step two for right and bottom.

Walk-Through:

2. Now let's find the left and right value (denoted *) at dp. They are both =0. Therefore, dp at (0,1) is min(0,0) + 1 = 1. Now it looks like

3. Now repeating this step for finding all ones, get the top & left values and reassign the values at dp. Finally our dp array looks like:

4. However, we are not done yet because we did not consider the values on the right and bottom. We will need to repeat the process again (starting from the bottom right of mat) for their right & bottom values.

You may ask why can't we do it in one pass and check all four directions at a time?

It is because we are looping and starting from the top left to right bottom of mat. If we find a 1, then we need to look at the top and left values in dp that prefilled with 0s. We cannot check the right and bottom there because we are not even there yet. The right and bottom values at each iteration are zeroes by default in the dp array. Therefore, if you do min(top, left, right, bot) will always be zero!

Assume we are at "here". If we were to find the minimum values of its four directions, it will be 0 because the right & bot at dp are 0. But the distance should be 2 right? This is why we can only check its top and left values first.

Actual Code in Python:

class Solution     def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]:         row = len(mat)         col = len(mat[0])         MAXNum = float("inf")

    # Prefill 0 to all cells
        dp = [[0 for in range(col)] for in range(row)]
    # From the top to bot
        for i in range(row):             for j in range(col):                 if mat[i][j]==1:                     left = dp[i][j-1] if j>0 else MAXNum                     top = dp[i-1][j] if i>0 else MAXNum                     dp[i][j] = min(left,top)+1

From the bot to top

        for i in range(row-1,-1,-1):             for j in range(col-1,-1,-1):                 if mat[i][j]==1:                     print(i,j)                     right = dp[i][j+1] if j

Summary:

This is a very good problem to practice BFS/DFS and DP. Whenever you are asked to find the smallest/ minimum/ nearest values, you should definitely consider BFS first.

BFS is more suitable for searching vertices closer to the given source while DFS is more suitable when there are solutions away from source.

The DP solution is just so beautiful to me. There are a lot of DFS/BFS problems that can be optimized using DP. Sometimes it is easier to do it with DP while sometimes it is not.

Where is BFS and DFS used in real life?

BFS is good to use when the depth of the tree can vary or if a single answer is needed—for example, the shortest path in a tree. If the entire tree should be traversed, DFS is a better option. BFS always returns an optimal answer, but this is not guaranteed for DFS.

What is the difference between DP and BFS?

BFS is a subset of DP in a way, and its implementation with priority list is just boosting its performance. ... then go for BFS in O(E). Dynamic Programing is an algorithm where you decompose a problem into the same problem but smaller sizes. And then recursively to search through the whole solution space.

Which is best BFS or DFS?

BFS works better when a user searches for the vertices that stay closer to any given source. DFS works better when a user can find the solutions away from any given source. The amount of memory required for BFS is more than that of DFS. The amount of memory required for DFS is less than that of BFS.

Which gives optimal solution BFS or DFS?

Answer: BFS is complete and optimal, while DFS is not guaranteed to halt when there are loops. What is the advantage of DFS over BFS? Answer: If m is the maximum path length and b is the branching factor, the space complexity for DFS is mb while for BFS it is bm.

It is amazing how many graph, tree and string problems simply boil down to a DFS (Depth-first search) / BFS (Breadth-first search). Today we are going to explore this basic pattern in a novel way and apply the intuition gained to solve some medium problems on Leetcode.

Let us build on top of pattern 0. First of, a tree can be thought of as a connected acyclic graph with N nodes and N-1 edges. Any two vertices are connected by exactly one path. So naturally the question arises, what about a DFS or a BFS on binary trees ? well there are 6 possible DFS traversals for binary trees ( 3 rose to fame while the other 3 are just symmetric )

  1. left, right, root ( Postorder) ~ 4. right, left, root
  2. left, root, right ( Inorder) ~ 5. right, root, left
  3. root, left, right ( Preorder) ~ 6. root, right, left

And there is one BFS which is the level order traversal ( can be done using queue). Let us analyze the DFS pattern using a stack which we are already familiar with.

DFS is all about diving as deep as possible before coming back to take a dive again. Below is the iterative DFS pattern using a stack that will allow us to solve a ton of problems. ( iterative introduced first so as to develop intuition)

DFS magic spell: 1]push to stack, 2] pop top , 3] retrieve unvisited neighbours of top, push them to stack 4] repeat 1,2,3 while stack not empty. Now form a rap !

144. Preorder Traversal

Let’s walk through the above spell using an example tree.

Two things to ponder on as we move further:

1] Why are we using stack for DFS , couldn’t we use a queue ? ( always remember : stack for DFS, imagine a vertical flow | queue for BFS, horizontal flow, more on this later)

2] How do we extend this DFS process to general graphs or graphs disguised as matrices ( as in most LC problems). ( Include a mechanism to track visited)

323. Number of Connected Components in an Undirected Graph

The base problem upon which we will build other solutions is this one which directly states to find the number of connected components in the given graph. We could use DFS / BFS to solve this. Below is the DFS code using the stack spell.

200. Number of Islands

This falls under a general category of problems where in we just need to find the number of connected components but the details could be twisted.

The intuition here is that once we find a “1” we could initiate a new group, if we do a DFS from that cell in all 4 directions we can reach all 1’s connected to that cell and thus belonging to same group. We could mark these as visited and move on to count other groups. One optimization here is that we don’t need the matrix later so we could as well destroy the visited cells by placing a special character saving us extra memory for the visited array.

Even if we needed the matrix later one could always restore the original value after the dfs call. Below is a simple recursive DFS solution.

547. Friend Circles

Same concept, find connected components. The only twist is that the connected neighbors are presented in a different form. Here we don’t destroy the matrix, but use an array to keep track of visited ( friendZoned ). Notice the stack pattern, it is exactly the same as in connected components problem. All that changes is the way neighbors are defined. If we were to write an iterative version for the islands problems, it would also be very similar.

I broke this post into 2 parts as it got too long, I will visit BFS in the next part. So let’s conclude this part by pondering on why we always use stack for dfs and queue for bfs. Have you ever wondered why we don’t use queue for dfs or stack for bfs? questions like these really give us some insights on the difference between stacks and queues.

So using a stack I could pop 2 and push it’s kids and keep doing so eventually exhausting 2’s subtrees, 3 stays calmly in the stack just below the part where the real push-pop action is going, we pop 3 when all subtrees of 2 are done. This feature of stack is essential for DFS.

While in a queue, I could dequeue 2 and enqueue it’s subtrees which go behind 3 as it was already sitting in the queue. So the next time I dequeue I get 3 and only after that do I move on to visiting 2’s subtrees, this is essentially a BFS !

For me this revelation was pure bliss. Take a moment to celebrate the history of Computer Science and the geniuses behind these simple yet powerful ideas.

http://people.idsia.ch/~juergen/bauer.html

Do comment your views and feel free to connect with me on LI : https://www.linkedin.com/in/sourabh-reddy

I think this problem is very classic with high frequency in tech interviews. It involves with DFS/BFS and Dynamic Programming (DP). Let's get started with this problem!

1. Read the Problem

We are given an m x n binary matrix. We are to return the distance of the nearest 0 for each cell. The distance between two adjacent cells is 1.

For example, given mat = [[0,0,0],[0,1,0],[1,1,1]], we need to return [[0,0,0],[0,1,0],[1,2,1]]

  • For the middle cell, result[1][1], it is still 1 because the nearest 0 for this cell is on its left, right OR top, so the shortest distance is 1.
  • For result[2][1], the nearest 0 is either going:

- top > top

- left > top

- right > top

The distances for all these paths are 2. Therefore at result[2][1], we'll have a 2.

2. Figure Out the Target ( What do we really want?)

Our goal is to find all 1s. If we find a 1, then we should check all four directions (top, left, right &bottom) to see if we find any 0s. If we do, we immediately know the answer is 1 because we've found the nearest 0.

🤔 But what happens if all four directions of a particular cell are all non-zeroes? In that case, we will need to go visit all cells from all four directions and repeat the same process again, which is to check the cells in all four directions for a value of zero! ....

Now, what is the first thing comes into your mind? It seems like we can use Depth First Search (DFS), doesn't it?

Hold on a second! Even though DFS (brute force way😅) might solve this problem, it will not be the optimal solution because

  • we are not trying to explore all possible paths. Once we find a zero, we can just return the distance from it. In that case, we are not interested in the rest of the directions.
  • it's very time inefficient - O(N^2) where N = m x n

Imagine if your interviewer asks you to improve the efficiency, what would you do? 😅

Let's think about our target again 💪

Our goal was to find all 1s, right? How about finding all 0s instead? But why?

If we were to find all 1s and then to search for any zeroes from all directions (like what we originally did above), we could only find the distance for that cell only.

BUT, if we find all 0s and go outward finding the 1s, we can find at most FOUR distances at a time!

3. What Data structure & Algorithms?

We are going to find all zeroes. Then we will ask each of the zero -Do you have 1s in your neighbors (four directions)? If so, please tell your neighbors to update their numbers to be (your number + 1) which is 1.

As our goal is not to find all possible paths or outcomes, instead of using a DFS, a Breadth-First Search (BFS) would be a better choice here.

And we know to implement a BFS, we often use a queue. It's like pizza and cheese. 🤡

And we definitely need to keep track of all visited cells. You don't want to search over and over again to the same cell which might ruin our solution here.

But instead of using a Set, I will be changing all 1s to a specific value. When we later search for the 1s, instead we will search for that specific value.

* You could use a set. It's going to the same. I'm just trying to be special here 😛

* Please ask interviewer if you are allowed to mutate the input array because I'm going to mutate it anyway 🫢

Algorithms:

  1. Define a queue array.
  2. Loop over the matrix, if the cell is a zero, add the coordinates (i, j) to the end of the queue. If it is a one, change the value to be "#". ( It could be anything. You'll see why we do that).
  3. Loop over the queue, pop off the first element of the queue and check its neighbors (top, left, right, bottom) for the value "#". If we find it, then re-assign that neighbor's value to be the the popped element's number + 1. Then add the coordinates of the neighbors to the queue

Walk-Through:

- Step 1

We will add the coordinates of all zeroes to our queue. And change all ones to "#" in one pass. Our matrix &queue will become:

- Step 2

We will pop the first element in the queue and search for "#" in four directions.

Assume we are at "here" now. We will check its right and bottom. We do not find a "#", so we do nothing. Let's fast-forward a bit.

Now we are at (1,0), again we will check its right and bottom. We see a "#" on the right. That's exciting! Let's re-assign this value to be the value at (1,0) + 1 = 1. Now it becomes

We'll do the same on its bottom cell. Same logic and result and it becomes

We'll then add both coordinates (1,1) and (2,0) to the end of our queue.

queue was = [ (0,0), (0,1), (0,2), (1,0), (1,2) ]

Queue now is = [ (1,2) , (1,1), (2,0) ]

- Step 3 (keep looping)

...

...

At all three locations in the queue, we do not find "#" so we stop and return the result.

Complexity

We basically search the whole matrix for all zeroes and add them to the queue. And we also perform a BFS with the elements in queue.

Time Complexity: O(m x n) , where m = number of the rows and n = number of the columns.

Space Complexity: O(m x n) due to the queue we used.

Actual Code in Python:

class Solution: def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]:

queue = deque()
row = len(mat)
col = len(mat[0])
for i in range(row):
  for j in range(col):
    if mat[i][j]==0:
      queue.append((i,j))
    else:
      mat[i][j]="#"
while queue:
  i,j = queue.popleft()
  for dx,dy in (1,0),(-1,0),(0,1),(0,-1):
    newX = dx+i
    newY = dy+j
    if newX>=0 and newX=0 and newY