diff --git a/combination-sum/unpo88.py b/combination-sum/unpo88.py new file mode 100644 index 0000000000..4ed0ac7c7a --- /dev/null +++ b/combination-sum/unpo88.py @@ -0,0 +1,77 @@ +class Solution: + def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]: + result = [] + + def backtrack(start, current, current_sum): + if current_sum == target: + result.append(current[:]) + return + + if current_sum > target: + return + + for i in range(start, len(candidates)): + current.append(candidates[i]) + backtrack(i, current, current_sum + candidates[i]) + current.pop() + + backtrack(0, [], 0) + return result + + +""" +================================================================================ +풀이 과정 +================================================================================ + +1. 이거 모든 가능한 조합을 찾아야하는데 합이 target을 넘으면 중단되야하네? +2. 문제는 같은 숫자를 여러 번 쓸 수 있는건데.. +3. 백트래킹으로 접근해야할 것 같은데.. + + +[1차 시도] 백트래킹 (Backtracking) +──────────────────────────────────────────────────────────────────────────────── +4. 모든 조합을 탐색하되, 조건에 맞지 않으면 가지치기 +5. 같은 숫자를 여러 번 사용할 수 있으므로 재귀 호출 시 같은 인덱스부터 시작 +6. 중복 조합 방지를 위해 start 인덱스 사용 + + result = [] + + def backtrack(start, current, current_sum): + # target을 만족하면 결과에 추가 + if current_sum == target: + result.append(current[:]) # 복사해서 추가 + return + + # 가지치기: 합이 target 초과 + if current_sum > target: + return + + # 조합 탐색 + for i in range(start, len(candidates)): + current.append(candidates[i]) + # 같은 숫자 재사용 가능하므로 i부터 시작 + backtrack(i, current, current_sum + candidates[i]) + current.pop() # 백트래킹: 상태 복원 + + backtrack(0, [], 0) + return result + +7. Example: candidates = [2,3,6,7], target = 7 + + backtrack(0, [], 0) + ├─ 2 추가 → backtrack(0, [2], 2) + │ ├─ 2 추가 → backtrack(0, [2,2], 4) + │ │ ├─ 2 추가 → backtrack(0, [2,2,2], 6) + │ │ │ └─ 2 추가 → backtrack(0, [2,2,2,2], 8) → 8 > 7 return + │ │ └─ 3 추가 → backtrack(1, [2,2,3], 7) → 7 == 7 ✅ [2,2,3] + │ └─ 3 추가 → backtrack(1, [2,3], 5) + │ └─ ... (탐색 계속) + └─ 7 추가 → backtrack(3, [7], 7) → 7 == 7 ✅ [7] + +8. 시간 복잡도: O(N^(T/M)) + - N: candidates 길이 + - T: target 값 + - M: candidates의 최소값 +9. 공간 복잡도: O(T/M) - 재귀 호출 스택 깊이 +""" diff --git a/decode-ways/unpo88.py b/decode-ways/unpo88.py new file mode 100644 index 0000000000..17c6dc9129 --- /dev/null +++ b/decode-ways/unpo88.py @@ -0,0 +1,66 @@ +class Solution: + def numDecodings(self, s: str) -> int: + # 예외 처리: "0"으로 시작하면 불가능 + if not s or s[0] == '0': + return 0 + + n = len(s) + dp = [0] * (n + 1) + + # 초기값 + dp[0] = 1 # 빈 문자열 + dp[1] = 1 # 첫 번째 문자 (이미 "0" 체크함) + + for i in range(2, n + 1): + # 한 자리 숫자 (1~9) + if s[i-1] != '0': + dp[i] += dp[i-1] + + # 두 자리 숫자 (10~26) + two_digit = int(s[i-2:i]) + if 10 <= two_digit <= 26: + dp[i] += dp[i-2] + + return dp[n] + + +""" +================================================================================ +풀이 과정 +================================================================================ +- "226" → 2|2|6 (BBF), 22|6 (VF), 2|26 (BZ) → 3가지 방법 존재 +- DP를 이용해서 각 위치에서 가능한 디코딩 경우의 수를 계산 +- 한 자리(1~9)와 두 자리(10~26) 숫자를 고려하여 누적 + +[1차 시도] +──────────────────────────────────────────────────────────────────────────────── +1. 접근 방법 + - DP 배열 사용: dp[i] = 문자열의 처음부터 i번째까지의 디코딩 경우의 수 + - 각 위치에서 한 자리 숫자(1~9)로 디코딩 가능하면 dp[i-1] 더하기 + - 두 자리 숫자(10~26)로 디코딩 가능하면 dp[i-2] 더하기 + +2. 구현 + n = len(s) + dp = [0] * (n + 1) + + # 초기값 설정 + dp[0] = 1 # 빈 문자열 + dp[1] = 1 # 첫 번째 문자 (0이 아니면 1가지) + + for i in range(2, n + 1): + # 한 자리 숫자 (1~9) + if s[i-1] != '0': + dp[i] += dp[i-1] + + # 두 자리 숫자 (10~26) + two_digit = int(s[i-2:i]) + if 10 <= two_digit <= 26: + dp[i] += dp[i-2] + +3. 예외 처리 + - "0"으로 시작하는 문자열은 디코딩 불가능 → 0 반환 + - 빈 문자열 체크 + +4. 시간 복잡도: O(n) - 문자열을 한 번만 순회 +5. 공간 복잡도: O(n) - DP 배열 사용 +""" diff --git a/maximum-subarray/unpo88.py b/maximum-subarray/unpo88.py new file mode 100644 index 0000000000..294fe53497 --- /dev/null +++ b/maximum-subarray/unpo88.py @@ -0,0 +1,63 @@ +class Solution: + def maxSubArray(self, nums: List[int]) -> int: + current_sum = nums[0] + max_sum = nums[0] + + for i in range(1, len(nums)): + # 현재 값부터 새로 시작 vs 이전 합에 추가 + current_sum = max(nums[i], current_sum + nums[i]) + max_sum = max(max_sum, current_sum) + + return max_sum + + +""" +================================================================================ +풀이 과정 +================================================================================ + +1. 부분 배열 중 가장 큰 합을 찾아야 하는데 어떻게 접근하지? +2. Sliding Window? → 음수/양수가 섞여있어서 윈도우 크기를 언제 조절할지 불명확 +3. 모든 부분배열을 확인? → O(n²)이라 비효율적 +4. 각 위치에서 "현재까지의 최대 부분합"을 추적하면 되지 않을까? + + +[1차 시도] Dynamic Programming +──────────────────────────────────────────────────────────────────────────────── +5. 핵심 아이디어: 각 위치에서 "이 위치를 끝으로 하는" 최대 부분배열의 합 추적 +6. current_sum = "현재 위치를 끝으로 하는 최대 부분합" +7. 매 위치에서 선택: 이전 합에 추가 vs 여기서 새로 시작 + + current_sum = nums[0] # 현재 위치까지의 최대 부분합 + max_sum = nums[0] # 전체 최댓값 + + for i in range(1, len(nums)): + # 이전 합이 양수면 계속, 음수면 버리고 새로 시작 + current_sum = max(nums[i], current_sum + nums[i]) + max_sum = max(max_sum, current_sum) + + return max_sum + +8. Example: nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4] + + i=0: current=-2, max=-2 + i=1: max(1, -2+1=-1) = 1 (새로 시작), max=1 + i=2: max(-3, 1-3=-2) = -2 (이어감), max=1 + i=3: max(4, -2+4=2) = 4 (새로 시작), max=4 + i=4: max(-1, 4-1=3) = 3 (이어감), max=4 + i=5: max(2, 3+2=5) = 5 (이어감), max=5 + i=6: max(1, 5+1=6) = 6 (이어감), max=6 ✓ + i=7: max(-5, 6-5=1) = 1 (이어감), max=6 + i=8: max(4, 1+4=5) = 5 (이어감), max=6 + + 결과: [4, -1, 2, 1] = 6 + +9. 왜 작동하는가? + - 모든 부분배열은 어딘가에서 끝남 + - 각 위치에서 "여기를 끝으로 하는 최댓값" 추적 + - 이전 합이 음수면 버리는 게 이득 (Greedy한 선택) + - 이전 합이 양수면 현재 값이 음수여도 계속 (예: 4 + (-1) = 3) + +10. 시간 복잡도: O(n) - 배열을 한 번만 순회 +11. 공간 복잡도: O(1) - 변수 2개만 사용 +""" diff --git a/number-of-1-bits/unpo88.py b/number-of-1-bits/unpo88.py new file mode 100644 index 0000000000..a6c5fe23b8 --- /dev/null +++ b/number-of-1-bits/unpo88.py @@ -0,0 +1,24 @@ +class Solution: + def hammingWeight(self, n: int) -> int: + return bin(n).count('1') + + +""" +================================================================================ +풀이 과정 +================================================================================ + +1. 일단 이진법으로 변경이 필요하고 +2. 변경된 이진법에서 1을 카운팅해야하네? + + +[1차 시도] 내장 함수 활용 +──────────────────────────────────────────────────────────────────────────────── +3. Python의 bin() 함수로 이진 문자열 변환 +4. count() 메서드로 '1' 문자 개수 세기 + + return bin(n).count('1') + +5. 시간 복잡도: O(log n) - 이진 표현의 비트 수만큼 +6. 공간 복잡도: O(log n) - 이진 문자열 생성 +""" diff --git a/valid-palindrome/unpo88.py b/valid-palindrome/unpo88.py new file mode 100644 index 0000000000..b85d8527dd --- /dev/null +++ b/valid-palindrome/unpo88.py @@ -0,0 +1,66 @@ +class Solution: + def isPalindrome(self, s: str) -> bool: + left = 0 + right = len(s) - 1 + + while left < right: + if not s[left].isalnum(): + left += 1 + continue + + if not s[right].isalnum(): + right -= 1 + continue + + if s[left].upper() != s[right].upper(): + return False + + left += 1 + right -= 1 + + return True + + +""" +================================================================================ +풀이 과정 +================================================================================ +- A man, a plan, a canal: Panama +- 띄어쓰기는 무시하고 앞 뒤에서 똑같은지 체크를 해야하네? +- 앞 포인터와 뒷 포인터에서 시작해서 띄어쓰기 만나면 건너뛰고 +- 틀린것 나오면 False 반환하고, False를 만난적 없으면 True 반환 + +[1차 시도] Two Pointer +──────────────────────────────────────────────────────────────────────────────── +1. 접근 방법 + - Two Pointer 사용: left는 앞에서, right는 뒤에서 시작 + - 유효하지 않은 문자(알파벳/숫자가 아닌 것)는 건너뛰기 + - 유효한 문자끼리 비교하며 중앙으로 이동 + +2. 구현 + left = 0 + right = len(s) - 1 + + while left < right: + # 왼쪽 포인터: 알파벳/숫자가 아니면 건너뛰기 + if not s[left].isalpha() and not s[left].isnumeric(): + left += 1 + continue + + # 오른쪽 포인터: 알파벳/숫자가 아니면 건너뛰기 + if not s[right].isalpha() and not s[right].isnumeric(): + right -= 1 + continue + + # 대소문자 무시하고 비교 + if s[left].upper() != s[right].upper(): + return False + + left += 1 + right -= 1 + + return True + +4. 시간 복잡도: O(n) - 문자열을 한 번만 순회 +5. 공간 복잡도: O(1) - 추가 공간 사용 없음 (포인터 2개만 사용) +"""