Skip to content

[seungriyou] Week 09 Solutions #1518

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 5 commits into from
May 31, 2025
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
55 changes: 55 additions & 0 deletions linked-list-cycle/seungriyou.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# https://leetcode.com/problems/linked-list-cycle/

from typing import Optional

# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None

class Solution:
def hasCycle_on(self, head: Optional[ListNode]) -> bool:
"""
[Complexity]
- TC: O(n)
- SC: O(n)

[Approach]
hash table로 visited 노드를 기록한다.
"""
curr = head
visited = set()

while curr:
if curr in visited:
return True

visited.add(curr)
curr = curr.next

return False

def hasCycle(self, head: Optional[ListNode]) -> bool:
"""
[Complexity]
- TC: O(n)
- SC: O(1)

[Approach]
Floyd’s Cycle Detection 알고리즘을 사용한다.
linked list에서 slow (1칸씩 전진) & fast (2칸씩 전진) runner를 이용하면,
- slow와 fast가 만난다면 cyclic
- 만나지 않은 채로 fast가 끝에 도달하면 not cyclic
이다.
"""
slow = fast = head

while fast and fast.next:
slow = slow.next
fast = fast.next.next

if slow == fast:
return True

return False
38 changes: 38 additions & 0 deletions maximum-product-subarray/seungriyou.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# https://leetcode.com/problems/maximum-product-subarray/

from typing import List

class Solution:
def maxProduct(self, nums: List[int]) -> int:
"""
[Complexity]
- TC: O(n)
- SC: O(1)

[Approach]
nums를 순회하며 현재 보고 있는 값 num이 포함되는 max subarray product를 구하기 위해서는
max(이전 값과 연속되는 subarray인 경우, 이전 값과 연속되지 않는 subarray인 경우)를 구해야 한다.
- num이 양수일 때: max(이전까지의 max subarray product * num, num)
- num이 음수일 때: max(이전까지의 min subarray product * num, num)
따라서 매 단계마다 이전까지의 max subarray product(= max_p)와 min subarray product(= min_p)를 트래킹해야 한다.
"""
res = max_p = min_p = nums[0]

for i in range(1, len(nums)):
num = nums[i]

# 1. num이 음수라면, max_p와 min_p 바꿔치기
# if num < 0:
# max_p, min_p = min_p, max_p

# 2. max_p, min_p 업데이트
# max_p = max(max_p * num, num)
# min_p = min(min_p * num, num)

# 1 & 2번 대신, 다중 할당으로 처리 가능
max_p, min_p = max(max_p * num, min_p * num, num), min(max_p * num, min_p * num, num)

# 3. res 값 업데이트
res = max(res, max_p)

return res
114 changes: 114 additions & 0 deletions minimum-window-substring/seungriyou.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# https://leetcode.com/problems/minimum-window-substring/

class Solution:
def minWindow1(self, s: str, t: str) -> str:
"""
[Complexity]
- TC: O(m * k + n) (k = is_included() = len(set(t)))
- SC: O(k) (res 제외)

[Approach]
다음과 같은 counter를 유지하며, two pointer로 window를 이동하며 min window substring을 트래킹한다.
- 생성 시) t의 모든 문자에 대해 ++
- pointer 이동 시) window에 포함되는 문자 중 t에 속하는 문자에 대해 --
"""
from collections import Counter

# early stop
m, n = len(s), len(t)
if m < n:
return ""

# t에 대한 counter 생성
counter = Counter(t)

# t의 모든 문자가 window에 포함되는지 확인하는 함수
def is_included():
# counter의 모든 값이 0 이하이면, t의 모든 문자가 window에 포함되는 것 (중복 포함)
return all(c <= 0 for c in counter.values())

lo, min_len = 0, 1e6
res = ""

for hi in range(m):
# 현재 window에 포함된 문자를 counter에 반영
if s[hi] in counter:
counter[s[hi]] -= 1

