Course Scheduler 1
Hi, here’s your problem today. This problem was recently asked by Google:
You are given a hash table where the key is a course code, and the value is a list of all the course codes that are prerequisites for the key. Return a valid ordering in which we can complete the courses. If no such ordering exists, return NULL.
Example:
{
'CSC300': ['CSC100', 'CSC200'],
'CSC200': ['CSC100'],
'CSC100': []
}This input should return the order that we need to take these courses:
[‘CSC100’, ‘CSC200’, ‘CSCS300’]
Here’s your starting point:
def courses_to_take(course_to_prereqs): # Fill this in.
courses = {
'CSC300': ['CSC100', 'CSC200'],
'CSC200': ['CSC100'],
'CSC100': []
}
print courses_to_take(courses)
# ['CSC100', 'CSC200', 'CSC300']Time: O(n), where n is the number of keys. The edges of the keys are already included in n, so we do not need to include them
Space: O(3n), we have 3 lists that can be the size of the graph
class Solution:
Works = {
'CSC300': ['CSC100', 'CSC200'],
'CSC200': ['CSC100'],
'CSC100': []
}
Does not work = {
'CSC300': ['CSC100', 'CSC200'],
'CSC200': ['CSC100'],
'CSC100': ['CSC300]
}
Works = {
'g': ['e', 'f'],
'f': ['d', 'c'],
'e': ['d', 'c', 'b'],
'd': ['b', 'a'],
'c': ['b', 'a'],
'b': ['a'],
'a': []
}Cycle in graph
Given an undirected graph, determine if a cycle exists in the graph.
undirected_graph_true = {
"A": ["B", "C"],
"B": ["A", "D"],
"C": ["A"],
"D": ["A", "B", "E"],
"E": ["D"]
}time: O n, do not include the edges of the graph bc they are included in the vertexes
space: O n, the visited map can grow to the size of the graph
strategies for graphs and how to recognize a graph problem
Know your directed/cyclic/tological/disjoined graphs
If they talk about things connecting, probably a graph problem
There are n cities. Some of them are connected, while some are not. If city a is connected directly with city b, and city b is connected directly with city c, then city a is connected indirectly with city c.
A province is a group of directly or indirectly connected cities and no other cities outside of the group.
You are given an n x n matrix isConnected where isConnected[i][j] = 1 if the ith city and the jth city are directly connected, and isConnected[i][j] = 0 otherwise.
Return the total number of provinces.
time: O N^2 + N. optimized path/union are amortized constant time, N bc we build the disjointed graph, optimized union find would be
space: N for the disjointed graph
class Solution:
-------- class Dg: ----def \_\_init\_\_(self, size): --------self.root = [i for i in range(size)] --------self.rank = [1 for i in range(size)] -------- ----def find(self, x): --------if x is not self.root[x]: ------------self.root[x] = self.find(self.root[x]) ------------return self.root[x] --------return x ---- ----def union(self, x, y): --------rootX = self.find(x) --------rootY = self.find(y) --------if rootX is not rootY: ------------if self.rank[rootX] [greater than] self.rank[rootY]: ----------------self.root[rootY] = rootX ------------elif self.rank[rootX] [less than] self.rank[rootY]: ----------------self.root[rootX] = rootY ------------else: ----------------self.root[rootY] = rootX ----------------self.rank[rootX] +=1 ------------return 1 --------return 0
Graph Valid Tree
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.
time: O (N + a(N)) where a(n) is the inverse ackerman function since we used optimized union and find. the ackerman function only gets as large as 4 for reasonable inputs, so in practice it is a constant time operation
space: O (N) to keep the array for root/graph
class Solution: ----def validTree(self, n: int, edges): --------if len(edges) != n -1: ------------return False --------dg = DisjointedGraph(n) -------- --------for x, y in edges: ------------if not dg.union(x, y): ----------------return False --------return True ---- class DisjointedGraph: ----def \_\_init\_\_(self, size): --------self.root = [i for i in range(size)] --------self.rank = [1 for i in range(size)] -------- ----def find(self, x): --------if x is not self.root[x]: ------------self.root[x] = self.find(self.root[x]) ------------return self.root[x] --------return x ---- ----def union(self, x, y): --------rootX = self.find(x) --------rootY = self.find(y) --------if rootX is rootY: ------------return False --------if rootX is not rootY: ------------if self.rank[rootX] [greater than] self.rank[rootY]: ----------------self.root[rootY] = rootX ------------elif self.rank[rootY] [greater than] self.rank[rootX]: ----------------self.root[rootX] = rootY ------------else: ----------------self.root[rootY] = rootX ----------------self.rank[rootX] += 1 --------return True
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
time: O (N + a(m)) where a(m) is ackerman’s function, which is constant, max goes up to 4
space: O N for the graph/response
class Solution:
class Dg:
The Earliest Moment When Everyone Become Friends
There are n people in a social group labeled from 0 to n - 1. You are given an array logs where logs[i] = [timestampi, xi, yi] indicates that xi and yi will be friends at the time timestampi.
Friendship is symmetric. That means if a is friends with b, then b is friends with a. Also, person a is acquainted with a person b if a is friends with b, or a is a friend of someone acquainted with b.
Return the earliest time for which every person became acquainted with every other person. If there is no such earliest time, return -1.
time: e*log(e) + N + e, sorting + create graph + loop through edges
space: N for the graph + N for recursion depth of quick sort (average could be log(n))
class Solution:
Smallest string with swaps
You are given a string s, and an array of pairs of indices in the string pairs where pairs[i] = [a, b] indicates 2 indices(0-indexed) of the string.
You can swap the characters at any pair of indices in the given pairs any number of times.
Return the lexicographically smallest string that s can be changed to after using the swaps.
Example 1:
Input: s = "dcab", pairs = [[0,3],[1,2]] Output: "bacd" Explaination: Swap s[0] and s[3], s = "bcad" Swap s[1] and s[2], s = "bacd" Example 2:
Input: s = "dcab", pairs = [[0,3],[1,2],[0,2]] Output: "abcd" Explaination: Swap s[0] and s[3], s = "bcad" Swap s[0] and s[2], s = "acbd" Swap s[1] and s[2], s = "abcd"
time: creating graph is N, greating defaultdict while keeping the lists sorted is Nlog(N), returning final response is N, so N + Nlog(n)
space: N for the graph, N for the default dict, so 2N or just N
class Solution: ----def smallestStringWithSwaps(self, s, pairs): --------from collections import defaultdict --------graph = Dg(len(s)) -------- --------for x, y in pairs: ------------graph.union(x, y) -------- --------m = defaultdict(lambda: []) --------for i in range(len(s)): ------------index = graph.find(i) ------------found_index = self.binary_search(s[i], m[index]) ------------m[index].insert(found_index, s[i]) -------- --------response = [] --------for i in range(len(s)): ------------response.append(m[graph.find(i)].pop(0)) --------return "".join(response) -------- ----def binary_search(self, target, arr): --------if len(arr) is 0: ------------return 0 --------high = len(arr)-1 --------low = 0 --------while high [greater than]= low: ------------mid = (high + low) // 2 ------------if arr[mid] is target: ----------------return mid ------------elif arr[mid] [greater than] target: ----------------high = mid - 1 ------------elif arr[mid] [less than] target: ----------------low = mid + 1 --------return low -------- class Dg: ----def \_\_init\_\_(self, size): --------self.root = [i for i in range(size)] --------self.rank = [1 for i in range(size)] ----def find(self, x): --------if x is not self.root[x]: ------------self.root[x] = self.find(self.root[x]) ------------return self.root[x] --------return x ----def union(self, x, y): --------if x [less than] y: ------------low = x ------------high = y --------else: ------------low = y ------------high = x --------rootX = self.find(low) --------rootY = self.find(high) --------if rootX is not rootY: ------------if self.rank[rootX] [greater than] self.rank[rootY]: ----------------self.root[rootY] = rootX ------------elif self.rank[rootX] [less than] self.rank[rootY]: ----------------self.root[rootX] = rootY ------------else: ----------------self.root[rootY] = rootX ----------------self.rank[rootX] +=1
You are given an array of variable pairs equations and an array of real numbers values, where equations[i] = [Ai, Bi] and values[i] represent the equation Ai / Bi = values[i]. Each Ai or Bi is a string that represents a single variable.
You are also given some queries, where queries[j] = [Cj, Dj] represents the jth query where you must find the answer for Cj / Dj = ?.
Return the answers to all queries. If a single answer cannot be determined, return -1.0.
Note: The input is always valid. You may assume that evaluating the queries will not result in division by zero and that there is no contradiction.
Example 1:
Input: equations = [[“a”,”b”],[“b”,”c”]], values = [2.0,3.0], queries = [[“a”,”c”],[“b”,”a”],[“a”,”e”],[“a”,”a”],[“x”,”x”]]
Output: [6.00000,0.50000,-1.00000,1.00000,-1.00000]
Explanation:
Given: a / b = 2.0, b / c = 3.0
queries are: a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ?
return: [6.0, 0.5, -1.0, 1.0, -1.0 ]
Example 2:
Input: equations = [[“a”,”b”],[“b”,”c”],[“bc”,”cd”]], values = [1.5,2.5,5.0], queries = [[“a”,”c”],[“c”,”b”],[“bc”,”cd”],[“cd”,”bc”]]
Output: [3.75000,0.40000,5.00000,0.20000]
Example 3:
Input: equations = [[“a”,”b”]], values = [0.5], queries = [[“a”,”b”],[“b”,”a”],[“a”,”c”],[“x”,”y”]]
Output: [0.50000,2.00000,-1.00000,-1.00000]
time: O((equations + queries )* log(N) ) where N is the number of nodes in the disjointed graph we make. We have to loop through equations to make the map and the graph, the queries to get the answer
space: O(equations), this would count the size of the map and graph. If we included size of output, we add queries to space too
class Solution:
class Dg:
BRUTE FORCE
time: O(equations * queries), each equation in the graph can be seen by each query in the second loop
space: O(N), the graph’s edges are 2N with no overlap, the keys then count for N. The recursion depth fo the backtracking is also N
class Solution:
find if path exists in graph
There is a bi-directional graph with n vertices, where each vertex is labeled from 0 to n - 1 (inclusive). The edges in the graph are represented as a 2D integer array edges, where each edges[i] = [ui, vi] denotes a bi-directional edge between vertex ui and vertex vi. Every vertex pair is connected by at most one edge, and no vertex has an edge to itself.
You want to determine if there is a valid path that exists from vertex start to vertex end.
Given edges and the integers n, start, and end, return true if there is a valid path from start to end, or false otherwise.
optimized union find, union all the edges, test if start is connected to end
time: N + E*ackerman(N)
space: N for the graph
alternative
time: N + E bc we traverse through all the edges to make the graph, then all the nodes in the search
space: N^2, worst-case each node is connected to each other node (check graph card, traversing all paths between 2 points explanation)
from collections import defaultdict
class Solution:
All Paths From Source to Target
Given a directed acyclic graph (DAG) of n nodes labeled from 0 to n - 1, find all possible paths from node 0 to node n - 1 and return them in any order.
The graph is given as follows: graph[i] is a list of all nodes you can visit from node i (i.e., there is a directed edge from node i to node graph[i][j]).
Example 1:
Input: graph = [[1,2],[3],[3],[]]
Output: [[0,1,3],[0,2,3]]
Explanation: There are two paths: 0 -> 1 -> 3 and 0 -> 2 -> 3.
time: O((2^N) * N), adding another node doubles the possible paths for a DAG, (undirected is N!), and N bc for each path we append an item to the result which takes N time
space: O((2^N) * N), we have that many paths that have N length in the response, but without response we just have N bc of the carry_list + recursion stack
from functools import lru_cache class Solution: ----def allPathsSourceTarget(self, graph): --------n = len(graph) - 1 --------response = [] -------- --------self.backtrack(graph, 0, [], response, n) --------return response ---- ----def backtrack(self, graph, index, carry_list, response, n): --------carry_list.append(index) --------if index == n: ------------response.append(list(carry_list)) ------------carry_list.pop() ------------return -------- --------for child in graph[index]: ------------self.backtrack(graph, child, carry_list, response, n) ------------ --------carry_list.pop()
Clone Graph
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 neighbors;
}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.
time: O(N + M) where N is the nodes and M are edges, it looks like it could be N^2, but you see each node during the DFS algo, and each node will call each of their edges. This can be confusing, but it is standard graph theory if you do DFS with a map checking if you have seen it before
space: O(n) bc of the map, the recursion stack would give O(H) but worst case H=N since each node could be connected to each other node
class Solution:
time/space are same as above
Reconstruct itinerary
You are given a list of airline tickets where tickets[i] = [fromi, toi] represent the departure and the arrival airports of one flight. Reconstruct the itinerary in order and return it.
All of the tickets belong to a man who departs from “JFK”, thus, the itinerary must begin with “JFK”. If there are multiple valid itineraries, you should return the itinerary that has the smallest lexical order when read as a single string.
For example, the itinerary [“JFK”, “LGA”] has a smaller lexical order than [“JFK”, “LGB”].
You may assume all tickets form at least one valid itinerary. You must use all the tickets once and only once.
Example 1:
Input: tickets = [[“MUC”,”LHR”],[“JFK”,”MUC”],[“SFO”,”SJC”],[“LHR”,”SFO”]]
Output: [“JFK”,”MUC”,”LHR”,”SFO”,”SJC”]
time: O(E^D) where E is the number of flights and D is the flights from airports
space: A + E A is the number of airports and E is the number of flights. The hashmaps we make are A+ E, recursive stack is at most E bc of the result
from collections import defaultdict
import random
class Solution:
populating next node’s pointer
You are given a perfect binary tree where all leaves are on the same level, and every parent has two children. The binary tree has the following definition:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
Populate each next pointer to point to its next right node. If there is no next right node, the next pointer should be set to NULL.Initially, all next pointers are set to NULL.
Example 1:
Input: root = [1,2,3,4,5,6,7]
Output: [1,#,2,3,#,4,5,6,7,#]
Explanation: Given the above perfect binary tree (Figure A), your function should populate each next pointer to point to its next right node, just like in Figure B. The serialized output is in level order as connected by the next pointers, with ‘#’ signifying the end of each level.
time: O(n), we visit all the nodes
space: O(n) recursive stack (constant otherwise)
class Solution:
time: O(n), we visit all the nodes
space: constant
time: O(n), we visit all the nodes
space: O(n), the queue
shortest path in binary matrix
Given an n x n binary matrix grid, return the length of the shortest clear path in the matrix. If there is no clear path, return -1.
A clear path in a binary matrix is a path from the top-left cell (i.e., (0, 0)) to the bottom-right cell (i.e., (n - 1, n - 1)) such that:
All the visited cells of the path are 0.
All the adjacent cells of the path are 8-directionally connected (i.e., they are different and they share an edge or a corner).
The length of a clear path is the number of visited cells of this path.
2 solutions do the same thing except 1 modifies the input data. It is important to ask if we can do this as a clarification so the interviewer thinks we are smart. This is bc in a multithreaded program, we cannot modify the input bc another thread might be using it
time: O(n), the visited matrix only allows each cell to be visited once. The inner loop for all the surrounding cells is also size 8, so it is a constant operation
space: O(n), the queue can grow to size very close to N in large inputs, also the visited matrix could be size N
class Solution:
—-def overwrite_input(self, grid):
——–self.grid = grid
——–self.n = len(self.grid)
——–if self.grid[0][0] != 0 or self.grid[self.n - 1][self.n - 1] != 0:
————return -1—-
——–
——–self.directions = [
————(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
——–
——–queue = [(0,0)]
——–self.grid[0][0] = 1
——–
——–while queue:
————row, col = queue.pop(0)
————distance = self.grid[row][col]
————if row == self.n - 1 and col == self.n - 1:
—————-return distance
————else:
—————-for adj_row, adj_col in self.get_valid_neighbors(row, col):
——————–self.grid[adj_row][adj_col] = distance + 1
——————–queue.append((adj_row, adj_col))
——–return -1
—-
—-def get_valid_neighbors(self, row, col):
——–for dr, dc in self.directions:
————new_row = row + dr
————new_col = col + dc
————if not (0 [less than]= new_row [less than]= self.n - 1 and 0 [less than]= new_col [less than]= self.n - 1):
—————-continue
————if self.grid[new_row][new_col] == 0:
—————-yield (new_row, new_col)
Rotting oranges
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.
!!! You can keep track of ones and during bfs, if you have seen all the ones you saw in the initial loop, return answer. Start answer off at 0, increment it 1 each while loop. If this condition is never met, return -1 bc you will not find it.
time: O(N) where N is the size of the matrix
space: O(N) because the queue could take up N space if they are all 2s. In normal cases with lots of 2s, we’d have n/2 but still n.
from collections import deque class Solution: ----def modify_input(self, grid): --------answer = 0 --------y = len(grid) --------x = len(grid[0]) --------ones = 0 --------queue = deque() --------directions = [(1, 0), (-1, 0), (0, 1), (0, -1)] -------- --------for row in range(y): ------------for col in range(x): ----------------if grid[row][col] == 1: --------------------ones += 1 ----------------elif grid[row][col] == 2: --------------------queue.append((row, col))---- -------- --------if ones == 0: ------------return 0 -------- --------while queue: ------------size = len(queue) ------------answer += 1 ------------for _ in range(size): ----------------cr, cc = queue.popleft() ----------------for dr, dc in directions: --------------------nr = cr + dr --------------------nc = cc + dc --------------------if not (y - 1 >= nr >= 0 and x - 1 >= nc >= 0): ------------------------continue --------------------if grid[nr][nc] == 1: ------------------------grid[nr][nc] = 2 ------------------------queue.append((nr, nc)) ------------------------ones -= 1 ------------------------if ones == 0: ----------------------------return answer --------return -1
time: O(N) where N is the size of the matrix
space: O(N) because the queue could take up N space if they are all 2s. In normal cases with lots of 2s, we’d have n/2 but still n.
class Solution:
Min Cost to Connect All Points
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.
time: O(E log E) where E is the number of edges (points ^ 2). In the heap section, we loop through the heap and heappop at each section, adding the logE time
space: O(E) where E is the edges, n^2 in respect to points
import heapq
class Solution:
—-def minCostConnectPoints(self, points):
——–manhattan = lambda p1, p2: abs(p1[0]-p2[0]) + abs(p1[1]-p2[1])
——–n = current = len(points)
——–response = 0
——–weighted_list = []
——–for i in range(n):
————for j in range(i+1, n):
—————-weight = manhattan(points[i], points[j])
—————-weighted_list.append([weight, i, j])
—————-
——–# sorted_weights = sorted(weighted_list, key = lambda x: x[2])
——–heapq.heapify(weighted_list)
——–disjointed_graph = DisjointedGraph(n)
——–
——–while weighted_list:
————w, i, j = heapq.heappop(weighted_list)
————if disjointed_graph.union(i, j):
—————-response += w
—————-current -= 1
————if current == 1:
—————-break
——–return response
——–
class DisjointedGraph:
Complexity analysis to traverse all paths in an undirected graph, a directed graph? In a directed graph?
undirected is (V-1)! time and V^3 space. This is bc to each vertex can have edges to all other graphs, meaning traversing it would be v-1 * v-2, * v-3…. and the space would mean the stack could have v^2 items on the stack, plus the number of paths per vertex, so v^3 (since each vertex on the graph has V paths to all other nodes)
directed is 2^n * n. Adding a node to a directed graph increases the possible edges by 2, so 2^n. Space is 2^n as well bc we need to store those in the stack.
Network delay time
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 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
No good brute force, just different implementations of Dijkstra’s
time: O(E * log(E)) where E is the number of edges, hence the length of the time array. This is because we loop through edges to make the graph, then we go through them again in the heap using heappop
space: O (E + V), the queue can have E length, and the map has V + E length where V are the vertexes (N in this case too)
time: O(V * E), every V could see every E in the worst case since we do not have any seen matrix
space: O(E + V), same reason as above, we can store all the edges in the list at once and the map is E + V
——–return total_distance[0] if total_distance[0] != float(‘inf’) else -1
Cheapest Flights Within K Stops
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.
2 solutions, dijkstras or bellman-ford
time: O( K * E ), we do K+1 iterations bc with K stops and each stop we iterate over each flight
space: O(V) for the vertexes occupied by the bellman-ford prev/current lists
from collections import defaultdict
import heapq
class Solution:
—-def bellman_ford(self, n, flights, src, dst, k):
——–prev = [float(‘inf’) for _ in range(n)]
——–current = [float(‘inf’) for _ in range(n)]
——–
——–prev[src], current[src] = 0, 0
——–
——–for _ in range(k+1):
————for e in flights:
—————-root, to, price = e
—————-current[to] = min(prev[root] + price, current[to])
————prev, current = current, prev
———-# prev = current.copy()
——–
——–return -1 if prev[dst] == float(‘inf’) else prev[dst]
time: O( E log E ) where E is the lenght of edges. LC says V^2 log V, but other dijkstras implementations always say E log E, and that makes sense bc the heap can be as large as E when we heappush onto it
space: O (E + V), where E is the number of flights and V are the vertexes
Path With Minimum Effort
You are a hiker preparing for an upcoming hike. You are given heights, a 2D array of size rows x columns, where heights[row][col] represents the height of cell (row, col). You are situated in the top-left cell, (0, 0), and you hope to travel to the bottom-right cell, (rows-1, columns-1) (i.e., 0-indexed). You can move up, down, left, or right, and you wish to find a route that requires the minimum effort.
A route’s effort is the maximum absolute difference in heights between two consecutive cells of the route.
Return the minimum effort required to travel from the top-left cell to the bottom-right cell.
path finding, we need to think of dijkstras, bellman-ford, BFS and DFS
time: O(rowcol * log(rowcol)), we loop through all the rows and columns, all the rows and columns can be on the list when we heappush
space: O(row * col ), this is for the seen matrix and the heap
import heapq
class Solution:
—-def dijkstra(self, heights):
——–seen = set()
——–y, x = len(heights) - 1, len(heights[0]) - 1
——–# effort, val, row, col
——–heap = [(0, heights[0][0], 0, 0)]
——–deltas = [(0, 1), (0, -1), (1, 0), (-1, 0)]
——–while heap:
————effort, val, row, col = heapq.heappop(heap)
————if row == y and col == x:
—————-return effort
————
————if (row, col) not in seen:
—————-seen.add((row, col))
————else:
—————-continue
—————-
————for dr, dc in deltas:
—————-new_row = row + dr
—————-new_col = col + dc
—————-if y >= new_row >= 0 and x >= new_col >= 0:
——————–height = heights[new_row][new_col]
——————–new_effort = max(effort, abs(val - height))
——————–heapq.heappush(heap, (new_effort, height, new_row, new_col))
time: O(MN * log(MN)), we need to loop through the matrix and then sort it. If we do a heap the heappush will add the log time as well
space: O(M*N), the length of the new objects we create
course scheduler 2
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.
Anytime you hear other things must be completed to continue, think topological sort!
time: O(V + E) where V are the vertexes and E are the edges
space: O(V + E) since we need the adjacency list
from collections import defaultdict, deque
class Solution:
—-def proper_topological(self, numCourses, prerequisites):
——–adj = defaultdict(list)
——–response = []
——–indegree = {}
——–for course, prereq in prerequisites:
————adj[prereq].append(course)
————indegree[course] = indegree.get(course, 0) + 1
——–
——–queue = deque([n for n in range(numCourses) if n not in indegree])
——–
——–while queue:
————current = queue.popleft()
————response.append(current)
————for child in adj[current]:
—————-indegree[child] -= 1
—————-if indegree[child] == 0:
——————–queue.append(child)
—————-
——–return response if len(response) == numCourses else []
There is a new alien language that uses the English alphabet. However, the order among the letters is unknown to you.
You are given a list of strings words from the alien language’s dictionary, where the strings in words are sorted lexicographically by the rules of this new language.
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 is no solution, return “”. If there are multiple solutions, return any of them.
A string s is lexicographically smaller than a string t if at the first letter where they differ, the letter in s comes before the letter in t in the alien language. If the first min(s.length, t.length) letters are the same, then s is smaller if and only if s.length < t.length.
time: O(W) where W is the length of the words in the initial word list. Each word can only give us 1 edge each, so if N is the total words, we can get N-1 edges. Also, if there are U unique characters, we can get U^2 edges assuming the input gives words like “AAA, AAB, AAC….” so each character has U edges, so that is U^2. BUT in both those cases, W is larger bc W > N as long as N has more than 1 character, and there are only U characters so W > U. comparing N to U, we know that if there are N = 100000, U=26, then we only have 26^2 edges, conversely, if we only have N=10, U=26, we only have 9 edges, so it is the min(N, U^2)
space: O(min(U^2, N). This is the same reason as the time complexity reasoning for U and N
from collections import defaultdict, deque
class Solution:
—-def alienOrder(self, words):
——–graph = defaultdict(set)
——–indegree = {}
——–for word in words:
————for s in word:
—————-indegree[s] = 0
——–# indegree = Counter({s:0 for word in words for s in word})
—————-
——–response = []
——–for first_word, second_word in zip(words, words[1:]):
————for x, y in zip(first_word, second_word):
—————-if x != y:
——————–if y not in graph[x]:
————————graph[x].add(y)
————————indegree[y] = indegree.get(y, 0) + 1
——————–break
————else:
# this test case makes the problem statement invalid, it fails on [‘abc’, ‘ab’], but ab < abc, so it should be the reverse if the problem statement was correct.
—————-if len(second_word) < len(first_word):
——————–return “”
——————–
————
——–queue = deque([s for s in indegree.keys() if indegree[s] == 0])
——–while queue:
————current = queue.popleft()
————for child in graph[current]:
—————-indegree[child] -= 1
—————-if indegree[child] == 0:
——————–queue.append(child)
————response.append(current)
——–
——–return ““.join(response) if len(response) == len(indegree.keys()) else “”
Minimum height trees
A tree is an undirected graph in which any two vertices are connected by exactly one path. In other words, any connected graph without simple cycles is a tree.
Given a tree of n nodes labelled from 0 to n - 1, and an array of n - 1 edges where edges[i] = [ai, bi] indicates that there is an undirected edge between the two nodes ai and bi in the tree, you can choose any node of the tree as the root. When you select a node x as the root, the result tree has height h. Among all possible rooted trees, those with minimum height (i.e. min(h)) are called minimum height trees (MHTs).
Return a list of all MHTs’ root labels. You can return the answer in any order.
The height of a rooted tree is the number of edges on the longest downward path between the root and a leaf.
brute force is create the adjacency graph, then for range(n) make a heap with root as the starting node in the heap, perform BFS, get the min height! O(V(V+E)) bc we loop through each V and E, V times where V is N, space is V+E
time: O(V + E) since we have a valid tree, V = n and E = n - 1. we create the adj_list and iterate over n-2 nodes
space: O(V+E) for the adj_list and the queue
from collections import defaultdict, deque class Solution: ----def findMinHeightTrees(self, n, edges): --------if n == 1: ------------return [0] --------graph = defaultdict(set) --------for x, y, in edges: ------------graph[x].add(y) ------------graph[y].add(x) -------- --------leaves = deque() --------for i in range(n): ------------if len(graph[i]) == 1: ----------------leaves.append(i) ---------------- --------nodes = n --------while nodes > 2: ------------size = len(leaves) ------------for i in range(size): ----------------nodes -= 1 ----------------current = leaves.popleft() ----------------child = graph[current].pop() ----------------graph[child].remove(current) ----------------if len(graph[child]) == 1: --------------------leaves.append(child) --------return leaves