Skip to content

[seungriyou] Week 05 Solutions #1387

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 3, 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
48 changes: 48 additions & 0 deletions best-time-to-buy-and-sell-stock/seungriyou.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# https://leetcode.com/problems/best-time-to-buy-and-sell-stock/

from typing import List

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

[Approach]
prices를 순회하면서 다음을 트래킹하면 된다.
(1) 지금까지의 최소 가격인 min_price 트래킹: min(min_price, price)
(2) 지금까지의 최대 수익인 max_profit 트래킹: max(max_profit, price - min_price)
"""
max_profit, min_price = 0, prices[0]

for price in prices:
min_price = min(min_price, price)
max_profit = max(max_profit, price - min_price)

return max_profit

def maxProfit(self, prices: List[int]) -> int:
"""
[Complexity]
- TC: O(n)
- SC: O(1)

[Approach]
two-pointer(buy, sell)로도 접근 가능하다.
두 개의 pointer buy와 sell을 0, 1 인덱스에서 시작한 후, sell을 움직여가며 curr_profit = prices[sell] - prices[buy]를 구한다.
그리고 curr_profit이 0보다 크면 max_profit을 트래킹하고, 그렇지 않으면 buy를 sell 위치로 당긴다.
(curr_profit이 0보다 크지 않다는 것은, buy 시점의 price 이하인 price가 sell 시점에서 발견되었다는 뜻이다!)
"""
buy, sell = 0, 1
max_profit = 0

while sell < len(prices):
curr_profit = prices[sell] - prices[buy]
if curr_profit > 0:
max_profit = max(max_profit, curr_profit)
else:
buy = sell
sell += 1

return max_profit
45 changes: 45 additions & 0 deletions encode-and-decode-strings/seungriyou.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# https://leetcode.com/problems/encode-and-decode-strings/

from typing import List

class Codec:
def encode(self, strs: List[str]) -> str:
"""Encodes a list of strings to a single string.

[Approach]
(length + delimiter + string)의 형태로 str를 구성하면 length 만큼 포인터를 건너뛰며 확인할 수 있게 된다.
delimiter는 숫자(length) 바로 뒤에 처음으로 나오는 문자여야 하므로, 숫자가 아닌 값으로 해야 한다. (나는 "."으로)
"""
# length + delimiter + string
return f"".join(str(len(s)) + "." + s for s in strs)

def decode(self, s: str) -> List[str]:
"""Decodes a single string to a list of strings.
"""
strs = []

start = 0
while start < len(s):
# 1. start 이후에 나오는 첫 delimiter 위치 찾기
# for delim in range(start, len(s)):
# if s[delim] == ".":
# break
delim = s.find(".", start)

# 2. 보고 있는 str의 length 구하기
length = int(s[start:delim])

# 3. 보고 있는 str의 다음 위치 구하기
end = delim + 1 + length

# 4. 현재 str 모으기
strs.append(s[delim + 1:end])

# 5. start를 end로 이동
start = end

return strs

# Your Codec object will be instantiated and called as such:
# codec = Codec()
# codec.decode(codec.encode(strs))
24 changes: 24 additions & 0 deletions group-anagrams/seungriyou.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# https://leetcode.com/problems/group-anagrams/

from typing import List

class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
"""
[Approach]
- TC: O(n * klogk) (n = len(strs), k = max length of an element)
- SC: O(n * k)

[Complexity]
(1) strs의 원소들을 정렬해서 (2) hash table의 key 값으로 사용하면
매 단계마다 O(1)에 anagram을 찾을 수 있다.
이때, key 값으로는 list는 사용할 수 없고, string이나 tuple을 사용해야 한다.
"""
from collections import defaultdict

anagrams = defaultdict(list)

for s in strs:
anagrams["".join(sorted(s))].append(s)

return list(anagrams.values())
48 changes: 48 additions & 0 deletions implement-trie-prefix-tree/seungriyou.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# https://leetcode.com/problems/implement-trie-prefix-tree/

from collections import defaultdict


class TrieNode:
def __init__(self):
self.is_word = False
self.children = defaultdict(TrieNode)


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

def insert(self, word: str) -> None:
curr = self.root

for w in word:
curr = curr.children[w]

curr.is_word = True

def search(self, word: str) -> bool:
curr = self.root

for w in word:
if w not in curr.children:
return False
curr = curr.children[w]

return curr.is_word

def startsWith(self, prefix: str) -> bool:
curr = self.root

for p in prefix:
if p not in curr.children:
return False
curr = curr.children[p]

return True

# Your Trie object will be instantiated and called as such:
# obj = Trie()
# obj.insert(word)
# param_2 = obj.search(word)
# param_3 = obj.startsWith(prefix)
131 changes: 131 additions & 0 deletions word-break/seungriyou.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# https://leetcode.com/problems/word-break/

from typing import List

class Solution:
def wordBreak_bfs(self, s: str, wordDict: List[str]) -> bool:
"""
[Complexity]
- TC: O(n^3 + m * k) (n = len(s), m = len(wordDict), k = avg(len(word) for word in wordDict))
- O(n^3): (1) s[start:end] slicing * (2) end * (3) start
- O(m * k): set(wordDict)
- SC: O(n + m * k)
- O(n): seen, q
- O(m * k): set(wordDict)

