Graphs Flashcards

1
Q
  1. Number of Islands
    Solved
    Medium
    Topics
    Companies: Facebook Microsoft Amazon TikTok Google

Given an m x n 2D binary grid grid which represents a map of ‘1’s (land) and ‘0’s (water), return the number of islands.

An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.

Example 1:

Input: grid = [
[“1”,”1”,”1”,”1”,”0”],
[“1”,”1”,”0”,”1”,”0”],
[“1”,”1”,”0”,”0”,”0”],
[“0”,”0”,”0”,”0”,”0”]
]
Output: 1

Example 2:

Input: grid = [
[“1”,”1”,”0”,”0”,”0”],
[“1”,”1”,”0”,”0”,”0”],
[“0”,”0”,”1”,”0”,”0”],
[“0”,”0”,”0”,”1”,”1”]
]
Output: 3

Constraints:

m == grid.length
n == grid[i].length
1 <= m, n <= 300
grid[i][j] is '0' or '1'.

https://leetcode.com/problems/number-of-islands/description/

A

from collections import deque
class Solution(object):
def numIslands(self, grid):
“””
:type grid: List[List[str]]
:rtype: int
“””

    seen = set()
    q = deque()
    q.append((0,0))
    ROWS = len(grid)
    COLS = len(grid[0])
    directions = [[0,1], [0,-1], [1,0], [-1,0]]

    def bfs(ir, ic):
        q.append((ir, ic))
        while q:
            r,c = q.popleft()
            for rm, cm in directions:
                row, col = rm + r, cm + c
                if row in range(ROWS) and col in range(COLS) and (row, col) not in seen and grid[r][c] == "1":
                    seen.add((row, col))
                    q.append((row, col))
    islands = 0
    for r in range(ROWS):
        for c in range(COLS):
            if (r,c) not in seen and grid[r][c] == "1":
                bfs(r, c)
                islands += 1
    return islands

DFS

from collections import deque
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
if not grid:
return 0
rows = len(grid)
cols = len(grid[0])
visited = set()
directions = [[0,1], [0,-1], [-1,0], [1,0]]
islands = 0

    def dfs(ir, ic):
        visitstack = []
        visitstack.append((ir, ic))
        visited.add((ir, ic))
        while visitstack:
            r,c = visitstack.pop()
            for rm, cm in directions:
                row, col = rm+r, cm+c
                if row in range(rows) and col in range(cols) and (row, col) not in visited and grid[row][col] == "1":
                    visited.add((row,col))
                    visitstack.append((row, col))

    for r in range(rows):
        for c in range(cols):
            if (r,c) not in visited and grid[r][c] == "1":
                dfs(r,c)
                islands += 1
    return islands
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q
  1. Max Area of Island
    Solved
    Medium
    Topics
    Companies

You are given an m x n binary matrix grid. An island is a group of 1’s (representing land) connected 4-directionally (horizontal or vertical.) You may assume all four edges of the grid are surrounded by water.

The area of an island is the number of cells with a value 1 in the island.

Return the maximum area of an island in grid. If there is no island, return 0.

Example 1:

Input: grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]]
Output: 6
Explanation: The answer is not 11, because the island must be connected 4-directionally.

Example 2:

Input: grid = [[0,0,0,0,0,0,0,0]]
Output: 0

Constraints:

m == grid.length
n == grid[i].length
1 <= m, n <= 50
grid[i][j] is either 0 or 1.

https://leetcode.com/problems/max-area-of-island/