# t의 모든 문자가 window에 포함되어있다면(= counter의 모든 값이 <= 0이면),
# counter 값이 음수 ~ 0이 될 때까지 lo 증가시키면서 (min window를 구해야하므로) counter 업데이트
if is_included():
while True:
# window의 첫 번째 문자가 t에 속하는 문자라면
if s[lo] in counter:
# counter에서의 값이 0이면, 더이상 lo를 오른쪽으로 줄일 수 없음
if counter[s[lo]] == 0:
break
# coutner에서의 값이 음수라면, lo를 오른쪽으로 줄일 수 있으므로 counter 반영
else:
counter[s[lo]] += 1
# lo를 오른쪽으로 한 칸 이동
lo += 1

# (1) t의 모든 문자가 window에 포함되어 있고 (2) 현재 window의 length가 min_len 보다 작다면
# window substring 업데이트
if is_included() and (new_len := hi - lo + 1) < min_len:
min_len = new_len
res = s[lo:hi + 1]

return res

def minWindow(self, s: str, t: str) -> str:
"""
[Complexity]
- TC: O(m + n)
- SC: O(k)

[Approach]
1) 위의 풀이에서 is_included()를 실행하는 데에 O(k)이 소요되므로,
(t에 속하는 문자 중, 아직 window에 모두 포함되지 않은 문자 종류 개수)를 트래킹하는 변수 remains를 이용하여 최적화 한다.
즉, remains == 0이라면 is_included()인 것과 동일하다.
2) 반복문 안에서 문자열 슬라이싱으로 res를 업데이트 할 때 추가적인 복잡도가 소요된다.
따라서 min window에 대한 pointer인 min_lo, min_hi만 트래킹하고, return 문에서 문자열 슬라이싱으로 반환한다.
단, min window substring이 존재하지 않는다면 결과가 빈 문자열이 될 수 있으므로, min_lo ~ min_hi - 1 범위를 반환하도록 한다.
"""
from collections import Counter

# early stop
m, n = len(s), len(t)
if m < n:
return ""

# t에 대한 counter 생성
counter = Counter(t)

# counter의 값을 모두 확인하는 is_include() 최적화
remains = len(counter) # t에 속하는 문자 중, 아직 window에 모두 포함되지 않은 문자 종류 개수
lo = min_lo = min_hi = 0
min_len = m + 1

for hi in range(m):
# 현재 window에 포함된 문자를 counter에 반영
if s[hi] in counter:
counter[s[hi]] -= 1
# 현재 window에 해당 문자가 t에 존재하는 개수만큼 들어와있다면, remains--
if counter[s[hi]] == 0:
remains -= 1

# t의 모든 문자가 window에 포함되어있는 동안 lo 이동
while not remains:
# 최소 길이 window substring 갱신
if (new_len := hi - lo + 1) < min_len:
min_len, min_lo, min_hi = new_len, lo, hi + 1

# lo 이동 전, counter 업데이트
if s[lo] in counter:
counter[s[lo]] += 1
# counter의 값이 0 초과가 된다면, 더이상 window에 해당 문자가 모두 들어있지 않다는 것이므로 remains++
if counter[s[lo]] > 0:
remains += 1

# lo를 오른쪽으로 한 칸 이동
lo += 1

return s[min_lo:min_hi]
97 changes: 97 additions & 0 deletions pacific-atlantic-water-flow/seungriyou.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# https://leetcode.com/problems/pacific-atlantic-water-flow/

from typing import List

class Solution:
def pacificAtlantic_bfs(self, heights: List[List[int]]) -> List[List[int]]:
"""
[Complexity]
- TC: O(m * n) (모든 칸은 최대 한 번씩 방문)
- SC: O(m * n) (queue)

[Approach]
거꾸로 각 바다에 맞닿아있는 edges에서부터 시작해서, BFS로 height 값이 gte인 칸(-> 바다에서부터 거꾸로 확인하므로)을 방문하며 기록한다.
pacific & atlantic 쪽 edges에서 시작해서 방문한 칸들의 교집합을 구하면 된다.
"""
from collections import deque

m, n = len(heights), len(heights[0])
dr, dc = [-1, 1, 0, 0], [0, 0, -1, 1]

def bfs(start_cells):
q = deque(start_cells)
visited = set(start_cells)

while q:
r, c = q.popleft()

for i in range(4):
nr, nc = r + dr[i], c + dc[i]

