[alphaorderly] WEEK 08 Solutions - #2810
Conversation
📊 alphaorderly 님의 학습 현황이번 주 제출 문제
누적 학습 요약
문제 풀이 현황
🤖 이 댓글은 GitHub App을 통해 자동으로 작성되었습니다. 🔢 API 사용량 (gpt-5-nano)
|
There was a problem hiding this comment.
🏷️ 알고리즘 패턴 분석
clone-graph/alphaorderly.py
"""
시간복잡도 : O(V + E)
공간복잡도 : O(V)
1. made 딕셔너리를 초기화한다.
2. copy_node 함수를 정의한다.
3. copy_node 함수는 target 노드를 복사한 후 반환한다.
4. 이미 복사한 노드는 made에서 재사용한다.
5. 복사한 노드의 이웃들 또한 copy_node를 이용해 재귀적으로 복사한다.
6. 최종적으로 복제된 그래프의 시작 노드를 반환한다.
"""
class Solution:
def cloneGraph(self, node: Optional["Node"]) -> Optional["Node"]:
made = dict()
def copy_node(target: Node) -> Node:
if not target:
return None
copied = Node(target.val)
made[target.val] = copied
for nei in target.neighbors:
if nei.val in made:
copied.neighbors.append(made[nei.val])
else:
copied.neighbors.append(copy_node(nei))
return copied
return copy_node(node)- 패턴: Depth-First Search, Hash Map / Hash Set
- 설명: 그래프의 각 노드를 재귀적으로 방문하며 이웃 노드를 재귀적으로 복제한다. 이미 복제된 노드를 해시 맵으로 재사용하여 순환과 중복 방문을 방지한다.
📊 시간/공간 복잡도 분석
| 복잡도 | |
|---|---|
| Time | O(V + E) |
| Space | O(V) |
피드백: 각 노드를 한 번씩 방문하고 이웃 노드를 재귀적으로 복제하므로 전체 그래프의 노드 수와 간선 수에 비례하는 시간 복잡도와 노드 수에 비례하는 추가 공간이 필요하다.
개선 제안: 현재 구현이 적절해 보입니다.
💡 풀이에 시간/공간 복잡도를 주석으로 남겨보세요!
There was a problem hiding this comment.
🏷️ 알고리즘 패턴 분석
longest-common-subsequence/alphaorderly.py
"""
시간복잡도 : O(T1 * T2)
공간복잡도 : O(T1) # T1에 짧은 문자열을 설정했기에 공간복잡도는 T1이 된다.
1. text1과 text2 중 더 짧은 쪽을 text1으로 설정한다.
2. T1과 T2를 각각 text1과 text2의 길이로 설정한다.
3. dp 배열을 T1 + 1 크기로 0으로 초기화한다.
4. text2의 각 문자(i)를 순회하면서 비교한다.
5. new_dp 배열을 T1 + 1 크기로 0으로 초기화한다.
6. text1의 각 문자(j)에 대해 순회하면서 비교한다.
7. text2[i - 1]과 text1[j - 1]가 같으면, new_dp[j]를 dp[j - 1] + 1로 설정한다.
8. 다르면, new_dp[j]를 max(new_dp[j - 1], dp[j])로 설정한다.
9. dp를 new_dp로 갱신하고, 마지막 원소를 반환한다.
"""
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
if len(text1) > len(text2):
text1, text2 = text1, text2
T1, T2 = len(text1), len(text2)
dp = [0] * (T1 + 1)
for i in range(1, T2 + 1):
new_dp = [0] * (T1 + 1)
for j in range(1, T1 + 1):
if text2[i - 1] == text1[j - 1]:
new_dp[j] = dp[j - 1] + 1
else:
new_dp[j] = max(new_dp[j - 1], dp[j])
dp = new_dp
return dp[-1]- 패턴: Dynamic Programming, Two Pointers
- 설명: 두 문자열의 부분수열 문제를 DP로 해결하는 전형적인 패턴으로, 각 위치의 부분문자열 조합에 대한 최장 공통 부분수열 길이를 점진적으로 계산한다. 또한 길이가 서로 다른 두 문자열에 대해 간접적으로 두 포인터처럼 인덱스 순회와 DP 배열 갱신으로 구성된다.
📊 시간/공간 복잡도 분석
| 복잡도 | |
|---|---|
| Time | O(T1 * T2) |
| Space | O(T1) |
피드백: 공간을 줄이기 위해 두 열 배열을 교체하며 시간 복잡도는 두 문자열 길이의 곱이다.
개선 제안: 현재 구현이 적절해 보입니다.
💡 풀이에 시간/공간 복잡도를 주석으로 남겨보세요!
There was a problem hiding this comment.
🏷️ 알고리즘 패턴 분석
longest-repeating-character-replacement/alphaorderly.py
"""
시간복잡도 : O(N)
공간복잡도 : O(1)
1. freq 딕셔너리를 초기화한다.
2. s의 각 문자(value)에 대해 루프를 돈다.
3. freq[value]를 1 증가시킨다.
4. maxima를 현재 윈도우에서 가장 빈도가 높은 문자 빈도로 갱신한다.
5. 윈도우 크기에서 maxima의 값만큼을 뺀 값이 k보다 크면,
6. freq[s[left]]를 1 감소시키고 left를 1 증가시킨다.
7. ans를 윈도우의 최대 길이로 갱신한다.
8. 마지막으로 ans를 반환한다.
"""
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
freq = defaultdict(int)
# maxima : 현재 윈도우 내에서 가장 많이 등장한 문자의 빈도수
maxima = left = ans = 0
for right, value in enumerate(s):
freq[value] += 1
maxima = max(maxima, freq[value])
while (right - left + 1) - maxima > k:
freq[s[left]] -= 1
left += 1
ans = max(ans, right - left + 1)
return ans
"""
시간복잡도 : O(log N)
공간복잡도 : O(N)
### 세그먼트 트리 구현 ### (윈도우 내 전체만 사용하므로 query 구현 없음)
#### 세그먼트 트리 원리
- 각 알파벳 빈도 합과 빈도가 가장 높은 문자의 빈도를 저장하는 세그먼트 트리를 사용한다.
#### 코드 설명
- window.update로 알파벳 개수를 갱신하며,
- window.tree[1][0]에서 현재 윈도우 내 전체 문자 수,
- window.tree[1][1]에서 윈도우 내 등장 빈도가 가장 높은 문자의 개수를 구한다.
- 윈도우 크기 - 최대 빈도가 k를 초과하면 left를 옮기며 윈도우를 줄인다.
> 불필요하게 복잡한 구현이지만, 세그먼트 트리를 공부하기엔 좋은 예제가 될 수 있다.
"""
class SegTree:
def __init__(self):
# [문자 개수 합, 최대값]
self.tree = [[0, 0] for _ in range(26 * 4 + 1)]
def _update(
self,
node_index: int,
target_index: int,
target_update: int,
seg_left: int,
seg_right: int,
):
if seg_left == seg_right:
self.tree[node_index][0] += target_update
self.tree[node_index][1] += target_update
return
mid = (seg_left + seg_right) // 2
if target_index <= mid:
self._update(node_index * 2, target_index, target_update, seg_left, mid)
else:
self._update(
node_index * 2 + 1, target_index, target_update, mid + 1, seg_right
)
self.tree[node_index][0] = (
self.tree[node_index * 2][0] + self.tree[node_index * 2 + 1][0]
)
self.tree[node_index][1] = max(
self.tree[node_index * 2][1], self.tree[node_index * 2 + 1][1]
)
def update(self, target: str, update: int):
self._update(1, ord(target) - ord("A"), update, 0, 25)
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
left = 0
window = SegTree()
ans = 0
for right, value in enumerate(s):
window.update(value, 1)
while window.tree[1][0] > 0 and window.tree[1][0] - window.tree[1][1] > k:
window.update(s[left], -1)
left += 1
ans = max(ans, right - left + 1)
return ans- 패턴: Sliding Window, Greedy
- 설명: 첫 번째 구현은 슬라이딩 윈도우를 이용해 문자 빈도 차이를 k 이하로 유지하며 윈도우 길이를 확장하는 패턴이고, 두 번째 구현도 윈도우 크기를 조절하며 최대 빈도와의 차이가 k를 넘지 않도록 왼쪽 포인터를 이동하는 그리디 성격의 접근이다. 두 방법 모두 문자열 내 적절한 부분문자열 길이를 최적화한다.
📊 시간/공간 복잡도 분석
ℹ️ 이 파일에는 2가지 풀이가 포함되어 있어 각각 분석합니다.
풀이 1: Solution.characterReplacement — Time: O(N) / Space: O(1)
| 복잡도 | |
|---|---|
| Time | O(N) |
| Space | O(1) |
피드백: 윈도우 안의 문자 빈도수 추적으로 최대 길이를 결정한다. 최대 빈도수를 유지하는 방식이 핵심이다.
개선 제안: 현재 구현이 적절해 보입니다.
풀이 2: SegTree — Time: O(log N) / Space: O(N)
| 복잡도 | |
|---|---|
| Time | O(log N) |
| Space | O(N) |
피드백: 복잡도가 불필요하게 커지는 예제지만 세그먼트 트리 학습에 유용하다.
개선 제안: 현재 구현이 적절해 보입니다.
💡 풀이에 시간/공간 복잡도를 주석으로 남겨보세요!
There was a problem hiding this comment.
🏷️ 알고리즘 패턴 분석
palindromic-substrings/alphaorderly.py
"""
시간복잡도 : O(N^2)
공간복잡도 : O(1)
1. n을 s의 길이로 설정한다.
2. count를 0으로 초기화한다.
3. 각 문자를 중심(center)으로 확장하여 홀수 길이의 팰린드롬을 센다.
- radius를 0부터 시작해, center - radius >= 0, center + radius < n 이고 s[center - radius] == s[center + radius]인 동안 count를 1 증가, radius += 1 한다.
4. 각 문자 쌍(center, center+1)을 중심으로 확장하여 짝수 길이의 팰린드롬을 센다.
- radius를 0부터 시작해, center - radius >= 0, center + radius + 1 < n 이고 s[center - radius] == s[center + radius + 1]인 동안 count를 1 증가, radius += 1 한다.
5. 총 팰린드롬 부분 문자열 개수인 count를 반환한다.
"""
class Solution:
def countSubstrings(self, s: str) -> int:
n = len(s)
count = 0
for center in range(n):
radius = 0
# 홀수 길이 팰린드롬 (center를 기준)
while (
center - radius >= 0
and center + radius < n
and s[center - radius] == s[center + radius]
):
count += 1
radius += 1
radius = 0
# 짝수 길이 팰린드롬 (center, center+1을 기준)
while (
center - radius >= 0
and center + radius + 1 < n
and s[center - radius] == s[center + radius + 1]
):
count += 1
radius += 1
return count- 패턴: Two Pointers, Monotonic Stack, Dynamic Programming
- 설명: 문자열의 중심에서 확장하며 팰린드롬을 탐색하는 방식으로, 홀수/짝수 중심을 각각 확장하는 두 포인터(센터-확장) 기법이다. 각 확장에서 일치 여부를 확인하며 카운트를 증가시키므로 한 위치에서의 비교로 전체를 탐색한다.
📊 시간/공간 복잡도 분석
| 복잡도 | |
|---|---|
| Time | O(N^2) |
| Space | O(1) |
피드백: 가운데를 확장하는 방식으로 모든 홀/짝수 길이 팰린드롬을 탐지한다.
개선 제안: 현재 구현이 적절해 보입니다.
💡 풀이에 시간/공간 복잡도를 주석으로 남겨보세요!
There was a problem hiding this comment.
🏷️ 알고리즘 패턴 분석
reverse-bits/alphaorderly.py
"""
시간복잡도: O(1) # 32번만 반복하므로 항상 상수 시간 복잡도임
공간복잡도: O(1)
1. ans를 0으로 초기화한다.
3. ans를 왼쪽으로 1비트 시프트한 후, n의 마지막 비트를 OR 연산한다.
4. n을 오른쪽으로 1비트 시프트한다.
- 3, 4 과정을 32번 반복한다.
5. ans를 반환한다.
"""
class Solution:
def reverseBits(self, n: int) -> int:
ans = 0
for _ in range(32):
ans = (ans << 1) | (n & 1)
n >>= 1
return ans- 패턴: Bit Manipulation, Divide and Conquer, Greedy
- 설명: 비트 조작으로 각 비트를 좌우로 이동시키며 결과를 구성하는 패턴으로, 반복문 내에서 비트를 shift/OR로 처리하는 비트 연산 기법이다. 입력 비트를 순차적으로 처리해 최종 값을 얻는 방식은 단순하고 명시적이다.
📊 시간/공간 복잡도 분석
| 복잡도 | |
|---|---|
| Time | O(1) |
| Space | O(1) |
피드백: 고정된 비트 수를 반복하며 비트를 뒤집는다.
개선 제안: 현재 구현이 적절해 보입니다.
💡 풀이에 시간/공간 복잡도를 주석으로 남겨보세요!
There was a problem hiding this comment.
🏷️ 알고리즘 패턴 분석
longest-repeating-character-replacement/alphaorderly.py
"""
시간복잡도 : O(N)
공간복잡도 : O(1)
1. freq 딕셔너리를 초기화한다.
2. s의 각 문자(value)에 대해 루프를 돈다.
3. freq[value]를 1 증가시킨다.
4. maxima를 현재 윈도우에서 가장 빈도가 높은 문자 빈도로 갱신한다.
5. 윈도우 크기에서 maxima의 값만큼을 뺀 값이 k보다 크면,
6. freq[s[left]]를 1 감소시키고 left를 1 증가시킨다.
7. ans를 윈도우의 최대 길이로 갱신한다.
8. 마지막으로 ans를 반환한다.
"""
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
freq = defaultdict(int)
# maxima : 현재 윈도우 내에서 가장 많이 등장한 문자의 빈도수
maxima = left = ans = 0
for right, value in enumerate(s):
freq[value] += 1
maxima = max(maxima, freq[value])
while (right - left + 1) - maxima > k:
freq[s[left]] -= 1
left += 1
ans = max(ans, right - left + 1)
return ans
"""
시간복잡도: O(NlogN)
- 세그먼트 트리 한 번 업데이트: O(logN)
- 슬라이딩 윈도우 오른쪽 포인터 한 번씩 전진: O(N)
공간복잡도: O(N)
- 세그먼트 트리 크기: O(N) (알파벳 26개에 대해 4N+1 노드)
[세그먼트 트리 개요]
- 각 알파벳(A~Z)의 빈도수를 표현하는 세그먼트 트리를 구현함.
- 각 트리 노드는 구간 내 현재 알파벳 빈도 총합과 그 구간 내 최빈값(가장 많이 등장한 알파벳의 빈도)을 저장함.
- 이진 트리 구조로 각 알파벳 인덱스를 리프 노드(0~25)에 매핑.
[핵심 동작 설명]
- window.update(문자, ±1): 현재 윈도우에 새 문자를 추가/제거할 때 해당 알파벳의 빈도수를 O(logN)에 갱신.
- window.tree[1][0]: 세그먼트 트리 루트의 첫 번째 값으로, 현재 윈도우 내 전체 문자 개수(윈도우 길이)를 의미.
- window.tree[1][1]: 루트의 두 번째 값으로, 현재 윈도우 내에서 가장 많이 등장한 문자의 등장 횟수를 의미.
- (윈도우 전체길이 - 최빈값) > k 를 만족할 때까지 왼쪽 포인터를 옮기며(=왼쪽 문자 제거), 윈도우가 k개 이하의 문자만 바꾸면 모두 동일하게 만들 수 있는 범위로 축소.
- 매 반복마다 ans를 최대 윈도우 크기로 갱신.
※ 세그먼트 트리 사용은 이 문제에 최적해는 아니나, 자료구조 학습에는 좋은 연습 예제.
"""
class SegTree:
def __init__(self):
# [문자 개수 합, 최대값]
self.tree = [[0, 0] for _ in range(26 * 4 + 1)]
def _update(
self,
node_index: int,
target_index: int,
target_update: int,
seg_left: int,
seg_right: int,
):
if seg_left == seg_right:
self.tree[node_index][0] += target_update
self.tree[node_index][1] += target_update
return
mid = (seg_left + seg_right) // 2
if target_index <= mid:
self._update(node_index * 2, target_index, target_update, seg_left, mid)
else:
self._update(
node_index * 2 + 1, target_index, target_update, mid + 1, seg_right
)
self.tree[node_index][0] = (
self.tree[node_index * 2][0] + self.tree[node_index * 2 + 1][0]
)
self.tree[node_index][1] = max(
self.tree[node_index * 2][1], self.tree[node_index * 2 + 1][1]
)
def update(self, target: str, update: int):
self._update(1, ord(target) - ord("A"), update, 0, 25)
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
left = 0
window = SegTree()
ans = 0
for right, value in enumerate(s):
window.update(value, 1)
while window.tree[1][0] > 0 and window.tree[1][0] - window.tree[1][1] > k:
window.update(s[left], -1)
left += 1
ans = max(ans, right - left + 1)
return ans- 패턴: Sliding Window, Hash Map / Hash Set, Divide and Conquer
- 설명: 첫 번째 구현은 슬라이딩 윈도우로 연속 부분 문자열에서 필요 문자 갱신 횟수를 관리한다. freq 해시 맵으로 문자 빈도를 추적하고 윈도우를 좌우 포인터로 조정하는 패턴이 핵심이다. 두 번째 구현은 세그먼트 트리로 윈도우 내부 빈도와 최빈값을 관리하지만 여전히 윈도우 확장/축소의 흐름은 Sliding Window에 가깝다.
📊 시간/공간 복잡도 분석
| 복잡도 | |
|---|---|
| Time | O(N) |
| Space | O(1) |
피드백: 해당 풀이에서는 슬라이딩 윈도우를 사용해 각 step에서 필요 최소한의 정보를 유지한다. 다만 주석에 제시된 시간/공간 표기가 실제 구현과 다르므로 구현의 동작에 집중하면 된다.
개선 제안: 현재 구현이 적절해 보입니다.
💡 풀이에 시간/공간 복잡도를 주석으로 남겨보세요!
There was a problem hiding this comment.
🏷️ 알고리즘 패턴 분석
longest-repeating-character-replacement/alphaorderly.py
"""
시간복잡도 : O(N)
공간복잡도 : O(1)
1. freq 딕셔너리를 초기화한다.
2. s의 각 문자(value)에 대해 루프를 돈다.
3. freq[value]를 1 증가시킨다.
4. maxima를 현재 윈도우에서 가장 빈도가 높은 문자 빈도로 갱신한다.
5. 윈도우 크기에서 maxima의 값만큼을 뺀 값이 k보다 크면,
6. freq[s[left]]를 1 감소시키고 left를 1 증가시킨다.
7. ans를 윈도우의 최대 길이로 갱신한다.
8. 마지막으로 ans를 반환한다.
"""
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
freq = defaultdict(int)
# maxima : 현재 윈도우 내에서 가장 많이 등장한 문자의 빈도수
maxima = left = ans = 0
for right, value in enumerate(s):
freq[value] += 1
maxima = max(maxima, freq[value])
while (right - left + 1) - maxima > k:
freq[s[left]] -= 1
left += 1
ans = max(ans, right - left + 1)
return ans
"""
시간복잡도: O(NlogN)
- 세그먼트 트리 한 번 업데이트: O(logN)
- 슬라이딩 윈도우 오른쪽 포인터 한 번씩 전진: O(N)
공간복잡도: O(N)
- 세그먼트 트리 크기: O(N) (알파벳 26개에 대해 4N+1 노드)
[세그먼트 트리 개요]
- 각 알파벳(A~Z)의 빈도수를 표현하는 세그먼트 트리를 구현함.
- 각 트리 노드는 구간 내 현재 알파벳 빈도 총합과 그 구간 내 최빈값(가장 많이 등장한 알파벳의 빈도)을 저장함.
- 이진 트리 구조로 각 알파벳 인덱스를 리프 노드(0~25)에 매핑.
[핵심 동작 설명]
- window.update(문자, ±1): 현재 윈도우에 새 문자를 추가/제거할 때 해당 알파벳의 빈도수를 O(logN)에 갱신.
- window.tree[1][0]: 세그먼트 트리 루트의 첫 번째 값으로, 현재 윈도우 내 전체 문자 개수(윈도우 길이)를 의미.
- window.tree[1][1]: 루트의 두 번째 값으로, 현재 윈도우 내에서 가장 많이 등장한 문자의 등장 횟수를 의미.
- (윈도우 전체길이 - 최빈값) > k 를 만족할 때까지 왼쪽 포인터를 옮기며(=왼쪽 문자 제거), 윈도우가 k개 이하의 문자만 바꾸면 모두 동일하게 만들 수 있는 범위로 축소.
- 매 반복마다 ans를 최대 윈도우 크기로 갱신.
※ 세그먼트 트리 사용은 이 문제에 최적해는 아니나, 자료구조 학습에는 좋은 연습 예제.
"""
class SegTree:
def __init__(self):
# summation, largest
self.tree = [[0, 0] for _ in range(26 * 4 + 1)]
def _update(
self,
node_index: int,
target_index: int,
target_update: int,
seg_left: int,
seg_right: int,
):
if seg_left == seg_right:
self.tree[node_index][0] += target_update
self.tree[node_index][1] += target_update
return
mid = (seg_left + seg_right) // 2
if target_index <= mid:
self._update(node_index * 2, target_index, target_update, seg_left, mid)
else:
self._update(
node_index * 2 + 1, target_index, target_update, mid + 1, seg_right
)
self.tree[node_index][0] = (
self.tree[node_index * 2][0] + self.tree[node_index * 2 + 1][0]
)
self.tree[node_index][1] = max(
self.tree[node_index * 2][1], self.tree[node_index * 2 + 1][1]
)
def update(self, target: str, update: int):
self._update(1, ord(target) - ord("A"), update, 0, 25)
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
left = 0
window = SegTree()
ans = 0
for right, value in enumerate(s):
window.update(value, 1)
while window.tree[1][0] - window.tree[1][1] > k:
window.update(s[left], -1)
left += 1
ans = max(ans, right - left + 1)
return ans- 패턴: Sliding Window, Hash Map / Hash Set, Dynamic Programming
- 설명: 첫 번째 코드에서 윈도우 크기를 변동시키며 조건을 만족하는 최대 길이를 찾는 Sliding Window 패턴이다. freq로 문자 빈도를 추적하나 해시 맵/해시셋으로 구현된 핵심 아이디어를 사용한다. 두 번째 코드는 세그먼트 트리로 윈도우 내 총합과 최빈값을 관리하나, 윈도우를 좌우로 확장/축소하는 흐름은 Sliding Window의 확장으로 볼 수 있다.
📊 시간/공간 복잡도 분석
ℹ️ 이 파일에는 2가지 풀이가 포함되어 있어 각각 분석합니다.
풀이 1: Solution.characterReplacement — Time: O(n) / Space: O(1)
| 복잡도 | |
|---|---|
| Time | O(n) |
| Space | O(1) |
피드백: 단일 루프와 상수 개의 추가 변수로 구성되어 시간복잡도는 선형이며, 공간은 고정된 상수 크기의 변수로 구성되어 있다.
개선 제안: 현재 구현이 적절해 보입니다.
풀이 2: SegTree.characterReplacement — Time: O(n log N) / Space: O(N)
| 복잡도 | |
|---|---|
| Time | O(n log N) |
| Space | O(N) |
피드백: 세그먼트 트리로 구현했지만 문제의 최적해는 아닐 수 있으며, 슬라이딩 윈도우의 간단한 구현에 비해 비효율적일 수 있다. 트리 업데이트가 매 문자마다 필요하다.
개선 제안: 고려해볼 만한 대안: 간단한 해시맵+윈도우 방식으로 O(n) 시간과 O(1) 공간에 더 가까운 구현을 우선 시도해볼 것.
💡 풀이에 시간/공간 복잡도를 주석으로 남겨보세요!
|
이번 주차와 상관없는 커밋 로그는 정리 부탁드립니다. |
답안 제출 문제
작성자 체크 리스트
In Review로 설정해주세요.검토자 체크 리스트
Important
본인 답안 제출 뿐만 아니라 다른 분 PR 하나 이상을 반드시 검토를 해주셔야 합니다!