A
class UnionFind:
    def \_\_init\_\_(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n
        self.size = [0] * n
    
    def find(self, node):
        if self.parent[node] != node:
            self.parent[node] = self.find(self.parent[node])
        return self.parent[node]
    
    def union(self, a, b):
        pa, pb = self.find(a), self.find(b)
        if pa != pb:
            if self.rank[pa] > self.rank[pb]:
                self.parent[pb] = pa
                self.size[pa] += self.size[pb]
            elif self.rank[pa] < self.rank[pb]:
                self.parent[pa] = pb
                self.size[pb] += self.size[pa]
            else:
                self.parent[pb] = pa
                self.rank[pa] += 1
                self.size[pa] += self.size[pb]
            return True
        else:
            # They are already connected
            return False
    def init_component(self, c):
        if self.size[c] == 0:
            self.size[c] = 1

class Solution:
    def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
        ROWS = len(grid)
        COLS = len(grid[0])
        total = ROWS * COLS
        uf = UnionFind(total)

        def idx(r, c):
            return r * COLS + c
        directions = [[0, 1], [0,-1], [1,0], [-1,0]]

        for r in range(ROWS):
            for c in range(COLS):
                if grid[r][c] == 1:
                    uf.init_component(idx(r,c))
                    for rm, cm in directions:
                        row = rm + r
                        col = cm + c
                        if row in range(ROWS) and col in range(COLS) and grid[row][col] == 1:
                            uf.init_component(idx(row,col))
                            uf.union(idx(r,c), idx(row, col))
        area = max(uf.size)
        return area
BFS

from collections import deque
class Solution(object):
    def maxAreaOfIsland(self, grid):
        """
        :type grid: List[List[int]]
        :rtype: int
        """
        seen = set()
        rows = len(grid)
        cols = len(grid[0])
        max_area = 0
        def bfs(r, c):
            q = deque()
            q.append((r,c))
            seen.add((r,c))
            directions = [[0,1], [0,-1], [1,0], [-1,0]]
            area = 0
            while q:
                row,col = q.popleft()
                area += 1
                for dr, dc  in directions:
                    r = dr + row
                    c = dc + col
                    if ((r,c ) not in seen and r in range(rows) and c in range(cols) and grid[r][c] == 1):
                        q.append((r,c))
                        seen.add((r,c))
            return area
        for r in range(rows):
            for c in range(cols):
                if ((r,c) not in seen and grid[r][c] == 1):
                    max_area = max(max_area, bfs(r,c))
        return max_area
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q
  1. Clone Graph
    Solved
    Medium
    Topics
    Companies: FB

Given a reference of a node in a connected undirected graph.

Return a deep copy (clone) of the graph.

Each node in the graph contains a value (int) and a list (List[Node]) of its neighbors.

class Node {
public int val;
public List<Node> neighbors;
}</Node>

Test case format:

For simplicity, each node’s value is the same as the node’s index (1-indexed). For example, the first node with val == 1, the second node with val == 2, and so on. The graph is represented in the test case using an adjacency list.

An adjacency list is a collection of unordered lists used to represent a finite graph. Each list describes the set of neighbors of a node in the graph.

The given node will always be the first node with val = 1. You must return the copy of the given node as a reference to the cloned graph.

Example 1:

Input: adjList = [[2,4],[1,3],[2,4],[1,3]]
Output: [[2,4],[1,3],[2,4],[1,3]]
Explanation: There are 4 nodes in the graph.
1st node (val = 1)’s neighbors are 2nd node (val = 2) and 4th node (val = 4).
2nd node (val = 2)’s neighbors are 1st node (val = 1) and 3rd node (val = 3).
3rd node (val = 3)’s neighbors are 2nd node (val = 2) and 4th node (val = 4).
4th node (val = 4)’s neighbors are 1st node (val = 1) and 3rd node (val = 3).

Example 2:

Input: adjList = [[]]
Output: [[]]
Explanation: Note that the input contains one empty list. The graph consists of only one node with val = 1 and it does not have any neighbors.

Example 3:

Input: adjList = []
Output: []
Explanation: This an empty graph, it does not have any nodes.

Constraints:

The number of nodes in the graph is in the range [0, 100].
1 <= Node.val <= 100
Node.val is unique for each node.
There are no repeated edges and no self-loops in the graph.
The Graph is connected and all nodes can be visited starting from the given node.

https://leetcode.com/problems/clone-graph/description/

A

Definition for a Node.

from collections import deque
"""
# Definition for a Node.
class Node:
    def \_\_init\_\_(self, val = 0, neighbors = None):
        self.val = val
        self.neighbors = neighbors if neighbors is not None else []
"""

from typing import Optional
class Solution:
    def cloneGraph(self, node: Optional['Node']) -> Optional['Node']:
        if not node:
            return None
        copy_dict = defaultdict(lambda: Node())

        def get_clone(original):
            clone = copy_dict[original]
            clone.val = original.val
            return clone

        q = deque()
        q.append(node)
        seen = set()
        ret = get_clone(node)
        while q:
            cur = q.popleft()
            if cur in seen:
                continue
            new_cur = get_clone(cur)
            for neighbor in cur.neighbors:
                new_cur_neighbor = get_clone(neighbor)
                new_cur.neighbors.append(new_cur_neighbor)
                q.append(neighbor)
            seen.add(cur)
        return ret
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q
  1. Walls and Gates
    Solved
    Medium
    Topics
    Companies

You are given an m x n grid rooms initialized with these three possible values.

-1 A wall or an obstacle.
0 A gate.
INF Infinity means an empty room. We use the value 231 - 1 = 2147483647 to represent INF as you may assume that the distance to a gate is less than 2147483647.

Fill each empty room with the distance to its nearest gate. If it is impossible to reach a gate, it should be filled with INF.

Example 1:

Input: rooms = [[2147483647,-1,0,2147483647],[2147483647,2147483647,2147483647,-1],[2147483647,-1,2147483647,-1],[0,-1,2147483647,2147483647]]
Output: [[3,-1,0,1],[2,2,1,-1],[1,-1,2,-1],[0,-1,3,4]]

Example 2:

Input: rooms = [[-1]]
Output: [[-1]]

Constraints:

m == rooms.length
n == rooms[i].length
1 <= m, n <= 250
rooms[i][j] is -1, 0, or 231 - 1.

https://leetcode.com/problems/walls-and-gates/description/

A

Multiple BFS start points, each time a cell is discovered, it has to be discovered from it’s nearest gate. So once it’s seen, it does not need to be visited again. Seen can be done by either storing row,cols or by just checking if the distance is not infinity.

Saves some space.

from collections import deque
class Solution(object):
    def wallsAndGates(self, rooms):
        """
        :type rooms: List[List[int]]
        :rtype: None Do not return anything, modify rooms in-place instead.
        """
        rows = len(rooms)
        cols = len(rooms[0])
        q = deque()
        movements = [[0,1], [1,0], [-1, 0], [0,-1]]
        is_valid = lambda r,c: (r >= 0 and r < rows) and (c >= 0 and c < cols) and rooms[r][c] == 2147483647
        def bfs(q):
            dist = 0
            while q:
                qlen = len(q)
                for _ in range(qlen):
                    r, c = q.popleft()
                    for rm, cm in movements:
                        row, col = r + rm, c + cm
                        if is_valid(row, col):
                            q.append((row, col))
                            # This is important here, as soon as you see the neighbor set its distance so the is_valid will act like
                            # seen set. The first time a cell is seen is from the closest gate.
                            rooms[row][col] = dist+1
                dist += 1
        for r in range(rows):
            for c in range(cols):
                if rooms[r][c] == 0:
                    q.append((r, c))
        bfs(q)
        return rooms

Standard BFS with seen

from collections import deque

class Solution:
    def wallsAndGates(self, rooms: List[List[int]]) -> None:
        """
        Do not return anything, modify rooms in-place instead.
        """
        gates = []
        valid_rooms = set()
        rows = len(rooms)
        cols = len(rooms[0])
        for r in range(rows):
            for c in range(cols):
                if rooms[r][c] == 0:
                    gates.append([r,c])
                elif rooms[r][c] == 2147483647:
                    valid_rooms.add((r,c))

        directions = [[0,1], [1,0], [0,-1], [-1,0]]

        def bfs(start_i, start_j):
            seen = set()
            q = deque()
            q.append((start_i, start_j))
            distance = 1
            while q:
                qlen = len(q)
                for _ in range(qlen):
                    r,c = q.popleft()
                    for dr, dc in directions:
                        row, col = dr + r, dc + c
                        if (row, col) in valid_rooms and (row, col) not in seen and rooms[row][col] > distance:
                            q.append((row, col))
                            seen.add((row, col))
                            rooms[row][col] = distance

                distance += 1
        for gr, gc in gates:
            bfs(gr, gc)
        return rooms
        
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q
  1. Rotting Oranges
    Solved
    Medium
    Topics
    Companies

You are given an m x n grid where each cell can have one of three values:

0 representing an empty cell,
1 representing a fresh orange, or
2 representing a rotten orange.

Every minute, any fresh orange that is 4-directionally adjacent to a rotten orange becomes rotten.

Return the minimum number of minutes that must elapse until no cell has a fresh orange. If this is impossible, return -1.

Example 1:

Input: grid = [[2,1,1],[1,1,0],[0,1,1]]
Output: 4

Example 2:

Input: grid = [[2,1,1],[0,1,1],[1,0,1]]
Output: -1
Explanation: The orange in the bottom left corner (row 2, column 0) is never rotten, because rotting only happens 4-directionally.

Example 3:

Input: grid = [[0,2]]
Output: 0
Explanation: Since there are already no fresh oranges at minute 0, the answer is just 0.

Constraints:

m == grid.length
n == grid[i].length
1 <= m, n <= 10
grid[i][j] is 0, 1, or 2.

https://leetcode.com/problems/rotting-oranges/description/

A

from collections import deque
class Solution(object):
def orangesRotting(self, grid):
“””
:type grid: List[List[int]]
:rtype: int
“””
fresh = 0
q = deque()
rows = len(grid)
cols = len(grid[0])
visited = set() # maybe not needed.
for r in range(rows):
for c in range(cols):
if grid[r][c] == 1:
fresh += 1
elif grid[r][c] == 2:
q.append((r,c))
time = -1
is_valid = lambda r,c: 0 <= r < rows and 0 <= c < cols and grid[r][c] == 1
directions = [[0,1], [0,-1], [1,0], [-1,0]]
while q:
qlen = len(q)
time += 1
for _ in range(qlen):
rv, cv = q.popleft()
for rm, cm in directions:
row, col = rm + rv, cm + cv
if is_valid(row, col):
grid[row][col] = 2
q.append((row, col))
fresh -= 1

    if fresh > 0:
        return -1
    else:
        return max(0, time)
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q
  1. Pacific Atlantic Water Flow
    Solved
    Medium
    Topics
    Companies

There is an m x n rectangular island that borders both the Pacific Ocean and Atlantic Ocean. The Pacific Ocean touches the island’s left and top edges, and the Atlantic Ocean touches the island’s right and bottom edges.

The island is partitioned into a grid of square cells. You are given an m x n integer matrix heights where heights[r][c] represents the height above sea level of the cell at coordinate (r, c).

The island receives a lot of rain, and the rain water can flow to neighboring cells directly north, south, east, and west if the neighboring cell’s height is less than or equal to the current cell’s height. Water can flow from any cell adjacent to an ocean into the ocean.

Return a 2D list of grid coordinates result where result[i] = [ri, ci] denotes that rain water can flow from cell (ri, ci) to both the Pacific and Atlantic oceans.

Example 1:

Input: heights = [[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],[5,1,1,2,4]]
Output: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]
Explanation: The following cells can flow to the Pacific and Atlantic oceans, as shown below:
[0,4]: [0,4] -> Pacific Ocean
[0,4] -> Atlantic Ocean
[1,3]: [1,3] -> [0,3] -> Pacific Ocean
[1,3] -> [1,4] -> Atlantic Ocean
[1,4]: [1,4] -> [1,3] -> [0,3] -> Pacific Ocean
[1,4] -> Atlantic Ocean
[2,2]: [2,2] -> [1,2] -> [0,2] -> Pacific Ocean
[2,2] -> [2,3] -> [2,4] -> Atlantic Ocean
[3,0]: [3,0] -> Pacific Ocean
[3,0] -> [4,0] -> Atlantic Ocean
[3,1]: [3,1] -> [3,0] -> Pacific Ocean
[3,1] -> [4,1] -> Atlantic Ocean
[4,0]: [4,0] -> Pacific Ocean
[4,0] -> Atlantic Ocean
Note that there are other possible paths for these cells to flow to the Pacific and Atlantic oceans.

Example 2:

Input: heights = [[1]]
Output: [[0,0]]
Explanation: The water can flow from the only cell to the Pacific and Atlantic oceans.

Constraints:

m == heights.length
n == heights[r].length
1 <= m, n <= 200
0 <= heights[r][c] <= 105

https://leetcode.com/problems/pacific-atlantic-water-flow/description/

A

Double BFS/DFS from each coast find intersection

from collections import defaultdict
class Solution(object):
def pacificAtlantic(self, heights):
“””
:type heights: List[List[int]]
:rtype: List[List[int]]
“””
rows = len(heights)
cols = len(heights[0])
p_starts = set()
a_starts = set()
for r in range(rows):
p_starts.add((r, 0))
a_starts.add((r, cols-1))
for c in range(cols):
p_starts.add((0, c))
a_starts.add((rows-1, c))

    p_seen = set()
    a_seen = set()
    directions = [[0,1], [1,0], [0, -1], [-1,0]]

    def dfs(r,c, seen):
        visit_stack = []
        visit_stack.append((r,c))
        seen.add((r, c))
        is_valid = lambda r, c: 0 <= r < rows and 0 <= c < cols and (r,c) not in seen
        while visit_stack:
            ir, ic = visit_stack.pop()
            for rm, cm in directions:
                row, col = ir + rm, ic + cm
                if is_valid(row, col) and heights[ir][ic] <= heights[row][col]:
                    seen.add((row, col))
                    visit_stack.append((row,col))

    for r,c in p_starts:
        dfs(r,c, p_seen)
    for r,c in a_starts:
        dfs(r,c, a_seen)
    output = p_seen.intersection(a_seen)
    
    return output
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q
  1. Surrounded Regions
    Solved
    Medium
    Topics
    Companies

