diff --git a/best-time-to-buy-and-sell-stock/unpo88.py b/best-time-to-buy-and-sell-stock/unpo88.py new file mode 100644 index 000000000..8f69f17d5 --- /dev/null +++ b/best-time-to-buy-and-sell-stock/unpo88.py @@ -0,0 +1,78 @@ +class Solution: + def maxProfit(self, prices: list[int]) -> int: + if not prices: + return 0 + + min_price = prices[0] + max_profit = 0 + + for price in prices[1:]: + max_profit = max(max_profit, price - min_price) + min_price = min(min_price, price) + + return max_profit + + +""" +================================================================================ +풀이 과정 +================================================================================ + +[문제 이해] +──────────────────────────────────────────────────────────────────────────────── +1. 주어진 배열 prices가 있고 prices[i]는 i날의 특정 주식 가격 +2. 하나의 주식을 사기 위해 하루를 선택하고, 그 주식을 팔기 위해 미래의 다른 날을 선택 +3. 이 거래에서 얻을 수 있는 최대 이익을 반환 +4. 이익을 얻을 수 없다면 0을 반환 + + 예시 1: [7, 1, 5, 3, 6, 4] → 5 + 2일에 사서 5일에 팔면 → 6 - 1 = 5가 나오면서 가장 큰 값 + + 예시 2: [7, 6, 4, 3, 1] → 0 + 아무런 이익이 있는 날이 없음 + + +[1차 시도] Brute Force 접근 +──────────────────────────────────────────────────────────────────────────────── +5. 모든 (구매일, 판매일) 쌍을 비교해서 최대 이익 찾기 +6. 이중 반복문으로 구현 + + profit = 0 + for i in range(len(prices)): + for j in range(i+1, len(prices)): + profit = max(profit, prices[j] - prices[i]) + return profit + +7. 시간복잡도: O(n²) → 시간 초과 발생! + + +[2차 시도] 최적화 방법 고민 +──────────────────────────────────────────────────────────────────────────────── +8. 그러면 다른 풀이 방식이 필요할 것 같은데 +9. 조건을 정리: + - 이전 값이 다음 값보다 크면 버려도 된다 + - 이전 값이 다음 값보다 작으면 해당 값을 기록한다 + - 이미 기록된게 있으면 비교해본다 + + [7, 1, 5, 9, 3, 6, 4] + min을 추적하면서 현재가 - min 중 최대값을 찾으면 됨! + + +[최종 구현] Kadane's Algorithm 변형 +──────────────────────────────────────────────────────────────────────────────── +10. 한 번의 순회로 최소 구매가와 최대 이익을 동시에 추적 +11. 각 시점에서: + - 현재가 - 최소가 = 지금 팔면 얻는 이익 + - 최대 이익 갱신 + - 최소가 갱신 + + min_price = prices[0] + max_profit = 0 + + for price in prices[1:]: + max_profit = max(max_profit, price - min_price) + min_price = min(min_price, price) + +12. 시간복잡도: O(n) - 한 번의 순회 +13. 공간복잡도: O(1) - 상수 공간만 사용 +""" diff --git a/group-anagrams/unpo88.py b/group-anagrams/unpo88.py new file mode 100644 index 000000000..2fc924eda --- /dev/null +++ b/group-anagrams/unpo88.py @@ -0,0 +1,58 @@ +class Solution: + def groupAnagrams(self, strs: List[str]) -> List[List[str]]: + anagrams = defaultdict(list) + for s in strs: + anagrams["".join(sorted(s))].append(s) + + return list(anagrams.values()) + + +""" +================================================================================ +풀이 과정 +================================================================================ + +[문제 이해] +──────────────────────────────────────────────────────────────────────────────── +1. 문자열 배열 strs가 주어지면 애너그램끼리 그룹화하여 반환 +2. 애너그램: 같은 문자들을 재배열하여 만든 단어 (각 문자를 한 번씩만 사용) +3. 결과의 순서는 상관없음 + + 예시 1: ["eat", "tea", "tan", "ate", "nat", "bat"] + → [["bat"], ["nat", "tan"], ["ate", "eat", "tea"]] + + 예시 2: [""] + → [[""]] + + 예시 3: ["a"] + → [["a"]] + + +[접근 방법] 해시맵 + 정렬 키 +──────────────────────────────────────────────────────────────────────────────── +4. 애너그램의 특징: 정렬하면 같은 문자열이 됨 + - "eat" → "aet" + - "tea" → "aet" + - "ate" → "aet" + +5. 정렬된 문자열을 키로 사용하는 해시맵 구성 + - 키: 정렬된 문자열 + - 값: 해당 애너그램 그룹 리스트 + +6. 구현: + anagrams = defaultdict(list) + for s in strs: + anagrams["".join(sorted(s))].append(s) + return list(anagrams.values()) + + +[복잡도 분석] +──────────────────────────────────────────────────────────────────────────────── +7. 시간복잡도: O(n × k log k) + - n: 문자열 개수 + - k: 가장 긴 문자열의 길이 + - 각 문자열을 정렬하는데 O(k log k) + +8. 공간복잡도: O(n × k) + - 해시맵에 모든 문자열 저장 +""" diff --git a/implement-trie-prefix-tree/unpo88.py b/implement-trie-prefix-tree/unpo88.py new file mode 100644 index 000000000..330223c70 --- /dev/null +++ b/implement-trie-prefix-tree/unpo88.py @@ -0,0 +1,80 @@ +class Trie: + + def __init__(self): + self.children = {} + self.is_end = False + + def insert(self, word: str) -> None: + node = self + for c in word: + if c not in node.children: + node.children[c] = Trie() + node = node.children[c] + node.is_end = True + + def search(self, word: str) -> bool: + node = self._find(word) + return node is not None and node.is_end + + def startsWith(self, prefix: str) -> bool: + return self._find(prefix) is not None + + def _find(self, s: str): + node = self + for c in s: + if c not in node.children: + return None + node = node.children[c] + return node + + +""" +================================================================================ +풀이 과정 +================================================================================ + +[문제 이해] +──────────────────────────────────────────────────────────────────────────────── +1. Trie(트라이) 자료구조 구현 +2. insert(word): 단어 삽입 +3. search(word): 정확히 일치하는 단어 있는지 +4. startsWith(prefix): prefix로 시작하는 단어 있는지 + + +[1차 시도] 해시맵 +──────────────────────────────────────────────────────────────────────────────── +5. 처음에 해싱으로 풀었더니 통과는 했는데 startsWith에서 결국 전체 순회 필요 +6. 시간복잡도 O(n × m) → 비효율적, 진짜 Trie로 풀어야겠다 + + +[2차 시도] 진짜 Trie +──────────────────────────────────────────────────────────────────────────────── +7. 각 노드가 children(자식 노드들)과 is_end(단어 끝 여부)를 가짐 + +8. 구조 예시: "app", "apple" 삽입 시 + + root + │ + a + │ + p + │ + p (is_end: "app") + │ + l + │ + e (is_end: "apple") + +9. insert: 문자 하나씩 따라가며 노드 생성, 마지막에 is_end = True +10. search: 문자 따라간 후 is_end가 True인지 확인 +11. startsWith: 문자 따라갈 수 있으면 True + + +[복잡도 분석] +──────────────────────────────────────────────────────────────────────────────── +12. 시간복잡도: O(m) - 모든 연산 + - m: 단어/prefix 길이 + +13. 공간복잡도: O(n × m) + - n: 단어 개수, m: 평균 단어 길이 +""" diff --git a/word-break/unpo88.py b/word-break/unpo88.py new file mode 100644 index 000000000..91573ef45 --- /dev/null +++ b/word-break/unpo88.py @@ -0,0 +1,60 @@ +class Solution: + def wordBreak(self, s: str, wordDict: List[str]) -> bool: + word_set = set(wordDict) + dp = [False] * (len(s) + 1) + dp[0] = True + + for i in range(1, len(s) + 1): + for j in range(i): + if dp[j] and s[j:i] in word_set: + dp[i] = True + break + + return dp[len(s)] + + +""" +================================================================================ +풀이 과정 +================================================================================ + +[문제 이해] +──────────────────────────────────────────────────────────────────────────────── +1. 문자열 s와 사전 wordDict가 주어짐 +2. s를 사전에 있는 단어들로 분할할 수 있는지 판단 + + 예시 1: s = "leetcode", wordDict = ["leet", "code"] + → True ("leet" + "code") + + 예시 2: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] + → False (어떻게 나눠도 안 됨) + + +[접근 방법] DP +──────────────────────────────────────────────────────────────────────────────── +3. dp[i] = s[0:i]를 사전 단어들로 분할 가능한가? + +4. 점화식: dp[i] = dp[j] and s[j:i] in word_set (어떤 j에 대해) + - dp[j]가 True이고, s[j:i]가 사전에 있으면 dp[i] = True + +5. 예시: s = "leetcode" + + l e e t c o d e + 0 1 2 3 4 5 6 7 8 + T ? ? ? T ? ? ? T + │ │ │ + 빈문자열 "leet" "code" + + dp[4] = dp[0] + "leet" → True + dp[8] = dp[4] + "code" → True + + +[복잡도 분석] +──────────────────────────────────────────────────────────────────────────────── +6. 시간복잡도: O(n² × m) + - n: 문자열 길이 + - m: 평균 단어 길이 (해시 비교) + +7. 공간복잡도: O(n) + - dp 배열 크기 +"""