Skip to content

[bhyun-kim, 김병현] Week 9 Solutions #150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions clone-graph/bhyun-kim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
133. Clone Graph
https://leetcode.com/problems/clone-graph/

Solution:
To clone a graph, we can use a depth-first search (DFS) to explore all nodes and their neighbors.
We can create a helper function that takes a node and returns its clone.

- We can use a dictionary to map old nodes to new nodes.
- We can create a helper function to clone a node and its neighbors.
- If the node has already been cloned, we return the clone.
- Otherwise, we create a new clone and add it to the dictionary.
- We clone all neighbors of the node recursively.
- We return the clone.
- We start the DFS from the given node and return the clone.

Time complexity: O(n+m)
- n is the number of nodes in the graph.
- m is the number of edges in the graph.
- We explore all nodes and edges once.

Space complexity: O(n)
- We use a dictionary to keep track of the mapping between old nodes and new nodes.
- The maximum depth of the recursive call stack is the number of nodes in the graph.
"""


# 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

old_to_new = {}

def dfs(node):
if node in old_to_new:
return old_to_new[node]

clone = Node(node.val)
old_to_new[node] = clone

for neighbor in node.neighbors:
clone.neighbors.append(dfs(neighbor))

return clone

return dfs(node)
63 changes: 63 additions & 0 deletions course-schedule/bhyun-kim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
207. Course Schedule
https://leetcode.com/problems/course-schedule/

Solution:
If there is a cycle in the graph, it is impossible to finish all courses.
We can detect a cycle by using DFS and keeping track of the state of each node.

- We can create an adjacency list to represent the graph.
- We can use a state array to keep track of the state of each node.
- 0: unvisited, 1: visiting, 2: visited
- We can create a helper function to check if there is a cycle starting from a node.
- If the node is being visited, we have a cycle.
- If the node has been visited, there is no cycle.
- If not, we mark the node as visiting and explore its neighbors.
- After exploring all neighbors, we mark the node as visited.
- We can iterate through all nodes and check for cycles.
- If we find a cycle, we return False.
- If no cycle is found, we return True.

Time complexity: O(m + n)
- m is the number of prerequisites.
- n is the number of courses.
- We explore all prerequisites and courses once.

Space complexity: O(m + n)
- We use an adjacency list to represent the graph.
- We use a state array to keep track of the state of each node.
"""

from collections import defaultdict
from typing import List


class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
adj_list = defaultdict(list)
for dest, src in prerequisites:
adj_list[src].append(dest)

# State: 0 = unvisited, 1 = visiting, 2 = visited
state = [0] * numCourses

def has_cycle(v):
if state[v] == 1: # Node is being visited, so we have a cycle
return True
if state[v] == 2: # Node has been visited, no cycle here
return False

state[v] = 1
for neighbor in adj_list[v]:
if has_cycle(neighbor):
return True

state[v] = 2
return False

for course in range(numCourses):
if state[course] == 0:
if has_cycle(course):
return False

return True
66 changes: 66 additions & 0 deletions design-add-and-search-words-data-structure/bhyun-kim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""
211. Design Add and Search Words Data Structure
https://leetcode.com/problems/design-add-and-search-words-data-structure/

Solution:
To solve this problem, we can use a trie data structure to store the words.
We can create a TrieNode class to represent each node in the trie.

In addWord, we can iterate through each character in the word and create nodes as needed.
We mark the end of the word by setting is_end_of_word to True.

In search, we can use a depth-first search (DFS) to explore all possible paths.
If we encounter a '.', we explore all children of the current node.
If we find a mismatch or reach the end of the word, we return False.


Time complexity:
- addWord: O(m)
- m is the length of the word.
- We iterate through each character in the word once.
- search: O(n * 26^l)
- n is the number of nodes in the trie.
- l is the length of the word.
- We explore all possible paths in the trie.
- The worst-case time complexity is O(n * 26^l) when all nodes have 26 children.

Space complexity: O(n)
- n is the number of nodes in the trie.
- We use a trie data structure to store the words.
"""


class TrieNode:
def __init__(self):
self.children = {}
self.is_end_of_word = False


class WordDictionary:
def __init__(self):
self.root = TrieNode()

def addWord(self, word):
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end_of_word = True

def search(self, word):
def dfs(j, node):
for i in range(j, len(word)):
char = word[i]
if char == ".":
for child in node.children.values():
if dfs(i + 1, child):
return True
return False
else:
if char not in node.children:
return False
node = node.children[char]
return node.is_end_of_word

return dfs(0, self.root)
52 changes: 52 additions & 0 deletions number-of-islands/bhyun-kim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""
200. Number of Islands
https://leetcode.com/problems/number-of-islands/

Solution:
To solve this problem, we can use a depth-first search (DFS) to explore all connected land cells.
We can create a helper function that takes the current cell and marks it as visited.

- We can iterate through all cells in the grid.
- If the current cell is land, we explore all connected land cells using DFS.
- We mark all connected land cells as visited.
- We increment the island count by 1.

Time complexity: O(m x n)
- m and n are the dimensions of the grid.
- We explore all cells in the grid once.

Space complexity: O(m x n)
- We use a recursive call stack to explore all connected land cells.
- The maximum depth of the call stack is the number of cells in the grid.

"""

from typing import List


class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
if not grid:
return 0

m, n = len(grid), len(grid[0])

island_count = 0

def dfs(i, j):
if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] != "1":
return
grid[i][j] = "#"

dfs(i - 1, j)
dfs(i + 1, j)
dfs(i, j - 1)
dfs(i, j + 1)

for i in range(m):
for j in range(n):
if grid[i][j] == "1":
dfs(i, j)
island_count += 1

return island_count
57 changes: 57 additions & 0 deletions pacific-atlantic-water-flow/bhyun-kim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
417. Pacific Atlantic Water Flow
https://leetcode.com/problems/pacific-atlantic-water-flow/

Solution:
To solve this problem, we can use depth-first search (DFS) to explore all possible paths starting from each cell.
We can create a helper function that takes the current cell and marks it as reachable.

- We can create two sets to store the cells that are reachable from the Pacific and Atlantic oceans.
- We can start DFS from the cells on the borders of the Pacific and Atlantic oceans.
- We can find the intersection of the two sets to get the cells that are reachable from both oceans.


Time complexity: O(m x n)
- m and n are the dimensions of the grid.
- We explore all cells in the grid once.

Space complexity: O(m x n)
- We use two sets to store the reachable cells from the Pacific and Atlantic oceans.
- The maximum size of the sets is the number of cells in the grid.
"""


from typing import List


class Solution:
def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:
if not heights or not heights[0]:
return []

def dfs(matrix, reachable, x, y):
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
reachable.add((x, y))
for dx, dy in directions:
nx, ny = x + dx, y + dy
if (
0 <= nx < m
and 0 <= ny < n
and (nx, ny) not in reachable
and matrix[nx][ny] >= matrix[x][y]
):
dfs(matrix, reachable, nx, ny)

m, n = len(heights), len(heights[0])
pacific_reachable = set()
atlantic_reachable = set()

for i in range(m):
dfs(heights, pacific_reachable, i, 0)
dfs(heights, atlantic_reachable, i, n - 1)

for j in range(n):
dfs(heights, pacific_reachable, 0, j)
dfs(heights, atlantic_reachable, m - 1, j)

return list(pacific_reachable & atlantic_reachable)