-
-
Notifications
You must be signed in to change notification settings - Fork 195
[haklee] week 5 #441
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
[haklee] week 5 #441
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
"""TC: O(n^2), SC: O(n^2) | ||
|
||
아이디어: | ||
- 합이 0(즉, 고정값)이 되는 세 수를 찾는 것이 문제. | ||
- 세 수 중 하나가 고정되어 있다면, 그리고 숫자들이 들어있는 리스트가 정렬되어 있다면 투 포인터 사용 가능. | ||
- 투 포인터 테크닉은 검색하면 많이 나오므로 아주 자세한 설명은 생략하겠다. | ||
- 이 문제에서는 리스트의 가장 작은 값과 가장 큰 값에 포인터를 둘 것이다. | ||
- 그리고 이 두 값의 합이 | ||
- 원하는 결과보다 작으면 작은 값의 포인터를 큰 쪽으로 옮기고, | ||
- 원하는 결과보다 크면 큰 값의 포인터를 작은 쪽으로 옮긴다. | ||
- 이 과정을 반복하면서 원하는 쌍을 찾는 것이 관건. | ||
- 고정된 숫자를 정렬된 리스트의 가장 작은 값부터 큰 값으로 하나씩 옮겨가면 중복 없이 탐색이 가능하다. | ||
- 이때 투 포인터를 쓸 구간은 고정된 숫자 뒤에 오는 숫자들로 둔다. | ||
- 코드를 보는 것이 이해가 더 빠를 것이다.. | ||
|
||
SC: | ||
- 자세한 설명은 TC 분석에서 확인 가능. | ||
- 종합하면 O(n^2). | ||
|
||
TC: | ||
- nums를 sort하는 과정에서 O(n * log(n)) | ||
- 정렬된 nums를 모두 순회. | ||
- 그리고 각 순회마다 n-1, n-2, ..., 2 크기의 구간에서 투 포인터 사용. | ||
- 투 포인터를 사용할때 단순 사칙연산 및 비교연산만 사용하므로 O(1). | ||
- 투 포인터 사용시 매 계산마다 포인터 사이의 거리가 1씩 줄어든다(s가 올라가든 e가 내려가든). | ||
- (SC) 매 계산마다 최대 한 번 solution을 추가하는 연산을 한다. | ||
- 그러므로 각 순회마다 C * (n-1), C * (n-2), ..., C * 1의 시간이 들어감. | ||
- (SC) 비슷하게, 매 순회마다 위와 같은 꼴로 solution 개수가 더해질 수 있다. | ||
- 종합하면 O(n^2) | ||
- 총 O(n^2) | ||
""" | ||
|
||
from collections import Counter | ||
|
||
|
||
class Solution: | ||
def threeSum(self, nums: List[int]) -> List[List[int]]: | ||
# 커팅. 어차피 세 쌍의 숫자에 등장할 수 있는 같은 숫자 개수가 최대 3개이므로, | ||
# 처음 주어진 nums에 같은 숫자가 네 번 이상 등장하면 세 번만 나오도록 바꿔준다. | ||
# 이 처리를 하면 같은 숫자가 많이 반복되는 케이스에서 시간 개선이 있을 수 있다. | ||
# Counter 쓰는 데에 O(n), 새로 tmp_nums 리스트를 만드는 데에 O(n)의 시간이 들어가므로 | ||
# 최종적인 시간 복잡도에 영향을 주지는 않는다. | ||
tmp_nums = [] | ||
for k, v in Counter(nums).items(): | ||
tmp_nums += [k] * min(v, 3) | ||
Comment on lines
+38
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 전처리 배워갑니다 👍 |
||
|
||
# 여기부터가 주된 구현. | ||
sorted_nums = sorted(tmp_nums) # TC: O(n * log(n)) | ||
nums_len = len(tmp_nums) | ||
|
||
sol = set() | ||
for i in range(nums_len): # TC: O(n) | ||
if i > 0 and sorted_nums[i] == sorted_nums[i - 1]: | ||
# 커팅. 고정 값이 이미 한 번 사용되었던 값이면 스킵해도 괜찮다. | ||
continue | ||
s, e = i + 1, nums_len - 1 | ||
while s < e: | ||
# 이 while문 전체에서 TC O(n). | ||
v = sorted_nums[s] + sorted_nums[e] | ||
if v == -sorted_nums[i]: | ||
# i < s < e 이므로, 이 순서대로 숫자는 정렬된 상태다. | ||
# 즉, 같은 값을 사용한 순서만 다른 쌍을 걱정하지 않아도 된다. | ||
sol.add((sorted_nums[i], sorted_nums[s], sorted_nums[e])) | ||
if v < -sorted_nums[i]: | ||
# s, e의 두 값을 더한 것이 원하는 값보다 작으면, 작은 쪽에 있는 포인터를 | ||
# 더 큰 숫자가 있는 쪽으로 옮기면 된다. | ||
# 여기서도 중복 값 커팅을 하려면 할 수 있겠지만, 이 커팅을 안 하려고 | ||
# 맨 앞에서 같은 숫자들을 미리 최대한 제거해두었다. | ||
s += 1 | ||
else: | ||
# s, e의 두 값을 더한 것이 원하는 값보다 크면, 큰 쪽에 있는 포인터를 | ||
# 더 작은 숫자가 있는 쪽으로 옮기면 된다. | ||
# 여기서도 중복 값 커팅을 하려면 할 수 있겠지만, 이 커팅을 안 하려고 | ||
# 맨 앞에서 같은 숫자들을 미리 최대한 제거해두었다. | ||
e -= 1 | ||
return sol # 타입힌트를 따르지 않아도 제출은 된다... |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
"""TC: O(n), SC: O(1) | ||
|
||
아이디어: | ||
- 특정 시점에서 stock을 팔았을때 최고 수익이 나려면 이전 가격 중 가장 낮은 가격에 stock을 사야 한다. | ||
- 모든 시점에 대해 위의 값을 계산하면 전체 기간 중 최고 수익을 구할 수 있다. | ||
- 이를 위해서 특정 시점 이전까지의 가격 중 가장 싼 가격인 minp값을 관리하고, | ||
- 각 시점의 가격에서 minp값을 빼서 `현재 최고 수익`을 구한 다음에, | ||
- `전체 최고 수익`을 max(`현재 최고 수익`, `전체 최고 수익`)으로 업데이트 한다. | ||
|
||
SC: | ||
- minp, profit(`전체 최고 수익`) 값을 관리한다. O(1). | ||
|
||
TC: | ||
- prices의 가격을 순회하면서 더하기, min, max 연산을 한다. O(n). | ||
""" | ||
|
||
|
||
class Solution: | ||
def maxProfit(self, prices: List[int]) -> int: | ||
minp, profit = prices[0], 0 | ||
for p in prices: | ||
profit = max(profit, p - (minp := min(minp, p))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
return profit |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
"""TC: O(n * l * log(l)), SC: O(n * l) | ||
|
||
전체 문자열 개수 n개, 문자열 최대 길이 l. | ||
|
||
아이디어: | ||
- 모든 문자열에 대해 해당 문자열을 이루는 문자 조합으로 고유한 키를 만드는 것이 주된 아이디어. | ||
- 문자열을 해체해서 sort한 다음 이를 바로 tuple로 만들어서 키로 사용했다. | ||
- list를 리턴하라고 되어있는 것을 `dict_values`로 리턴했지만 문제가 생기지 않아서 그냥 제출했다. | ||
|
||
SC: | ||
- dict 관리. | ||
- 키값 최대 n개, 각각 최고 길이 l. O(n * l). | ||
- 총 아이템 n개, 각각 최고 길이 l. O(n * l). | ||
- 종합하면 O(n * l). | ||
|
||
TC: | ||
- strs에 있는 각 아이템을 sort함. O(l * log(l)) | ||
- 위의 과정을 n번 반복. | ||
- 종합하면 O(n * l * log(l)). | ||
""" | ||
|
||
|
||
class Solution: | ||
def groupAnagrams(self, strs: List[str]) -> List[List[str]]: | ||
d = {} | ||
for s in strs: | ||
# k값 계산이 오른쪽에서 먼저 이루어지는군요?! | ||
d[k] = d.get(k := tuple(sorted(s)), []) + [s] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 파이썬 코드가 함축적이라서 공간 복잡도 분석이 좀 햇갈려서 질문드려요. 좀 풀어서 보면... d[k] = [애너그램1, 애너그램2, ..., 애너그램n] + [새로운 애너그램] 위와 같이 리스트 두 개를 더해서 새로운 리스트를 만들어서 사전에 들어있는 기존 리스트를 덮어쓰도록 구현을 하셨는데요. [애너그램1, 애너그램2, ..., 애너그램n].append(새로운 애너그램)
# 즉, d[k].append(새로운 애너그램) 위와 같이 그냥 사전에 들어있는 기존 리스트에 새로운 애너그램을 추가하도록 구현을 할 수도 있을 것 같아요. 이 두 가지 방식이 메모리 효율 측면에서 유의미한 차이가 있을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
return d.values() | ||
|
||
|
||
"""TC: O(n * l * log(l)), SC: O(n * l) | ||
각 단어를 sort하는 것보다 단어를 이루고 있는 문자를 카운터로 세어서 이 카운터를 키로 쓰는 것이 | ||
시간복잡도에 더 좋을 수도 있다. Counter를 써서 알파벳 개수를 dict로 만든 다음 json.dumps로 str | ||
로 만들어버리자. | ||
|
||
실제 이 솔루션을 제출하면 성능이 별로 좋지 않은데, l값이 작아서 위의 과정을 처리하는 데에 오버헤드가 | ||
오히려 더 붙기 때문을 추정된다. | ||
""" | ||
|
||
from collections import Counter | ||
from json import dumps | ||
|
||
|
||
class Solution: | ||
def groupAnagrams(self, strs: List[str]) -> List[List[str]]: | ||
d = {} | ||
for s in strs: | ||
d[k] = d.get(k := dumps(Counter(s), sort_keys=True), []) + [s] | ||
return d.values() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
""" | ||
단순한 trie 구현이므로 분석은 생략합니다. | ||
""" | ||
|
||
|
||
class Trie: | ||
def __init__(self): | ||
self.next: dict[str, Trie] = {} | ||
self.end: bool = False | ||
|
||
def insert(self, word: str) -> None: | ||
cur = self | ||
|
||
for c in word: | ||
cur.next[c] = cur.next.get(c, Trie()) | ||
cur = cur.next[c] | ||
|
||
cur.end = True | ||
|
||
def search(self, word: str) -> bool: | ||
cur = self | ||
|
||
for c in word: | ||
if c not in cur.next: | ||
return False | ||
cur = cur.next[c] | ||
|
||
return cur.end | ||
|
||
def startsWith(self, prefix: str) -> bool: | ||
cur = self | ||
|
||
for c in prefix: | ||
if c not in cur.next: | ||
return False | ||
cur = cur.next[c] | ||
|
||
return True |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이전 문제의 trie 응용한거 좋네요! 👍 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
"""TC: ?, SC: O(w * l + s^2) | ||
|
||
쪼개고자 하는 단어의 길이 s, wordDict에 들어가는 단어 개수 w, wordDict에 들어가는 단어 최대 길이 l | ||
|
||
아이디어: | ||
- trie를 구현하는 문제에서 만든 클래스를 여기서 한 번 사용해보자. | ||
- 주어진 단어들을 전부 trie에 집어넣는다. | ||
- 쪼개려고 하는 단어를 trie를 통해서 매칭한다. (`Trie` 클래스의 `find_prefix_indices` 메소드) | ||
- 단어를 앞에서부터 한 글자씩 매칭하면서 | ||
- 중간에 end가 있는 노드를 만나면 `prefix_indices`에 값을 추가한다. "이 단어는 이 index | ||
에서 쪼개질 수 있어요!" 하는 의미를 담은 index라고 보면 된다. | ||
- 글자 매칭이 실패하면 매칭 종료. | ||
- 매칭이 끝나고 나서 `prefix_indices`를 리턴한다. | ||
- e.g.) wordDict = ["leet", "le", "code"], s = "leetcode"일때 | ||
- 첫 글자 l 매칭. 아무 일도 일어나지 않음. | ||
- 다음 글자 e 매칭. 이 노드는 end가 true다. "le"에 대응되기 때문. prefix_indices에 2 추가. | ||
- 다음 글자 e 매칭. 아무 일도 일어나지 않음. | ||
- 다음 글자 t 매칭. 이 노드는 "leet"에 대응되어 end가 true다. prefix_indices에 4 추가. | ||
- 다음 글자 c 매칭. 매칭 실패 후 종료. | ||
- prefix_indices = [2, 4]를 리턴. | ||
- 위의 매칭 과정이 끝나면 주어진 단어를 쪼갤 수 있는 방법들이 생긴다. | ||
- 쪼개진 단어에서 뒷 부분을 취한다. | ||
- e.g.) wordDict = ["leet", "le", "code"], s = "leetcode", prefix_indices = [2, 4] | ||
- prefix_indices의 각 아이템을 돌면서 | ||
- s[2:]를 통해서 "le/etcode" 중 뒷 부분 "etcode"를 취할 수 있다. | ||
- s[4:]를 통해서 "leet/code" 중 뒷 부분 "code"를 취할 수 있다. | ||
- 만약 뒷 부분이 빈 문자열("")이 될 경우 탐색에 성공한 것이다. | ||
- 코드 상에서는 빈 문자열로 탐색을 시도할 경우 탐색 성공의 의미로 true 반환. | ||
- 만약 prefix_indices가 빈 리스트로 온다면 쪼갤 수 있는 방법이 없다는 뜻이므로 탐색 실패다. | ||
- 그 외에는 취한 뒷 부분들에 대해 각각 다시 쪼개는 것을 시도한다. | ||
- 위의 과정에서 쪼개는 것을 이미 실패한 단어를 fail_list라는 set으로 관리하여 중복 연산을 막는다. | ||
- 즉, memoization을 활용. | ||
|
||
SC: | ||
- trie 생성. 최악의 경우 O(w * l) | ||
- fail list에 들어갈 수 있는 단어 길이 | ||
- 1, 2, ..., s | ||
- O(s^2) | ||
- 이걸 전체 단어를 저장하는 것 대신 맨 앞 글자의 index를 저장하는 식으로 구현하면 O(s)가 될 것이다. | ||
여기에서는 구현 생략. | ||
- find함수의 호출 스택 최악의 경우 한 글자씩 앞에서 빼면서 탐색 시도, O(s) | ||
- 종합하면 O(w * l) + O(s^2) + O(s) = O(w * l + s^2) | ||
|
||
TC: | ||
- ??? | ||
""" | ||
|
||
|
||
class Trie: | ||
def __init__(self): | ||
self.next: dict[str, Trie] = {} | ||
self.end: bool = False | ||
|
||
def insert(self, word: str) -> None: | ||
cur = self | ||
|
||
for c in word: | ||
cur.next[c] = cur.next.get(c, Trie()) | ||
cur = cur.next[c] | ||
|
||
cur.end = True | ||
|
||
def find_prefix_indices(self, word: str) -> list[str]: | ||
prefix_indices = [] | ||
ind = 0 | ||
cur = self | ||
|
||
for c in word: | ||
ind += 1 | ||
if c not in cur.next: | ||
break | ||
cur = cur.next[c] | ||
if cur.end: | ||
prefix_indices.append(ind) | ||
|
||
return prefix_indices | ||
|
||
|
||
class Solution: | ||
def wordBreak(self, s: str, wordDict: List[str]) -> bool: | ||
# init | ||
trie = Trie() | ||
for word in wordDict: | ||
trie.insert(word) | ||
|
||
fail_list = set() | ||
|
||
# recursive find | ||
def find(word: str) -> bool: | ||
# 단어의 앞에서 쪼갤 수 있는 경우를 전부 찾아서 쪼개고 | ||
# 뒤에 남은 단어를 다시 쪼개는 것을 반복한다. | ||
if word == "": | ||
return True | ||
|
||
if word in fail_list: | ||
return False | ||
|
||
cut_indices = trie.find_prefix_indices(word) | ||
result = any([find(word[i:]) for i in cut_indices]) | ||
if not result: | ||
fail_list.add(word) | ||
|
||
return result | ||
|
||
return find(s) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
설명을 꼼꼼하게 작성해주셔서 코드와 함께 따라가기 정말 좋습니다~
한 가지 알고 있는 부분을 말씀드리면, 복잡도를 분석할 때 output 에 필요한 메모리는 보통 무시하는 것 같습니다!