Given an m x n matrix board containing ‘X’ and ‘O’, capture all regions that are 4-directionally surrounded by ‘X’.

A region is captured by flipping all ‘O’s into ‘X’s in that surrounded region.

Example 1:

Input: board = [[“X”,”X”,”X”,”X”],[“X”,”O”,”O”,”X”],[“X”,”X”,”O”,”X”],[“X”,”O”,”X”,”X”]]
Output: [[“X”,”X”,”X”,”X”],[“X”,”X”,”X”,”X”],[“X”,”X”,”X”,”X”],[“X”,”O”,”X”,”X”]]
Explanation: Notice that an ‘O’ should not be flipped if:
- It is on the border, or
- It is adjacent to an ‘O’ that should not be flipped.
The bottom ‘O’ is on the border, so it is not flipped.
The other three ‘O’ form a surrounded region, so they are flipped.

Example 2:

Input: board = [[“X”]]
Output: [[“X”]]

Constraints:

m == board.length
n == board[i].length
1 <= m, n <= 200
board[i][j] is 'X' or 'O'.

https://leetcode.com/problems/surrounded-regions/description/

A

Start from the border, mark all you can reach as “E” then go over board again and replace any E with O and any O with X.
# Similar to the atlantic ocean problem, start on the coast.

class Solution(object):
def solve(self, board):
“””
:type board: List[List[str]]
:rtype: None Do not return anything, modify board in-place instead.
“””
directions = [(0, 1), (1, 0), (-1,0), (0,-1)]
seen = set()
rows = len(board)
cols = len(board[0])
is_border = lambda r,c: (r == 0 or r == rows -1) or (c == 0 or c == cols -1)
is_valid = lambda r, c: 0 <= r < rows and 0 <= c < cols and (r,c) not in seen and board[r][c] == “O” and not is_border(r,c)

    def dfs(r,c):
        board[r][c] = "E"
        for rm, cm in directions:
            row, col = r + rm, c + cm
            if is_valid(row, col):
                seen.add((row, col))
                dfs(row, col)
    
    for r in range(rows):
        for c in range(cols):
            if is_border(r, c) and board[r][c] == "O":
                seen.add((r, c))
                dfs(r,c)
    
    for r in range(rows):
        for c in range(cols):
            if board[r][c] == "E":
                board[r][c] = "O"
            else:
                board[r][c] = "X"
    
    return board
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q
  1. Course Schedule
    Solved
    Medium
    Topics
    Companies
    Hint

There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai.

For example, the pair [0, 1], indicates that to take course 0 you have to first take course 1.

Return true if you can finish all courses. Otherwise, return false.

Example 1:

Input: numCourses = 2, prerequisites = [[1,0]]
Output: true
Explanation: There are a total of 2 courses to take.
To take course 1 you should have finished course 0. So it is possible.

Example 2:

Input: numCourses = 2, prerequisites = [[1,0],[0,1]]
Output: false
Explanation: There are a total of 2 courses to take.
To take course 1 you should have finished course 0, and to take course 0 you should also have finished course 1. So it is impossible.

Constraints:

1 <= numCourses <= 2000
0 <= prerequisites.length <= 5000
prerequisites[i].length == 2
0 <= ai, bi < numCourses
All the pairs prerequisites[i] are unique.

https://leetcode.com/problems/course-schedule/description/

A
from collections import defaultdict

class Solution(object):
    def canFinish(self, numCourses, prerequisites):
        """
        :type numCourses: int
        :type prerequisites: List[List[int]]
        :rtype: bool
        """
        adj = defaultdict(list)
        for course, preq in prerequisites:
            adj[course].append(preq)
        ncourses = numCourses
        # Global Seen, we do not need to re-visit courses that have been completed.
        seen = set()
        def dfs(c):
            if not adj[c]:
                ## Course has no preq.
                return True
            # This represents the current dfs stack.
            cur_seen = set()
            stack = []
            stack.append(c)
            while stack:
                course = stack[-1]
                # We do not pop the course yet because we do not if it's processed or not
                # One way to see it is that it needs to be popped twice first to add it's children and 
                # second when all nodes bellow it are processed. 
                if course not in seen:
                    # Seeing this node the first time.
                    cur_seen.add(course)
                    seen.add(course)
                else:
                    course = stack.pop()
                    if course in cur_seen:
                        cur_seen.remove(course)
                for preq in adj[course]:
                    if preq not in seen:
                        stack.append(preq)
                    elif preq in cur_seen:
                        # Cycle
                        return False  
                
            return True
        
        for c in range(numCourses):
            canComplete = dfs(c)
            if not canComplete:
                return False
            else:
                ncourses -= 1
        if ncourses > 0:
            return False
        return True
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q
  1. Course Schedule II
    Solved
    Medium
    Topics
    Companies
    Hint

There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai.

For example, the pair [0, 1], indicates that to take course 0 you have to first take course 1.

Return the ordering of courses you should take to finish all courses. If there are many valid answers, return any of them. If it is impossible to finish all courses, return an empty array.

Example 1:

Input: numCourses = 2, prerequisites = [[1,0]]
Output: [0,1]
Explanation: There are a total of 2 courses to take. To take course 1 you should have finished course 0. So the correct course order is [0,1].

Example 2:

Input: numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
Output: [0,2,1,3]
Explanation: There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2. Both courses 1 and 2 should be taken after you finished course 0.
So one correct course order is [0,1,2,3]. Another correct ordering is [0,2,1,3].

Example 3:

Input: numCourses = 1, prerequisites = []
Output: [0]

Constraints:

1 <= numCourses <= 2000
0 <= prerequisites.length <= numCourses * (numCourses - 1)
prerequisites[i].length == 2
0 <= ai, bi < numCourses
ai != bi
All the pairs [ai, bi] are distinct.

https://leetcode.com/problems/course-schedule-ii/description/

A

Topological Sort
from collections import defaultdict
~~~
class Solution(object):
def findOrder(self, numCourses, prerequisites):
“””
:type numCourses: int
:type prerequisites: List[List[int]]
:rtype: List[int]
“””
order = []
seen = set()

    m = defaultdict(list)
    ncourses = numCourses
    for course, preq in prerequisites:
        m[course].append(preq)

    def dfs(start):
        if start in seen:
            return True
        cur_seen = set()
        stack = [start]
        
        while stack:
            course = stack[-1]

            if course not in seen:
                seen.add(course)
                cur_seen.add(course)
            else:
                course = stack.pop()
                if course in cur_seen:
                    order.append(course)
                    cur_seen.remove(course)
                m[course] = []
            for preq in m[course]:
                if preq not in seen:
                    stack.append(preq)
                else:
                    if preq in cur_seen:
                        return False
        return True

    for c in range(numCourses):
        if not dfs(c):
            return []
        ncourses -= 1
    if ncourses > 0:
        return []
    return order ~~~

BFS Top Sort

from collections import deque, defaultdict

class Solution:
    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        # BFS
        q = deque()
        order = []
        indegree = [0] * numCourses
        adj = defaultdict(list)
        for course, preq in prerequisites:
            # Note this is preq -> courses it feeds into
            # So we can reduce their indegrees.
            adj[preq].append(course)
            indegree[course] += 1
        for course in range(numCourses):
            if indegree[course] == 0:
                q.append(course)
        seen = set()
        while q:
            course = q.popleft()
            seen.add(course)
            for preq in adj[course]:
                indegree[preq] -= 1
                if indegree[preq] == 0:
                    q.append(preq)
            order.append(course)
        if len(order) != numCourses:
            return []
        return order
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q
  1. Graph Valid Tree
    Solved
    Medium
    Topics
    Companies
    Hint

You have a graph of n nodes labeled from 0 to n - 1. You are given an integer n and a list of edges where edges[i] = [ai, bi] indicates that there is an undirected edge between nodes ai and bi in the graph.

Return true if the edges of the given graph make up a valid tree, and false otherwise.

Example 1:

Input: n = 5, edges = [[0,1],[0,2],[0,3],[1,4]]
Output: true

Example 2:

Input: n = 5, edges = [[0,1],[1,2],[2,3],[1,3],[1,4]]
Output: false

