Skip to content
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
192 changes: 192 additions & 0 deletions 3sum/unpo88.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
class Solution:
def threeSum(self, nums: list[int]) -> list[list[int]]:
nums.sort()
result = set()

for i in range(len(nums) - 2):
# 양수인 경우 더 이상 탐색할 필요가 없음
if nums[i] > 0:
break

# i 중복 skip
if i > 0 and nums[i] == nums[i - 1]:
continue

left = i + 1
right = len(nums) - 1

while left < right:
total = nums[i] + nums[left] + nums[right]

if total == 0:
result.add((nums[i], nums[left], nums[right]))

# 중복 건너뛰기
while left < right and nums[left] == nums[left + 1]:
left += 1
# 중복 건너뛰기
while left < right and nums[right] == nums[right - 1]:
right -= 1
left += 1
right -= 1
elif total < 0:
left += 1
else:
right -= 1

return [list(t) for t in result]


"""
================================================================================
풀이 과정 - 10:51 시작
================================================================================

1. 정수 배열 nums가 주어지면
2. 모든 세 쌍 [nums[i], nums[j], nums[k]]을 반환해야한다
3. i != j, i != k, j != k 여야하고
4. nums[i] + nums[j] + nums[k] == 0 이어야한다
5. 중복된 세 쌍이 포함되지 않아야한다


[1차 시도] Brute Force 3중 반복문
────────────────────────────────────────────────────────────────────────────────
6. 제약을 무시하고 생각해보면 Brute Force 3중 반복문으로 찾을 수 있을 것 같음
7. 중복도 제거해야하니까 마지막에 set을 두고 처리

result = []
n = len(nums)

for i in range(n):
for j in range(i+1, n):
for k in range(j+1, n):
if nums[i] + nums[j] + nums[k] == 0:
triplet = sorted([nums[i], nums[j], nums[k]])
if triplet not in result:
result.append(triplet)

return result

8. Time Limit Exceeded 발생
9. O(n³) 시간 복잡도로 너무 느림


[2차 시도] Two Sum 응용 (HashSet)
────────────────────────────────────────────────────────────────────────────────
10. 3개를 동시에 찾으려니까 어려운 것 같은데
11. 2개를 먼저 구하고, 나머지 하나를 더하면 0이 되는지를 확인해볼까
12. 두 수의 합 = 나머지 하나의 값
13. -nums[i] = nums[j] + nums[k]
14. 그럼 Two Sum 문제로 풀 수 있을듯?
15. 두 개의 합이 set에 존재하는지를 체크하는 형태로 가보자

result = set()

for i in range(len(nums)):
seen = set()
for j in range(i + 1, len(nums)):
complement = -(nums[i] + nums[j])
if complement in seen:
result.add(tuple(sorted([nums[i], nums[j], complement])))
seen.add(nums[j])

return [list(x) for x in result]

16. 아 근데 이것도 Time Limit Exceeded가 발생하네
17. O(n²) 시간 복잡도로 개선했지만 sorted() 호출과 set 연산이 추가 비용 발생
18. 다른 접근 방법이 필요할 듯


[3차 시도] Two Pointer 방식
────────────────────────────────────────────────────────────────────────────────
19. 그럼 Two Pointer 방식으로 풀어야할 것 같은데
20. Two Pointer 방식을 사용하려면 정렬이 필요함
21. Two Sum의 Two Pointer 패턴을 생각해보자
22. 근데 3개를 동시에 찾으려면 어떻게 해야할까?
23. i를 고정하고, 나머지에서 Two Sum으로 합이 -nums[i]인 두 수를 찾는다!

정렬 전: [-1, 0, 1, 2, -1, -4]
정렬 후: [-4, -1, -1, 0, 1, 2]
index: 0 1 2 3 4 5

i = 0, left = i + 1, right = len(nums) - 1
[-4, -1, -1, 0, 1, 2]
i L R

i = 1:
[-4, -1, -1, 0, 1, 2]
i L R

25. 정렬 먼저 한 후 합의 크기에 따라 포인터를 이동

nums.sort()
result = set()

for i in range(len(nums) - 2):
left = i + 1
right = len(nums) - 1

while left < right:
total = nums[i] + nums[left] + nums[right]

if total == 0:
result.add((nums[i], nums[left], nums[right]))
left += 1
right -= 1
elif total < 0:
left += 1
else:
right -= 1

return [list(t) for t in result]

25. O(n²) 시간 복잡도 (정렬 O(n log n) + 반복문 O(n²))
26. 정상적으로 통과되는 것 확인 완료
27. 근데 이 방식으로 풀었는데 느리다


[4차 개선] 중복 제거 최적화
────────────────────────────────────────────────────────────────────────────────
28. Claude에게 물어보니 tuple 생성할 때 상수 배수가 큰 것 같다고 함
29. 중복을 미리 스킵해주는 방법을 알려줌
30. i가 양수인 경우 더 이상 탐색할 필요 없음 (정렬되어 있으므로)
31. i, left, right 중복 건너뛰기 추가

nums.sort()
result = set()

for i in range(len(nums) - 2):
# 양수인 경우 더 이상 탐색할 필요가 없음
if nums[i] > 0:
break

# i 중복 skip
if i > 0 and nums[i] == nums[i - 1]:
continue

left = i + 1
right = len(nums) - 1

while left < right:
total = nums[i] + nums[left] + nums[right]

if total == 0:
result.add((nums[i], nums[left], nums[right]))

# 중복 건너뛰기
while left < right and nums[left] == nums[left + 1]:
left += 1
while left < right and nums[right] == nums[right - 1]:
right -= 1
left += 1
right -= 1
elif total < 0:
left += 1
else:
right -= 1

return [list(t) for t in result]

32. 중복 처리 최적화로 속도 개선
33. 최종 통과 확인 완료
"""
63 changes: 63 additions & 0 deletions climbing-stairs/unpo88.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
class Solution:
def climbStairs(self, n: int) -> int:
if n <= 2:
return n