if (
0 <= nr < m and 0 <= nc < n # valid한 범위 확인
and (nr, nc) not in visited # 방문 여부 확인
and heights[nr][nc] >= heights[r][c] # height 값이 현재 칸보다 gte인지 확인 (** 거꾸로 확인하므로)
):
q.append((nr, nc))
visited.add((nr, nc))

return visited

# pacific & atlantic 쪽 edges 모으기
edges_p, edges_a = [], []
for r in range(m):
edges_p.append((r, 0))
edges_a.append((r, n - 1))
for c in range(n):
edges_p.append((0, c))
edges_a.append((m - 1, c))

# edges로부터 BFS로 방문
visited_p = bfs(edges_p)
visited_a = bfs(edges_a)

# pacific과 atlantic의 교집합 반환
return [list(cell) for cell in visited_p & visited_a]

def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:
"""
[Complexity]
- TC: O(m * n) (모든 칸은 최대 한 번씩 방문)
- SC: O(m * n) (call stack)

[Approach]
BFS 풀이를 DFS로도 접근 가능하다.
"""
m, n = len(heights), len(heights[0])
dr, dc = [-1, 1, 0, 0], [0, 0, -1, 1]
visited_p, visited_a = set(), set()

def dfs(r, c, visited):
# base condition
if (r, c) in visited: # 방문 여부 확인
return

# 방문 처리
visited.add((r, c))

# recur
for i in range(4):
nr, nc = r + dr[i], c + dc[i]

if (
0 <= nr < m and 0 <= nc < n # valid한 범위 확인
and heights[nr][nc] >= heights[r][c] # height 값이 현재 칸보다 gte인지 확인 (** 거꾸로 확인하므로)
):
dfs(nr, nc, visited)

# edges로부터 DFS로 방문
for r in range(m):
dfs(r, 0, visited_p)
dfs(r, n - 1, visited_a)
for c in range(n):
dfs(0, c, visited_p)
dfs(m - 1, c, visited_a)

# pacific과 atlantic의 교집합 반환
return [list(cell) for cell in visited_p & visited_a]
43 changes: 43 additions & 0 deletions sum-of-two-integers/seungriyou.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# https://leetcode.com/problems/sum-of-two-integers/

class Solution:
def getSum(self, a: int, b: int) -> int:
"""
[Complexity]
- TC: O(1)
- SC: O(1)

[Approach]
덧셈 구현 시 주요한 요소는 carry와 value 값이다.
예를 들어 이진수 11과 101의 덧셈을 생각해보면, 각 자리에서 발생하는 carry와 value는 다음과 같다.
a = 0 1 1
b = 1 0 1
---------------
value) 1 1 0 (= a ^ b)
carry) 0 0 1 (= a & b)
이때, 어떤 자리에서 발생한 carry는 한 칸 left shift 되어 다시 덧셈을 수행하듯 적용되어야 함에 주의한다.
value) 1 1 0
carry) 0 0 1 0 (<<= 1)

이 과정을 carry가 0이 될 때까지 반복해나간다.
value) 0 1 0 0 (= a ^ b)
carry) 0 1 0 0 (= (a & b) << 1)

value) 0 0 0 0 (= a ^ b)
carry) 1 0 0 0 (= (a & b) << 1)

value) 1 0 0 0 (= a ^ b) -> RESULT
carry) 0 0 0 0 (= (a & b) << 1) -> END

하지만 파이썬에서는 int를 32bit 보다 큰 메모리에 저장하므로 음수가 들어올 경우에 대비해 다음의 작업을 추가로 수행해야 한다.
- 음수가 들어온다면 carry가 32bit를 넘어서도 계속 더해질 것이므로, while 문 조건식에의 carry에 32bit masking을 수행한다.
- (b & mask) == 0이지만 b > 0인 경우에는 a의 32bit 범위 밖에 overflow가 존재하는 것이므로, 결과 값에도 32bit masking을 수행한다.
"""

mask = 0xffffffff # 32bit masking

# carry가 0이 될 떄까지 반복 (음수가 들어오는 경우, 32bit masking 후 확인)
while (b & mask) != 0:
a, b = a ^ b, (a & b) << 1 # value, carry

return (a & mask) if b > 0 else a # 음수 (overflow) 처리