Skip to content

[seungriyou] Week 08 Solutions #1504

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
82 changes: 82 additions & 0 deletions clone-graph/seungriyou.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# https://leetcode.com/problems/clone-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_dfs(self, node: Optional['Node']) -> Optional['Node']:
"""
[Complexity]
- TC: O(n + m) (n = node 개수, m = edge 개수)
- SC: O(n) (call stack & cache)

[Approach]
{val: node} 형태의 dict를 캐시처럼 활용하여, DFS로 접근한다.
각 node를 복제해야 하므로, 원본 node의 복제본을 O(1)에 lookup 하려면 dict에 key를 Node.val 값으로 설정해야 한다.
"""
copies = dict() # {val: node}

if not node:
return None

def copy_node(curr):
# base condition: 이미 copies에 존재하는 node라면 반환
if curr.val in copies:
return copies[curr.val]

# 현재 노드 복사
copied_node = Node(val=curr.val)

# copies에 추가
copies[curr.val] = copied_node

# neighbors를 순회하며, neighbors에 copy를 만들어 추가
for ngbr in curr.neighbors:
copied_node.neighbors.append(copy_node(ngbr))

# 복사한 노드 반환
return copied_node

return copy_node(node)

def cloneGraph(self, node: Optional['Node']) -> Optional['Node']:
"""
[Complexity]
- TC: O(n + m)
- SC: O(n) (queue & cache)

[Approach]
{val: node} 형태의 dict를 캐시처럼 활용하여, BFS로 접근한다.
"""
from collections import deque

copies = dict() # {val: node}

if not node:
return None

q = deque([node])
copied_root_node = Node(val=node.val)
copies[node.val] = copied_root_node

while q:
curr = q.popleft()

for ngbr in curr.neighbors:
# ngbr.val이 캐시에 존재하지 않으면, copy 후 캐시에 저장
if ngbr.val not in copies:
# ngbr의 복사본 생성 및 캐시에 저장
copies[ngbr.val] = Node(val=ngbr.val)

# ngbr은 아직 방문되지 않았으므로, q에 추가
q.append(ngbr)

# curr 복사본의 neighbors에 ngbr 복사본 추가
copies[curr.val].neighbors.append(copies[ngbr.val])

return copied_root_node
53 changes: 53 additions & 0 deletions longest-common-subsequence/seungriyou.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# https://leetcode.com/problems/longest-common-subsequence/

class Solution:
def longestCommonSubsequence_2d(self, text1: str, text2: str) -> int:
"""
[Complexity]
- TC: O(m * n)
- SC: O(m * n)

[Approach]
dp[i][j] = text1[:i]와 text2[:j]의 longest common subsequence의 길이
= (text1[i - 1] == text2[j - 1] 라면) dp[i - 1][j - 1] + 1 (text1[:i - 1]와 text2[:j - 1]의 longest common subsequence 길이)
(text1[i - 1] != text2[j - 1] 라면) max(dp[i - 1][j], dp[i][j - 1])
"""
m, n = len(text1), len(text2)

dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]

for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

return dp[-1][-1]

def longestCommonSubsequence(self, text1: str, text2: str) -> int:
"""
[Complexity]
- TC: O(m * n)
- SC: O(n)

[Approach]
2D DP에서, 현재 row와 이전 row만 필요로 하기 때문에 1D DP로 space optimize 할 수 있다.
dp[i]를 curr로, dp[i - 1]를 prev로 유지한다. (이때, curr와 prev는 len이 n + 1이다.)
"""
m, n = len(text1), len(text2)

prev = [0 for _ in range(n + 1)]
curr = [0 for _ in range(n + 1)]

for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
curr[j] = prev[j - 1] + 1
else:
curr[j] = max(prev[j], curr[j - 1])

# 다음 row로 넘어가기 위해 바꿔치기
curr, prev = prev, curr

return prev[-1]
39 changes: 39 additions & 0 deletions longest-repeating-character-replacement/seungriyou.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# https://leetcode.com/problems/longest-repeating-character-replacement/

class Solution:
def characterReplacement(self, s: str, k: int) -> int:
"""
[Complexity]
- TC: O(n)
- SC: O(1) (* s consists of only uppercase English letters)

[Approach]
two pointer로 sliding window를 이동해가면서, k번 이내로 replace 했을 때 모두 같은 문자가 될 때의 max_len를 트래킹하면 된다.
이때, 이 substring을 모두 같은 문자로 만들 수 있는 가장 작은 replacement 횟수를 구해 k와 비교해야 하는데,
이 횟수 max_replace는 (substring의 길이 - 가장 빈도가 높은 문자의 등장 횟수) 이다.
max_replace가 k 이하라면 max_len을 업데이트하고, 아니라면 left를 한 칸 전진한다.
"""
from collections import defaultdict

left = max_len = max_freq = 0 # left ~ right: k번 이내로 replace 했을 때, 모두 같은 문자일 때의 max_len 업데이트
cnt = defaultdict(int) # left ~ right sliding window 내에서의 counter

# right 한 칸씩 이동해가며 확인
for right in range(len(s)):
# max_freq 업데이트
cnt[s[right]] += 1
max_freq = max(max_freq, cnt[s[right]])

# 현재 sliding window를 모두 같은 문자로 만들 수 있는 가장 작은 replacement 횟수 구하기
sub_len = right - left + 1
min_replace = sub_len - max_freq

# min_replace가 k 이하이면, max_len 업데이트
if min_replace <= k:
max_len = max(max_len, sub_len)
# 아니라면, left 한 칸 이동
else:
cnt[s[left]] -= 1
left += 1