Constraints:

1 <= n <= 2000
0 <= edges.length <= 5000
edges[i].length == 2
0 <= ai, bi < n
ai != bi
There are no self-loops or repeated edges.

https://leetcode.com/problems/graph-valid-tree/description/

A

from collections import defaultdict

class Solution(object):
def validTree(self, n, edges):
“””
:type n: int
:type edges: List[List[int]]
:rtype: bool
“””
stack = []
visited = set()
adj = defaultdict(list)
for a, b in edges:
adj[a].append(b)
adj[b].append(a)
stack.append((0, -1))

    while stack:
        node, parent = stack.pop()
        visited.add(node)
        for neighbor in adj[node]:
            if neighbor not in visited:
                stack.append((neighbor, node))
            elif neighbor != parent:
                return False
    return len(visited) == n
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q
  1. Number of Connected Components in an Undirected Graph
    Solved
    Medium
    Topics
    Companies

You have a graph of n nodes. You are given an integer n and an array edges where edges[i] = [ai, bi] indicates that there is an edge between ai and bi in the graph.

Return the number of connected components in the graph.

Example 1:

Input: n = 5, edges = [[0,1],[1,2],[3,4]]
Output: 2

Example 2:

Input: n = 5, edges = [[0,1],[1,2],[2,3],[3,4]]
Output: 1

Constraints:

1 <= n <= 2000
1 <= edges.length <= 5000
edges[i].length == 2
0 <= ai <= bi < n
ai != bi
There are no repeated edges.

https://leetcode.com/problems/number-of-connected-components-in-an-undirected-graph/description/

A

Can use DFS or BFS or Union Find

DFS
class Solution(object):
def countComponents(self, n, edges):
“””
:type n: int
:type edges: List[List[int]]
:rtype: int
“””
seen = set()
adj_list = defaultdict(list)
for i, j in edges:
adj_list[i].append(j)
adj_list[j].append(i)

    def dfs(i):
        seen.add(i)
        neighbors = adj_list[i]
        for neighbor in neighbors:
            if neighbor not in seen:
                dfs(neighbor)

    cc = 0
    for v in range(n):
        if v not in adj_list:
            cc += 1
        elif v not in seen:
            dfs(v)
            cc += 1
    return cc 

Union Find
from collections import defaultdict

class Solution:
def countComponents(self, n: int, edges: List[List[int]]) -> int:

    parent = [i for i in range(n)]
    rank = [0] * n

    def find(node):
        if parent[node] != node:
            parent[node] = find(parent[node])
        return parent[node]
    
    def union(a, b):
        pa, pb = find(a), find(b)

        if pa != pb:
            if rank[pa] > rank[pb]:
                parent[pb] = pa
            elif rank[pb] > rank[pa]:
                parent[pa] = pb
            else:
                parent[pb] = pa
                rank[pa] += 1

    adj = defaultdict(list)  
    for a, b in edges:
        adj[a].append(b)
        adj[b].append(a)
    
    for a in range(n):
        for b in adj[a]:
            union(a,b)
    
    cc = sum(1 for i in range(n) if parent[i] == i)
    return cc
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q
  1. Redundant Connection
    Solved
    Medium
    Topics
    Companies

In this problem, a tree is an undirected graph that is connected and has no cycles.

You are given a graph that started as a tree with n nodes labeled from 1 to n, with one additional edge added. The added edge has two different vertices chosen from 1 to n, and was not an edge that already existed. The graph is represented as an array edges of length n where edges[i] = [ai, bi] indicates that there is an edge between nodes ai and bi in the graph.

Return an edge that can be removed so that the resulting graph is a tree of n nodes. If there are multiple answers, return the answer that occurs last in the input.

Example 1:

Input: edges = [[1,2],[1,3],[2,3]]
Output: [2,3]

Example 2:

Input: edges = [[1,2],[2,3],[3,4],[1,4],[1,5]]
Output: [1,4]

Constraints:

n == edges.length
3 <= n <= 1000
edges[i].length == 2
1 <= ai < bi <= edges.length
ai != bi
There are no repeated edges.
The given graph is connected.

https://leetcode.com/problems/redundant-connection/description/

A

DFS BFS you need to run the search as you encounter each connection, and then build the graph.