[Approach]
BFS로 접근할 수 있다.
이때, queue는 확인할 substring의 시작 index를 기록하면 되며, seen을 이용하여 각 start 당 한 번씩만 확인한다.
"""
from collections import deque

n, words = len(s), set(wordDict)

q = deque([0]) # 확인할 substring의 시작 index
seen = set() # 중복 X

while q:
start = q.popleft()

# base condition
if start == n:
return True

# string slicing에서 사용할 끝 index를 순회하며 확인
for end in range(start + 1, n + 1):
# (1) 방문하지 않았으며 (2) wordDict에 현재 보고 있는 substring이 있는 경우
if end not in seen and s[start:end] in words:
q.append(end)
seen.add(end)

return False

def wordBreak_dp(self, s: str, wordDict: List[str]) -> bool:
"""
[Complexity]
- TC: O(n^3 + m * k)
- O(n^3): (1) s[start:end] slicing * (2) end * (3) start
- O(m * k): set(wordDict)
- SC: O(n + m * k)
- O(n): dp
- O(m * k): set(wordDict)

[Approach]
DP로 접근할 수 있다.
이때, dp[i] = s[i:]가 wordDict의 원소만으로 구성될 수 있는지 여부이다.
따라서 핵심은 뒤에서부터 확인하는 것이다!
"""

n, words = len(s), set(wordDict)

# dp[i] = s[i:]가 wordDict의 원소만으로 구성될 수 있는지 여부
dp = [False] * (n + 1)
dp[n] = True

for start in range(n - 1, -1, -1):
for end in range(start + 1, n + 1):
# (1) s[end:]가 wordDict 원소로 구성 가능하고 (2) wordDict에 현재 보고 있는 substring이 있는 경우
if dp[end] and s[start:end] in words:
dp[start] = True
break

return dp[0]

def wordBreak(self, s: str, wordDict: List[str]) -> bool:
"""
[Complexity]
- TC: O(n^2 + m * k)
- O(n^2): 루프 수행 (slicing 제거)
- O(m * k): trie 생성
- SC: O(n + m * k)
- O(n): dp list
- O(m * k): trie

[Approach]
trie를 이용하면, dp에서 매 루프마다 s[start:end] slicing으로 O(n)이 소요되던 것을 O(1)로 최적화 할 수 있다.
"""
from collections import defaultdict

class TrieNode:
def __init__(self):
self.is_word = False
self.children = defaultdict(TrieNode)

def add_word(self, word):
curr = self

for w in word:
curr = curr.children[w]

curr.is_word = True

# 1. trie 구성하기
root = TrieNode()
for word in wordDict:
root.add_word(word)

# 2. dp 준비하기
n, words = len(s), set(wordDict)
# dp[i] = s[i:]가 wordDict의 원소만으로 구성될 수 있는지 여부
dp = [False] * (n + 1)
dp[n] = True

# 3. trie를 이용하여 최적화된 dp 수행
for start in range(n - 1, -1, -1):
curr = root
for end in range(start + 1, n + 1): # -- trie 사용으로 각 단계 O(n) -> O(1)
w = s[end - 1]

# 현재 보고 있는 substring의 마지막 문자가 curr.children에 없다면 중단
if w not in curr.children:
break

# 있다면 타고 내려가기
curr = curr.children[w]

# (1) s[end:]가 wordDict 원소로 구성 가능하고 (2) wordDict의 word가 발견되었다면 dp 리스트에 기록 후 중단
if dp[end] and curr.is_word:
dp[start] = True
break

return dp[0]