prev2, prev1 = 1, 2

for i in range(3, n + 1):
current = prev1 + prev2
prev2 = prev1
prev1 = current

return prev1

"""
================================================================================
풀이 과정 - 08:49 시작 ~ 08:54 종료 (5분 소요)
================================================================================

1. 계단 올라갈건데, n step씩 올라감
2. 1 또는 2단계씩 갈 수 있는데, 얼마나 많이 탑까지 올라갈 수 있는 방법이 있을까?
3. 재귀로 내가 1 또는 2로 갈 수 있는 방법을 모두 구해야할 것 같은데?
4. n - 1, n - 2
5. 3번째 계단부터 첫 번째 계단 갈 수 있는 방법 + 두 번째 계단 갈 수 있는 방법
6. DP로 풀어야할 것 같은데?
7. 이거 계단 경우의 수 보니까 피보나치 수열이네?


[1차 시도] 딕셔너리를 사용한 DP
────────────────────────────────────────────────────────────────────────────────
7. 피보나치 수열과 유사한 패턴 발견
8. dp[i] = dp[i-1] + dp[i-2] 점화식 도출
9. 기저 사례: dp[1] = 1, dp[2] = 2

dp = { 1: 1, 2: 2}
for i in range(3, n + 1):
dp[i] = dp[i-1] + dp[i-2]

return dp[n]

10. 정상적으로 통과되는 것 확인 완료


[2차 개선] 공간 복잡도 최적화
────────────────────────────────────────────────────────────────────────────────
11. 잘 생각해보면 공간 복잡도를 아예 딕셔너리를 안만들고 푸는 방법도 있을듯?
13. 변수 2개(prev2, prev1)로 공간 복잡도를 O(1)로 개선

if n <= 2:
return n

prev2, prev1 = 1, 2

for i in range(3, n + 1):
current = prev1 + prev2
prev2 = prev1
prev1 = current

return prev1

14. 공간 복잡도 O(n) → O(1)로 개선 완료
15. 최종 통과 확인 완료
"""
123 changes: 123 additions & 0 deletions product-of-array-except-self/unpo88.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
class Solution:
def productExceptSelf(self, nums: list[int]) -> list[int]:
n = len(nums)
answer = [1] * n

left = 1
for i in range(n):
answer[i] = left
left *= nums[i]

right = 1
for i in range(n-1, -1, -1):
answer[i] *= right
right *= nums[i]

return answer