```Union find or DFS/BFS

#DFS
from collections import defaultdict

class Solution(object):
def findRedundantConnection(self, edges):
“””
:type edges: List[List[int]]
:rtype: List[int]
“””
adj = defaultdict(list)

    output = []

    def dfs(start, target):
        stack = []
        seen = set()
        stack.append(start)

        while stack:
            node = stack.pop()
            if node == target:
                output.append([start, target])
                return True
            seen.add(node)

            for neighbor in adj[node]:
                if neighbor not in seen:
                    stack.append(neighbor)
        return False
    
    for a,b in edges:
        if dfs(a,b):
            return output[-1]
        adj[a].append(b)
        adj[b].append(a)    ~~~
Union Find, return if you find parent of both are same.
class Solution(object):
    def findRedundantConnection(self, edges):
        """
        :type edges: List[List[int]]
        :rtype: List[int]
        """
        par = {}
        rank = {}
        ret = [] 
        n_edges = len(edges)
        for i in range(1, n_edges + 1):
            par[i] = i
            rank[i] = 1
        
        def find(node):
            p = node
            while p != par[p]:
                par[p] = par[par[p]]
                p = par[p]
            return p
        
        def union(a, b):
            par_a, par_b = find(a), find(b)
            rank_a, rank_b = rank[par_a], rank[par_b]

            if par_a == par_b:
                return [a,b]
            if rank_a > rank_b:
                par[par_b] = par_a
                rank[par_a] += rank_b
            elif rank_b > rank_a:
                par[par_a] = par_b
                rank[par_b] += rank_a
            else:
                par[par_b] = par_a

        for a,b in edges:
            redundant = union(a,b)
            if redundant:
                ret = redundant
        return ret
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q
  1. Word Ladder
    Solved
    Hard
    Topics
    Companies

A transformation sequence from word beginWord to word endWord using a dictionary wordList is a sequence of words beginWord -> s1 -> s2 -> … -> sk such that:

Every adjacent pair of words differs by a single letter.
Every si for 1 <= i <= k is in wordList. Note that beginWord does not need to be in wordList.
sk == endWord

Given two words, beginWord and endWord, and a dictionary wordList, return the number of words in the shortest transformation sequence from beginWord to endWord, or 0 if no such sequence exists.

Example 1:

Input: beginWord = “hit”, endWord = “cog”, wordList = [“hot”,”dot”,”dog”,”lot”,”log”,”cog”]
Output: 5
Explanation: One shortest transformation sequence is “hit” -> “hot” -> “dot” -> “dog” -> cog”, which is 5 words long.

Example 2:

Input: beginWord = “hit”, endWord = “cog”, wordList = [“hot”,”dot”,”dog”,”lot”,”log”]
Output: 0
Explanation: The endWord “cog” is not in wordList, therefore there is no valid transformation sequence.

Constraints:

1 <= beginWord.length <= 10
endWord.length == beginWord.length
1 <= wordList.length <= 5000
wordList[i].length == beginWord.length
beginWord, endWord, and wordList[i] consist of lowercase English letters.
beginWord != endWord
All the words in wordList are unique.

https://leetcode.com/problems/word-ladder/description/

A

Create wildcard words from each word with one char replaced with * for each chars and use them to find neighbors. Then do a BFS

from collections import defaultdict, deque

class Solution(object):
def ladderLength(self, beginWord, endWord, wordList):
“””
:type beginWord: str
:type endWord: str
:type wordList: List[str]
:rtype: int
“””
adj = defaultdict(list)
wl = set(wordList)
wl.add(beginWord)
visited = set()
for word in wl:
for i in range(len(word)):
modified = word[:i] + “*” + word[i+1:]
adj[modified].append(word)
adj[word].append(modified)
q = deque()
q.append(beginWord)

    level = 0
    while q:
        level += 1
        looplen = len(q)
        for _ in range(looplen):
            w = q.popleft()
            if w == endWord:
                return level

            dup = set()
            dup.add(w)
            for alternative in adj[w]:
                for neighbor in adj[alternative]:
                    if neighbor not in dup:
                        if neighbor not in visited:
                            visited.add(neighbor)
                            q.append(neighbor)
                        dup.add(neighbor)
    return 0
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q
  1. Min Cost to Connect All Points
    Medium

You are given an array points representing integer coordinates of some points on a 2D-plane, where points[i] = [xi, yi].

The cost of connecting two points [xi, yi] and [xj, yj] is the manhattan distance between them: |xi - xj| + |yi - yj|, where |val| denotes the absolute value of val.

Return the minimum cost to make all points connected. All points are connected if there is exactly one simple path between any two points.

Example 1:

Input: points = [[0,0],[2,2],[3,10],[5,2],[7,0]]
Output: 20
Explanation:

We can connect the points as shown above to get the minimum cost of 20.
Notice that there is a unique path between every pair of points.

Example 2:

Input: points = [[3,12],[-2,5],[-4,1]]
Output: 18

Constraints:

1 <= points.length <= 1000
-106 <= xi, yi <= 106
All pairs (xi, yi) are distinct.

https://leetcode.com/problems/min-cost-to-connect-all-points/description/

A

select 0 as start node

Prims Algo,
~~~
# insert it in heap with dist 0
# while heap, pop the top most element,
# mark visited, else continue
# calculate its distance from all other points
# add them to min heap.
# This ensures we always pick the min distance from each point to the other and once we visited all of them, then we just finish through the loop.
# we track num vertices we have connected, so we can shortcut once we reach n-1
~~~

class Solution:
    def minCostConnectPoints(self, points: List[List[int]]) -> int:
        root = 0

        heap = [(0, root)]
        max_vertices = len(points) - 1
        vertices = 0
        seen = set()
        cost = 0
        while heap and vertices <= (len(points) - 1):
            dist, dest = heapq.heappop(heap)
            if dest in seen:
                continue
            seen.add(dest)
            cost += dist
            vertices += 1
            for ip, point in enumerate(points):
                if ip not in seen:
                    dist = abs(points[dest][0]-point[0]) + abs(points[dest][1]- point[1])
                    heapq.heappush(heap, (dist, ip))
        return cost
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q
  1. Network Delay Time
    Solved
    Medium
    Topics
    Companies
    Hint

You are given a network of n nodes, labeled from 1 to n. You are also given times, a list of travel times as directed edges times[i] = (ui, vi, wi), where ui is the source node, vi is the target node, and wi is the time it takes for a signal to travel from source to target.

We will send a signal from a given node k. Return the minimum time it takes for all the n nodes to receive the signal. If it is impossible for all the n nodes to receive the signal, return -1.

Example 1:

Input: times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2
Output: 2

Example 2:

Input: times = [[1,2,1]], n = 2, k = 1
Output: 1

Example 3:

Input: times = [[1,2,1]], n = 2, k = 2
Output: -1

Constraints:

1 <= k <= n <= 100
1 <= times.length <= 6000
times[i].length == 3
1 <= ui, vi <= n
ui != vi
0 <= wi <= 100
All the pairs (ui, vi) are unique. (i.e., no multiple edges.)

https://leetcode.com/problems/network-delay-time/

A

DJikstra

from collections import defaultdict
class Solution:
    def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
        heap = []
        adj = defaultdict(list)
        for src, dest, time in times:
            adj[src].append((time,dest))
        visited = set()
        heap.append((0, k))
        while heap:
            time, dest = heapq.heappop(heap)
            if dest in visited:
                continue    
            visited.add(dest)
            if len(visited) == n:
                return time
            for t, neighbor in adj[dest]:
                if neighbor not in visited:
                    heapq.heappush(heap, (t+time, neighbor))
        return -1
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
16
Q
  1. Swim in Rising Water
    Solved
    Hard
    Topics
    Companies
    Hint

You are given an n x n integer matrix grid where each value grid[i][j] represents the elevation at that point (i, j).

The rain starts to fall. At time t, the depth of the water everywhere is t. You can swim from a square to another 4-directionally adjacent square if and only if the elevation of both squares individually are at most t. You can swim infinite distances in zero time. Of course, you must stay within the boundaries of the grid during your swim.

Return the least time until you can reach the bottom right square (n - 1, n - 1) if you start at the top left square (0, 0).

Example 1:

Input: grid = [[0,2],[1,3]]
Output: 3
Explanation:
At time 0, you are in grid location (0, 0).
You cannot go anywhere else because 4-directionally adjacent neighbors have a higher elevation than t = 0.
You cannot reach point (1, 1) until time 3.
When the depth of water is 3, we can swim anywhere inside the grid.

Example 2:

Input: grid = [[0,1,2,3,4],[24,23,22,21,5],[12,13,14,15,16],[11,17,18,19,20],[10,9,8,7,6]]
Output: 16
Explanation: The final route is shown.
We need to wait until time 16 so that (0, 0) and (4, 4) are connected.

Constraints:

n == grid.length
n == grid[i].length
1 <= n <= 50
0 <= grid[i][j] < n2
Each value grid[i][j] is unique.

https://leetcode.com/problems/swim-in-rising-water/description/

A

Djikstra
~~~
from itertools import starmap
class Solution:
def swimInWater(self, grid: List[List[int]]) -> int:
movements = [[0,1], [0,-1], [-1,0], [1,0]]
rows = len(grid)
cols = len(grid[0])
visited = set()
is_valid = lambda rc: (lambda r,c: 0 <= r < rows and 0 <= c < cols and (r,c) not in visited)(*rc)
valid_movements = lambda r,c: filter(is_valid, starmap(lambda rm,cm: (r+rm, c+cm), movements))
pq = [(grid[0][0], (0,0))]

    while pq:
        cost, (r,c) = heapq.heappop(pq)
        if (r,c) in visited:
            continue
        visited.add((r,c))
        if (r,c) == (rows-1, cols-1):
            return cost
        for nxt_row, nxt_col in valid_movements(r,c):
            wait = max(0, grid[nxt_row][nxt_col] - cost)
            heapq.heappush(pq, (wait+cost, (nxt_row, nxt_col)))
    return -1 ~~~
17
Q
  1. Cheapest Flights Within K Stops
    Solved
    Medium
    Topics
    Companies

There are n cities connected by some number of flights. You are given an array flights where flights[i] = [fromi, toi, pricei] indicates that there is a flight from city fromi to city toi with cost pricei.

You are also given three integers src, dst, and k, return the cheapest price from src to dst with at most k stops. If there is no such route, return -1.

Example 1:

Input: n = 4, flights = [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src = 0, dst = 3, k = 1
Output: 700
Explanation:
The graph is shown above.
The optimal path with at most 1 stop from city 0 to 3 is marked in red and has cost 100 + 600 = 700.
Note that the path through cities [0,1,2,3] is cheaper but is invalid because it uses 2 stops.

Example 2:

Input: n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 1
Output: 200
Explanation:
The graph is shown above.
The optimal path with at most 1 stop from city 0 to 2 is marked in red and has cost 100 + 100 = 200.

Example 3:

Input: n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 0
Output: 500
Explanation:
The graph is shown above.
The optimal path with no stops from city 0 to 2 is marked in red and has cost 500.

Constraints:

1 <= n <= 100
0 <= flights.length <= (n * (n - 1) / 2)
flights[i].length == 3
0 <= fromi, toi < n
fromi != toi
1 <= pricei <= 104
There will not be any multiple flights between two cities.
0 <= src, dst, k < n
src != dst

https://leetcode.com/problems/cheapest-flights-within-k-stops/description/

A

DFS without visited, becasue we need to visit same node multiple times as it might cost less each time we visit.

from collections import defaultdict, deque
class Solution(object):
def findCheapestPrice(self, n, flights, src, dst, k):
“””
:type n: int
:type flights: List[List[int]]
:type src: int
:type dst: int
:type k: int
:rtype: int
“””
adj = defaultdict(list)
for i, j, price in flights:
adj[i].append((j, price))
q = deque()
visited = set()
q.append((0, src))
level = 0
cost_dict = [float(‘inf’)] * n
while q and level <= k:
qlen = len(q)
for _ in range(qlen):
cost, node = q.popleft()
if node not in adj:
continue
for n, p in adj[node]:
if cost_dict[n] > cost+p:
q.append((cost + p, n))
cost_dict[n] = cost + p
level += 1

    if cost_dict[dst] == float('inf'):
        return -1
    return cost_dict[dst]

Bellman Ford (slower but easy to read), key here is to compare current cost with the previous level cost.
class Solution:
def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int:

    prices = [float('inf')] * n
    prices[src] = 0
    level = 0
    while level < k+1:
        temp_prices = prices[:]
        for s,d,p in flights:
            if temp_prices[d] > prices[s] + p:
                temp_prices[d] = prices[s] + p
        prices = temp_prices[:]
        level += 1
    if prices[dst] == float('inf'):
        return -1
    else:
        return prices[dst]
18
Q
  1. Alien Dictionary
    Hard
    Topics
    Companies

There is a new alien language that uses the English alphabet. However, the order of the letters is unknown to you.

You are given a list of strings words from the alien language’s dictionary. Now it is claimed that the strings in words are
sorted lexicographically
by the rules of this new language.

If this claim is incorrect, and the given arrangement of string in words cannot correspond to any order of letters, return “”.

Otherwise, return a string of the unique letters in the new alien language sorted in lexicographically increasing order by the new language’s rules. If there are multiple solutions, return any of them.

Example 1:

Input: words = [“wrt”,”wrf”,”er”,”ett”,”rftt”]
Output: “wertf”

Example 2:

Input: words = [“z”,”x”]
Output: “zx”

Example 3:

Input: words = [“z”,”x”,”z”]
Output: “”
Explanation: The order is invalid, so return “”.

Constraints:

1 <= words.length <= 100
1 <= words[i].length <= 100
words[i] consists of only lowercase English letters.

https://leetcode.com/problems/alien-dictionary/description/

A

Recursive DFS
~~~
from collections import defaultdict
from itertools import zip_longest, pairwise
class Solution:
def alienOrder(self, words: List[str]) -> str:
adj_list = defaultdict(set)
possible_starts = set()

    for word in words:
        possible_starts = possible_starts.union(set(word))

    for word_a, word_b in pairwise(words):
        for a,b in zip_longest(word_a, word_b, fillvalue='#'):
            if b == "#":
                return ""
            elif a == "#":
                break
            if a!=b:
                adj_list[a].add(b)
                if b in possible_starts: possible_starts.remove(b)
                break

    out = []
    visited = set()

    def sort(start, path):
        nonlocal visited
        if start in path:
            return False
        path.add(start)
        
        if start in adj_list:
            for neighbor in adj_list[start]:
                if neighbor not in visited:
                    result = sort(neighbor, path)
                    if not result:
                        return False
        out.append(start)
        path.remove(start)
        visited.add(start)
        return True
    
    for start in adj_list.keys():
        if start not in visited:
            if not sort(start, set()):
                return ""
    left = list(possible_starts - set(out))
    out += left
    return "".join(out)[::-1] ~~~

Interative DFS
~~~
class Solution:
def alienOrder(self, words: List[str]) -> str:
adj = {c: set() for word in words for c in word}
for a,b in zip(words, words[1:]):
minlen = min(len(a), len(b))
if len(a) > len(b) and a[:minlen] == b[:minlen]:
return “”
# Once we find different letters in the words we just break
# because we have a relation a > b, any letter after that, does not matter because
# we can’t deduce a relation. eg. AX, BY we know A > B, thus AX is > BY but we can’t
# determine the relation between X and Y. So they can appear anywhere.
for ca, cb in zip(a,b):
if ca != cb:
adj[ca].add(cb)
break
stack = []
prime = words[0][0]
visited = set()
order = []
def dfs(start):
stack = []
cur_visited = set()
stack.append(start)
while stack:
c = stack[-1]
if c not in visited:
visited.add(c)
cur_visited.add(c)
else:
stack.pop()
if c in cur_visited:
cur_visited.remove(c)
order.append(c)
for n in adj[c]:
if n not in visited:
stack.append(n)
elif n in cur_visited:
## Cycle
return True
return False
starts = list(adj.keys())
for k in starts:
if k not in visited:
if dfs(k):
return “”
order.reverse()
return ““.join(order)
~~~

BFS

from collections import defaultdict, Counter, deque
class Solution(object):
    def alienOrder(self, words):
        indegree = defaultdict(int)
        adj = {c: set() for word in words for c in word}
        for a,b in zip(words, words[1:]):
            minlen = min(len(a), len(b))
            if len(a) > len(b) and a[:minlen] == b[:minlen]:
                return ""
            # Once we find different letters in the words we just break
            # because we have a relation a > b, any letter after that, does not matter because
            # we can't deduce a relation. eg.  AX, BY  we know A > B,  thus AX is > BY but we can't 
            # determine the relation between X and Y. So they can appear anywhere.
            for ca, cb in zip(a,b):
                if ca != cb:
                    if cb not in adj[ca]:
                        adj[ca].add(cb)
                        indegree[cb] += 1
                    break     
            
        q = deque()
        output = []
        for v in adj.keys():
            if v not in indegree or indegree[v] == 0:
                q.append(v)
        seen = set()
        while q:
            vertex = q.popleft()
            if vertex in seen:
                continue
            seen.add(vertex)
            for neighbor in adj[vertex]:
                if neighbor in indegree:
                    indegree[neighbor] -= 1
                if indegree[neighbor] == 0:
                    q.append(neighbor)
            output.append(vertex)
        
        if len(adj) > len(output):
            return ""
        return "".join(output)
19
Q
  1. Shortest Distance from All Buildings
    Hard

Companies: Facebook, Similar questions in multiple others (esp Google)
You are given an m x n grid grid of values 0, 1, or 2, where:

each 0 marks an empty land that you can pass by freely,
each 1 marks a building that you cannot pass through, and
each 2 marks an obstacle that you cannot pass through.
You want to build a house on an empty land that reaches all buildings in the shortest total travel distance. You can only move up, down, left, and right.

Return the shortest travel distance for such a house. If it is not possible to build such a house according to the above rules, return -1.

The total travel distance is the sum of the distances between the houses of the friends and the meeting point.

The distance is calculated using Manhattan Distance, where distance(p1, p2) = |p2.x - p1.x| + |p2.y - p1.y|.

Example 1:

Input: grid = [[1,0,2,0,1],[0,0,0,0,0],[0,0,1,0,0]]
Output: 7
Explanation: Given three buildings at (0,0), (0,4), (2,2), and an obstacle at (0,2).
The point (1,2) is an ideal empty land to build a house, as the total travel distance of 3+3+1=7 is minimal.
So return 7.
Example 2:

Input: grid = [[1,0]]
Output: 1
Example 3:

Input: grid = [[1]]
Output: -1

Constraints:

m == grid.length
n == grid[i].length
1 <= m, n <= 50
grid[i][j] is either 0, 1, or 2.
There will be at least one building in the grid.

Lookout for edge cases. Go brute force.

https://leetcode.com/problems/shortest-distance-from-all-buildings/description/

A
from collections import defaultdict
class Solution:
    def shortestDistance(self, grid: List[List[int]]) -> int:
        directions = [[0,1], [0,-1], [1,0], [-1,0]]
        distances = defaultdict(int)
        visitable_plots = set()
        rows = len(grid)
        cols = len(grid[0])

        def bfs(start_i, start_j, plots_to_visit):
            q = deque()
            q.append((start_i,start_j))
            distance = 1
            seen = set()
            while q:
                qlen = len(q)
                for _ in range(qlen):
                    i,j = q.popleft()
                    for rd, cd in directions:
                        row,col = rd+i, cd+j
                        if (row,col) not in seen and (row, col) in plots_to_visit:
                            seen.add((row, col))
                            q.append((row, col))
                            distances[(row, col)] += distance
                distance += 1
            plots_without_total_access = plots_to_visit - seen
            return plots_without_total_access
        
        for sr in range(rows):
            for sc in range(cols):
                if grid[sr][sc] == 0:
                    visitable_plots.add((sr,sc))
        
        for sr in range(rows):
            for sc in range(cols):
                if grid[sr][sc] == 1:
                    plots_without_total_access = bfs(sr, sc, visitable_plots)
                    if plots_without_total_access == visitable_plots:
                        # This building can't reach any plot
                        return -1
                    # These plots did not have access to all buildings, so we remove them from visitable plots
                    visitable_plots = visitable_plots - plots_without_total_access
           
        # Filter the dictionary items to only visitable lands
        plot_distances = filter(lambda i: i[0] in visitable_plots, distances.items())
        # Find the valid plot with minimum total distance
        return min(plot_distances, key=lambda i: i[1])[1] if distances else -1

O(N^2, M^2) For every building we visit every plot.

20
Q
  1. As Far from Land as Possible
    Solved
    Medium
    Topics
    Companies
    Hint
    Given an n x n grid containing only values 0 and 1, where 0 represents water and 1 represents land, find a water cell such that its distance to the nearest land cell is maximized, and return the distance. If no land or water exists in the grid, return -1.

The distance used in this problem is the Manhattan distance: the distance between two cells (x0, y0) and (x1, y1) is |x0 - x1| + |y0 - y1|.

Example 1:

Input: grid = [[1,0,1],[0,0,0],[1,0,1]]
Output: 2
Explanation: The cell (1, 1) is as far as possible from all the land with distance 2.
Example 2:

Input: grid = [[1,0,0],[0,0,0],[0,0,0]]
Output: 4
Explanation: The cell (2, 2) is as far as possible from all the land with distance 4.

Constraints:

n == grid.length
n == grid[i].length
1 <= n <= 100
grid[i][j] is 0 or 1

https://leetcode.com/problems/as-far-from-land-as-possible/description/

A
from collections import deque
from itertools import product
class Solution:
    
    def maxDistance(self, grid: List[List[int]]) -> int:
        rows = len(grid)
        cols = len(grid[0])

        is_valid = lambda r,c: 0 <= r < rows and 0 <= c < cols and grid[r][c] == 0
        directions = [[0,1], [0,-1], [1,0], [-1,0]]

        def get_valid(r,c):
            for rd, cd in directions:
                row, col = rd+r, cd+c
                if is_valid(rd+r, cd+c):
                    yield (row, col)

        lands = list(filter(lambda x: grid[x[0]][x[1]] == 1, product(range(rows), range(cols))))
    
        if len(lands) == rows*cols:
            return -1
        q = deque()
        q.extend(lands)
        dist = -1
        while q:
            qlen = len(q)
            dist += 1
            for _ in range(qlen):
                 r,c = q.popleft()
                 for row, col in get_valid(r,c):
                    grid[row][col] = 2
                    q.append((row,col))
        return dist
21
Q

Union Find General Purpose with Size, Rank and Parent of the connected components.

Can be used for any purpose, finding
* number of connected components (unique cells with size > 0 or set of uf.parent where values not -1)
* largest connected component: max uf.size
* what if? we add one land: go to idx, search up,left, down, right: for every unique parent, sum their size and add 1
* given elements one at a time to add as land: set 1 to uf.size for given element, set parent to itself in uf.parent, Then for all valid directions do union. add cell to visited.

A
from itertools import product, starmap
class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        class UnionFind:
            def \_\_init\_\_(self, mat):
                self.rows = len(mat)
                self.cols = len(mat[0])
                self.parent = [r * self.cols + c if mat[r][c] == "1" else -1 for r,c in product(range(self.rows), range(self.cols))]
                self.rank = [1 if mat[p[0]][p[1]] == "1" else 0 for p in product(range(self.rows), range(self.cols))]
                self.size = [1 if mat[p[0]][p[1]] == "1" else 0 for p in product(range(self.rows), range(self.cols))]
            
            def find(self, a):
                if isinstance(a, tuple) or isinstance(a, list):
                    a = a[0]*self.cols + a[1]
                if self.parent[a] != a:
                    self.parent[a] = self.find(self.parent[a])
                return self.parent[a]

            def union(self, a, b):
                a_idx = a[0]*self.cols + a[1]
                b_idx = b[0]*self.cols + b[1]
                parent_a = self.find(a_idx)
                parent_b = self.find(b_idx)
                if parent_a == parent_b:
                    return False

                if self.rank[parent_b] > self.rank[parent_a]:
                    self.parent[parent_a] = parent_b
                    self.size[parent_b] += self.size[parent_a]
                    self.size[parent_a] = 0
                    self.find(a_idx)
                else:
                    self.parent[parent_b] = parent_a
                    self.size[parent_a] += self.size[parent_b]
                    self.size[parent_b] = 0
                    if self.rank[parent_a] == self.rank[parent_b]:
                        self.rank[parent_a] += 1
                    self.find(b_idx)
                
                    
                
                return True
        uf = UnionFind(grid)
        rows = len(grid)
        cols = len(grid[0])
        directions = [[0,1], [1,0], [0,-1], [-1,0]]
        visited = set()
        is_valid = lambda rc: (lambda r,c: (r,c) not in visited and 0 <= r < rows and 0 <= c < cols and grid[r][c] == "1")(*rc)
        valid_movements = lambda r,c: filter(is_valid, starmap(lambda d,cell: [d[0]+cell[0], d[1]+cell[1]] , product(directions, [[r,c]])))

        for r in range(rows):
            for c in range(cols):
                if grid[r][c] == "1":
                    for row, col in valid_movements(r,c):
                        uf.union((r,c), (row, col))
                    visited.add((r,c))

        return sum(1 for _ in filter(lambda x: x > 0, uf.size))
22
Q
  1. Minimum Time to Visit a Cell In a Grid
    Hard
    Companies: Atlassian
    You are given a m x n matrix grid consisting of non-negative integers where grid[row][col] represents the minimum time required to be able to visit the cell (row, col), which means you can visit the cell (row, col) only when the time you visit it is greater than or equal to grid[row][col].

You are standing in the top-left cell of the matrix in the 0th second, and you must move to any adjacent cell in the four directions: up, down, left, and right. Each move you make takes 1 second.

Return the minimum time required in which you can visit the bottom-right cell of the matrix. If you cannot visit the bottom-right cell, then return -1.

Example 1:

Input: grid = [[0,1,3,2],[5,1,2,5],[4,3,8,6]]
Output: 7
Explanation: One of the paths that we can take is the following:
- at t = 0, we are on the cell (0,0).
- at t = 1, we move to the cell (0,1). It is possible because grid[0][1] <= 1.
- at t = 2, we move to the cell (1,1). It is possible because grid[1][1] <= 2.
- at t = 3, we move to the cell (1,2). It is possible because grid[1][2] <= 3.
- at t = 4, we move to the cell (1,1). It is possible because grid[1][1] <= 4.
- at t = 5, we move to the cell (1,2). It is possible because grid[1][2] <= 5.
- at t = 6, we move to the cell (1,3). It is possible because grid[1][3] <= 6.
- at t = 7, we move to the cell (2,3). It is possible because grid[2][3] <= 7.
The final time is 7. It can be shown that it is the minimum time possible.
Example 2:

Input: grid = [[0,2,4],[3,2,1],[1,0,4]]
Output: -1
Explanation: There is no path from the top left to the bottom-right cell.

Constraints:

m == grid.length
n == grid[i].length
2 <= m, n <= 1000
4 <= m * n <= 105
0 <= grid[i][j] <= 105
grid[0][0] == 0

https://leetcode.com/problems/minimum-time-to-visit-a-cell-in-a-grid/?envType=company&envId=atlassian&favoriteSlug=atlassian-thirty-days

A

Modified Djikstra
~~~
from collections import deque
from itertools import starmap
class Solution:
def minimumTime(self, grid: List[List[int]]) -> int:
if grid[0][1] > 1 and grid[1][0] > 1: return -1
directions = [[0,1], [1,0], [-1,0], [0,-1]]
visited = set()
rows = len(grid)
cols = len(grid[0])
movements = lambda r,c: starmap(lambda rm,cm: [rm+r, cm+c], directions)
is_valid = lambda r,c: 0 <= r < rows and 0 <= c < cols and (r,c) not in visited
valid_movements = lambda r,c: [m for m in movements(r,c) if is_valid(m[0], m[1])]
costs = defaultdict(lambda : float(‘inf’))

    heap = [(0,(0,0))]

    while heap:
        cur_time, (r,c) = heapq.heappop(heap)
        if (r,c) in visited:
            continue
        visited.add((r,c))
        if (r,c) == (rows-1, cols-1):
            return cur_time
        possible_vertices = valid_movements(r,c)
        for r_nxt, c_nxt in possible_vertices:
            # Get the time needed move to the next cell. sometimes the next cell will be low value so floor it to 0
            time_diff = max(0, (grid[r_nxt][c_nxt] - (cur_time+1)))
            base = cur_time + 1
            wait = time_diff + time_diff%2 # time_diff//2 * 2 = time_diff each flip-flop is 2 seconds.
            heapq.heappush(heap, (base + wait, (r_nxt, c_nxt)))
    return -1

~~~

O(mnLog(mn))

23
Q
  1. Number of Possible Sets of Closing Branches
    Hard
    Companies
    There is a company with n branches across the country, some of which are connected by roads. Initially, all branches are reachable from each other by traveling some roads.

The company has realized that they are spending an excessive amount of time traveling between their branches. As a result, they have decided to close down some of these branches (possibly none). However, they want to ensure that the remaining branches have a distance of at most maxDistance from each other.

The distance between two branches is the minimum total traveled length needed to reach one branch from another.

You are given integers n, maxDistance, and a 0-indexed 2D array roads, where roads[i] = [ui, vi, wi] represents the undirected road between branches ui and vi with length wi.

Return the number of possible sets of closing branches, so that any branch has a distance of at most maxDistance from any other.

Note that, after closing a branch, the company will no longer have access to any roads connected to it.

Note that, multiple roads are allowed.

Example 1:

Input: n = 3, maxDistance = 5, roads = [[0,1,2],[1,2,10],[0,2,10]]
Output: 5
Explanation: The possible sets of closing branches are:
- The set [2], after closing, active branches are [0,1] and they are reachable to each other within distance 2.
- The set [0,1], after closing, the active branch is [2].
- The set [1,2], after closing, the active branch is [0].
- The set [0,2], after closing, the active branch is [1].
- The set [0,1,2], after closing, there are no active branches.
It can be proven, that there are only 5 possible sets of closing branches.
Example 2:

Input: n = 3, maxDistance = 5, roads = [[0,1,20],[0,1,10],[1,2,2],[0,2,2]]
Output: 7
Explanation: The possible sets of closing branches are:
- The set [], after closing, active branches are [0,1,2] and they are reachable to each other within distance 4.
- The set [0], after closing, active branches are [1,2] and they are reachable to each other within distance 2.
- The set [1], after closing, active branches are [0,2] and they are reachable to each other within distance 2.
- The set [0,1], after closing, the active branch is [2].
- The set [1,2], after closing, the active branch is [0].
- The set [0,2], after closing, the active branch is [1].
- The set [0,1,2], after closing, there are no active branches.
It can be proven, that there are only 7 possible sets of closing branches.
Example 3:

Input: n = 1, maxDistance = 10, roads = []
Output: 2
Explanation: The possible sets of closing branches are:
- The set [], after closing, the active branch is [0].
- The set [0], after closing, there are no active branches.
It can be proven, that there are only 2 possible sets of closing branches.

Constraints:

1 <= n <= 10
1 <= maxDistance <= 105
0 <= roads.length <= 1000
roads[i].length == 3
0 <= ui, vi <= n - 1
ui != vi
1 <= wi <= 1000
All branches are reachable from each other by traveling some roads.

https://leetcode.com/problems/number-of-possible-sets-of-closing-branches/description

A

Djikstra (Fastest) also easiest to understand.

Select all possible combinations of stores, for each combination, do Djikstra for each store as src till all nodes have been found.
If at any point we get distance to be greater than maxDistance, return False or if heap finishes and we did not reach all stores (disconnected graph) return False.

for each combination count 1 if Djikstra is valid for ALL the nodes as starting points.

from collections import defaultdict
class Solution(object):
    def numberOfSets(self, n, maxDistance, roads):
        """
        :type n: int
        :type maxDistance: int
        :type roads: List[List[int]]
        :rtype: int
        """
        
        def djikstra(subset, graph, src):
            if len(subset) < 2:
                return 1
            shortest = defaultdict(lambda: float('inf'))
            pq = [(0, src)]
            while pq:
                dist, node = heapq.heappop(pq)
                if node in shortest:
                    continue
                shortest[node] = dist
                if dist > maxDistance:
                    return False
                if len(shortest.keys()) == len(subset):
                    return True
                for n, w in graph[node]:
                    if n not in subset or n in shortest:
                        continue
                    else:
                        heapq.heappush(pq, (w+dist, n))
            return False    
                
        
        graph = defaultdict(list)
        for s,d,w in roads:
            graph[s].append((d, w))
            graph[d].append((s, w))
        valid = 0
        for r in range(n+1):
            for subset in combinations(range(n), r):
                result = all([djikstra(set(subset), graph, start) for start in set(subset)])
                if result:
                    valid += 1
        return valid
            

Try every possible combination of stores apply floyd warshall on them
If for the subset we are looking for, any distance is > maxDistance , its an invalid subset so we do not count it.

from collections import defaultdict
from itertools import combinations, product

class Solution:
    def numberOfSets(self, n: int, maxDistance: int, roads: List[List[int]]) -> int:
        
        def check_subset(stores):
            dist = [[float('inf') for _ in range(n)] for _ in range(n)]
            for store in range(n):
                dist[store][store] = 0
            for a,b, w in roads:
                if a in stores and b in stores:
                    dist[a][b] = min(dist[a][b], w)
                    dist[b][a] = min(dist[b][a], w)
            
            for intermediate, a, b in product(range(n), range(n), range(n)):
                dist[a][b] = min(dist[a][b], dist[a][intermediate] + dist[intermediate][b])
            
            if len(list(filter(lambda x: dist[x[0]][x[1]] > maxDistance, product(stores, stores)))) > 0:
                return 0
            else:
                return 1
        
        valid_sets = 0
        for r in range(n+1):
            for subset in combinations(range(n), r):
                valid_sets += check_subset(set(subset))
        return valid_sets
24
Q
  1. Course Schedule II
    Solved
    Medium
    Topics
    Companies
    Hint
    There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai.

For example, the pair [0, 1], indicates that to take course 0 you have to first take course 1.
Return the ordering of courses you should take to finish all courses. If there are many valid answers, return any of them. If it is impossible to finish all courses, return an empty array.

Example 1:

Input: numCourses = 2, prerequisites = [[1,0]]
Output: [0,1]
Explanation: There are a total of 2 courses to take. To take course 1 you should have finished course 0. So the correct course order is [0,1].
Example 2:

Input: numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
Output: [0,2,1,3]
Explanation: There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2. Both courses 1 and 2 should be taken after you finished course 0.
So one correct course order is [0,1,2,3]. Another correct ordering is [0,2,1,3].
Example 3:

Input: numCourses = 1, prerequisites = []
Output: [0]

Constraints:

1 <= numCourses <= 2000
0 <= prerequisites.length <= numCourses * (numCourses - 1)
prerequisites[i].length == 2
0 <= ai, bi < numCourses
ai != bi
All the pairs [ai, bi] are distinct.

https://leetcode.com/problems/course-schedule-ii/description/

A
from collections import defaultdict, deque

class Solution:
    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        adj = defaultdict(list)
        indegree = defaultdict(int)
        for course in range(numCourses):
            indegree[course]
        
        for course, preq in prerequisites:
            adj[preq].append(course)
            indegree[course] += 1
        
        output = []
        q = deque()
        for course in indegree.keys():
            if indegree[course] == 0:
                q.append(course)
        
        while q:
            course = q.popleft()
            
            for preq in adj[course]:
                indegree[preq] -= 1
                if indegree[preq] == 0:
                    q.append(preq)
            output.append(course)
        
        return output if len(output) == numCourses else []

DFS
```
from collections import defaultdict
class Solution(object):
def findOrder(self, numCourses, prerequisites):
“””
:type numCourses: int
:type prerequisites: List[List[int]]
:rtype: List[int]
“””
adj = defaultdict(list)
indegree = defaultdict(int)
for n in range(numCourses):
indegree[n]

    for course, preq in prerequisites:
        adj[course].append(preq)
        indegree[preq] += 1
    sort = []
    stack = []
    for course in indegree:
        if indegree[course] == 0:
            stack.append(course)
    
    rec_stack = set()
    visited = set()
    while stack:
        course = stack[-1]
        if course not in visited:
            rec_stack.add(course)
            visited.add(course)
            all_preq_complete = True
            
            
            for preq in adj[course]:
                if preq not in visited:
                    stack.append(preq)
                    all_preq_complete = False
                elif preq in rec_stack:
                    return []
            if all_preq_complete:
                rec_stack.remove(course)
                sort.append(stack.pop())
        else:
            if course in rec_stack: 
                rec_stack.remove(course)
                sort.append(course)
            stack.pop()
    return sort if len(sort) == numCourses else [] ~~~