return max_len
116 changes: 116 additions & 0 deletions palindromic-substrings/seungriyou.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# https://leetcode.com/problems/palindromic-substrings/

class Solution:
def countSubstrings_dp(self, s: str) -> int:
"""
[Complexity]
- TC: O(n^2)
- SC: O(n^2)

[Approach]
palindromic substring인지 여부를 확인하기 위해, 크게 두 가지 경우에 대해 고려해야 한다.
- 길이가 홀수: len == 1인 단위 substring에서 좌우로 확장
- 길이가 짝수: len == 2인 단위 substring에서 좌우로 확장

이때, 기존의 짧은 substring에서 판단한 결과를 계속해서 재사용하므로 다음의 DP table을 사용하는 2D DP로 풀 수 있다.
dp[i][j] = s[i:j + 1]이 palindromic substring인지 여부

따라서 다음과 같이 1) & 2)에서 길이가 각각 1, 2인 단위 substring에 대해 초기화를 먼저 수행하고, 3)에서 len >= 3이상인 substring에 대해 판단한다.
단, **더 짧은 안 쪽 substring에서의 판단 결과를 사용해야 하므로** len을 3부터 n까지 늘려가면서, two pointer i & j로 판단한다.
1) length = 1 : dp[i][i]은 무조건 True
2) length = 2 : dp[i][i + 1]은 s[i] == s[i + 1]이면 True
3) length >= 3 : (j = i + length - 1일 때) dp[i][j]은 (s[i] == s[j]) && (dp[i + 1][j - 1])이면 True
"""

n = len(s)
dp = [[False for _ in range(n)] for _ in range(n)]
res = 0

# 1) length = 1 : dp[i][i]은 무조건 True
for i in range(n):
dp[i][i] = True
res += 1

# 2) length = 2 : dp[i][i + 1]은 s[i] == s[i + 1]이면 True
for i in range(n - 1):
if s[i] == s[i + 1]:
dp[i][i + 1] = True
res += 1

# 3) length >= 3 : (j = i + length - 1일 때) dp[i][j]은 s[i] == s[j]이면서 dp[i][j - 1]이면 True
for length in range(3, n + 1): # length는 3부터 n까지 늘려나가기 (**더 짧은 substring에서의 판단 결과를 사용해야 하므로**)
for i in range(n - length + 1): # i = substring 시작 인덱스
j = i + length - 1 # j = substring 종료 인덱스
if s[i] == s[j] and dp[i + 1][
j - 1]: # (1) i & j가 가리키는 문자가 서로 같고 (2) 안 쪽 substring이 palindrome 이라면 palindromic substring
dp[i][j] = True
res += 1

return res

def countSubstrings_dp2(self, s: str) -> int:
"""
[Complexity]
- TC: O(n^2)
- SC: O(n^2)

[Approach]
length <= 2 조건을 or 연산으로 연결함으로써 1), 2), 3) 케이스를 하나로 줄일 수 있다.
"""

n = len(s)
dp = [[False for _ in range(n)] for _ in range(n)]
res = 0

for length in range(1, n + 1): # length는 1부터 n까지 늘려나가기 (**더 짧은 substring에서의 판단 결과를 사용해야 하므로**)
for i in range(n - length + 1): # i = substring 시작 인덱스
j = i + length - 1 # j = substring 종료 인덱스
if s[i] == s[j] and (length <= 2 or dp[i + 1][j - 1]): # length <= 2 조건을 or 연산으로 연결
dp[i][j] = True
res += 1

return res

def countSubstrings(self, s: str) -> int:
"""
[Complexity]
- TC: O(n^2)
- SC: O(1)

[Approach]
palindromic substring인지 여부를 확인하기 위해, 크게 두 가지 경우에 대해 고려해야 한다.
- 길이가 홀수: len == 1인 단위 substring에서 좌우로 확장
- 길이가 짝수: len == 2인 단위 substring에서 좌우로 확장

따라서 s를 순회하며, 각 문자를 center로 하여 만들 수 있는 길이가 홀수 & 짝수인 palindrome의 개수를 좌우로 확장해가며 카운트한다.
"""

def count_palindrome_from_center(lo, hi):
cnt = 0

# lo와 hi가 범위를 벗어나지 않도록
while (lo >= 0 and hi < len(s)):
# lo와 hi가 가리키는 문자가 다르다면 더이상 확장하며 palindrome을 찾을 수 없음
if s[lo] != s[hi]:
break

# lo와 hi를 좌우로 확장
lo -= 1
hi += 1

# palindrome 개수 증가
cnt += 1

return cnt

res = 0

# center 인덱스 순회
for i in range(len(s)):
# 길이가 홀수: len == 1인 단위 substring에서 좌우로 확장
res += count_palindrome_from_center(i, i)

# 길이가 짝수: len == 2인 단위 substring에서 좌우로 확장
res += count_palindrome_from_center(i, i + 1)

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

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

[Approach]
n의 맨 오른쪽 bit부터 res의 맨 왼쪽에 붙여나가기
"""
res = 0
for i in range(32):
res |= ((n >> i) & 1) << (31 - i)
return res

def reverseBits(self, n: int) -> int:
"""
[Complexity]
- TC: O(16)
- SC: O(1)

[Approach]
n의 바깥쪽에서부터 two pointer 처럼 res에 모으기
"""
res = 0
for i in range(16):
left = (n >> (31 - i)) & 1
right = (n >> i) & 1
res |= (left << i) | (right << (31 - i))
return res