"""
================================================================================
풀이 과정 - 09:43 시작 ~ 풀이 과정 떠올리지 못함
================================================================================

1. 정수 배열 nums가 주어짐
2. 배열 answer를 반환해야 하는데
3. answer[i]는 nums의 모든 요소의 곱과 같다 (nums[i]를 제외한)
4. O(n) 시간에 실행되는 알고리즘을 작성해야하고, 나누기 연산을 사용해서는 안된다
5. 나누기 연산을 어떻게 이용 안하고 풀지?

────────────────────────────────────────────────────────────────────────────────
6. Claude 도움 → answer[i] = (i 왼쪽의 곱) * (i 오른쪽의 곱)


[1차 시도] 왼쪽/오른쪽 곱 배열 사용
────────────────────────────────────────────────────────────────────────────────
7. 각 위치의 왼쪽 누적곱과 오른쪽 누적곱을 미리 계산
8. left[i] = nums[0] * ... * nums[i-1]
9. right[i] = nums[i+1] * ... * nums[n-1]
10. answer[i] = left[i] * right[i]

n = len(nums)
left = [1] * n
for i in range(1, n):
left[i] = left[i-1] * nums[i-1]

right = [1] * n
for i in range(n-2, -1, -1):
right[i] = right[i+1] * nums[i+1]

return [left[i] * right[i] for i in range(n)]

11. 정상적으로 통과되는 것 확인 완료


[2차 개선] 공간 복잡도 최적화
────────────────────────────────────────────────────────────────────────────────
12. left, right 배열을 따로 만들면 O(n) 공간 복잡도 추가 사용
13. answer 배열을 재활용하면 추가 공간 없이 해결 가능
14. 첫 번째 순회: answer에 왼쪽 누적곱 저장
15. 두 번째 순회: answer에 오른쪽 누적곱을 곱하면서 최종 결과 완성

n = len(nums)
answer = [1] * n

left = 1
for i in range(n):
answer[i] = left
left *= nums[i]

right = 1
for i in range(n-1, -1, -1):
answer[i] *= right
right *= nums[i]

return answer

16. 추가 공간 복잡도 O(n) → O(1)로 개선 완료 (answer 배열 제외)
17. 최종 통과 확인 완료


[문제 복기] 패턴 발견 과정
────────────────────────────────────────────────────────────────────────────────
- 문제를 다시 복기해보면
- 각 위치에서 자기 자신을 제외한 모든 원소의 곱을 구해야함
- 제약을 무시하면 이중 반복문 Brute Force로 풀 수 있음
- 제약을 무시하면 나눗셈을 이용해서도 풀 수 있음
- 나눗셈도 금지되고 O(n) 시간에 풀어야한다면?
- "제외한다"를 어떻게 표현해주면 좋을까? (이 부분이 핵심)

작은 예시로 패턴 찾기:
nums = [2, 3, 4]

answer[0] = 3 * 4 = 12
answer[1] = 2 * 4 = 8
answer[2] = 2 * 3 = 6

answer[0] = (없음) * (3 * 4) = 12
answer[1] = (2) * (4) = 8
answer[2] = (2 * 3) * (없음) = 6

- 패턴 발견 → 왼쪽 부분 * 오른쪽 부분
- 해법 도출 → 왼쪽 부분의 모든 곱 * 오른쪽 부분의 모든 곱

패턴을 일반화하는 과정이 필요:
answer[i] = nums[0] * ... * nums[i-1] * nums[i+1] * ... * nums[n-1]
left[i] = nums[0] * ... * nums[i-1]
right[i] = nums[i+1] * ... * nums[n-1]
answer[i] = left[i] * right[i]

점화식 찾는데, DP적 사고 필요:
왼쪽 누적곱:
left[1] = nums[0]
left[2] = nums[0] * nums[1] = left[1] * nums[1]
left[3] = nums[0] * nums[1] * nums[2] = left[2] * nums[2]
→ left[i] = left[i-1] * nums[i-1]

오른쪽 누적곱:
right[2] = nums[3]
right[1] = nums[3] * nums[2] = right[2] * nums[2]
right[0] = nums[3] * nums[2] * nums[1] = right[1] * nums[1]
→ right[i] = right[i+1] * nums[i+1]

관련해서 코드로 구현하는 과정이 필요함
"""
Loading