25
Q
  1. Course Schedule
    Solved
    Medium
    Topics
    Companies
    Hint
    There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai.

For example, the pair [0, 1], indicates that to take course 0 you have to first take course 1.
Return true if you can finish all courses. Otherwise, return false.

Example 1:

Input: numCourses = 2, prerequisites = [[1,0]]
Output: true
Explanation: There are a total of 2 courses to take.
To take course 1 you should have finished course 0. So it is possible.
Example 2:

Input: numCourses = 2, prerequisites = [[1,0],[0,1]]
Output: false
Explanation: There are a total of 2 courses to take.
To take course 1 you should have finished course 0, and to take course 0 you should also have finished course 1. So it is impossible.

Constraints:

1 <= numCourses <= 2000
0 <= prerequisites.length <= 5000
prerequisites[i].length == 2
0 <= ai, bi < numCourses
All the pairs prerequisites[i] are unique.

https://leetcode.com/problems/course-schedule/

A
from collections import defaultdict, deque 
class Solution:
    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        adj = defaultdict(list)
        indegree = defaultdict(int)
        for n in range(numCourses):
            indegree[n]
        
        for course, preq in prerequisites:
            adj[course].append(preq)
            indegree[preq] += 1
        sort = []

        q = deque()

        # courses having 0 indegree are starting points
        for k,v in indegree.items():
            if v == 0:
                q.append(k)
        
        visited = set()
        while q:
            course = q.popleft()
            
            for preq in adj[course]:
                indegree[preq] -= 1
                if indegree[preq] == 0:
                    q.append(preq)
            visited.add(course)
        
        # If the number of courses we can complete is the same as we were given, its possible, otherwise false
        if len(visited) == numCourses:
            return True
        else:
            False