# 9-1 트리 알아보기
트리란?
- 노드와 에지로 연결된 그래프의 특수한 형태
- 순환 구조(cycle)를 지니고 있지 않고, 1개의 루트 노드가 존재한다.
- 루트 노드를 제외한 노드는 단 1개의 부모 노드를 갖는다.
- 트리의 부분 트리(subtree) 역시 트리의 모든 특징을 따른다.
트리의 핵심 이론(구성 요소)
- 노드 : 데이터의 index와 value를 표현하는 요소
- 에지 : 노드와 노드의 연결 관계를 나타내는 선
- 루트 노드 : 트리에서 가장 상위에 존재하는 노드
- 부모 노드 : 두 노드 사이의 관계에서 상위 노드에 해당하는 노드
- 자식 노드 : 두 노드 사이의 관계에서 하위 노드에 해당하는 노드
- 리프 노드 : 트리에서 가장 하위에 존재하는 노드(자식 노드가 없는 노드)
- 서브 트리 : 전체 트리에 속한 작은 트리
https://www.acmicpc.net/problem/11725
import sys
sys.setrecursionlimit(10**6)
input = sys.stdin.readline
N = int(input())
visited = [False]*(N+1)
tree = [[] for _ in range(N+1)]
answer= [0]*(N+1)
for _ in range(1, N):
n1, n2 = map(int, input().split())
tree[n1].append(n2)
tree[n2].append(n1)
# DFS 탐색 함수
def DFS(number):
visited[number] = True
for i in tree[number]:
if not visited[i]:
answer[i]=number # DFS를 수행하면서 부모 노드를 정답 리스트에 저장
DFS(i)
DFS(1) # 부모 노드부터 DFS 시작
for i in range(2, N+1):
print(answer[i])
- 인접 리스트로 트리 데이터를 구현한다.
- DFS 탐색을 수행한다. 수행할 때 부모 노드의 값을 정답 리스트에 저장한다.
- 정답 리스트의 2번 인덱스 부터 값을 차례대로 출력한다.(answer[1]부터)
https://www.acmicpc.net/problem/1068
import sys
sys.setrecursionlimit(10**6)
input = sys.stdin.readline
N = int(input())
visited = [False]*(N)
tree = [[] for _ in range(N+1)]
answer= 0
p = list(map(int, input().split()))
for i in range(N):
if p[i]!=-1:
tree[i].append(p[i])
tree[p[i]].append(i)
else:
root = i
# DFS 탐색 함수
def DFS(number):
global answer
visited[number]=True
cNode = 0
for i in tree[number]:
if not visited[i] and i!=deleteNode: # 삭제 노드일 때도 탐색 중지
cNode+=1
DFS(i)
if cNode==0:
answer +=1 # 자식 노드 수가 0개일 때 리프 노드로 간주하고 정답 값 증가
deleteNode = int(input())
if deleteNode==root:
print(0)
else:
DFS(root)
print(answer)
- 인접 리스트로 트리 데이터 구현하기
- DFS 또는 BFS 탐색을 수행하면서 리프 노드의 개수를 센다. 단, 제거 대상 노드(deleteNode)를 만났을 때는 그 아래 자식 노드들과 관련된 탐색은 중지한다.(리프 노드제거)
# 9-2. 트리아
트라이(trie)란?
- 문자열 검색을 빠르게 실행할 수 있도록 설계한 트리 형태의 자료구조
트라이의 핵심 이론
- 일반적으로 단어들을 사전의 형태로 생성한 후 트리의 부모 자식 노드 관계를 이용해 검색을 수행한다.
- N진 트리 : 문자 종류의 개수에 따라 N이 결정된다. 예를 들어 알파벳은 26개의 문자로 이뤄져 있으므로 26진 트리로 결정된다.
- 루트 노드는 항상 빈 문자열을 뜻하는 공백 상태를 유지한다.
https://www.acmicpc.net/problem/14425
from sys import stdin
from typing import Any
input = stdin.readline
class Node(object):
def __init__(self, isEnd):
self.isEnd = isEnd
self.childNode = {}
class Trie(object):
def __init__(self):
self.parent = Node(None)
# 문자 삽입
def insert(self, string):
nowNode = self.parent
temp_length = 0
for char in string:
# 자식 Node들 미완성된 문자열이면 새로 생성
if char not in nowNode.childNode:
nowNode.childNode[char] = Node(char)
# 자식 노드로 이동
nowNode = nowNode.childNode[char]
temp_length += 1
if temp_length ==len(string):
nowNode.isEnd = True
# 문자열이 존재하는지 탐색
def search(self, string):
nowNode = self.parent
temp_length = 0
for char in string:
if char in nowNode.childNode:
nowNode = nowNode.childNode[char]
temp_length += 1
if temp_length == len(string) and nowNode.isEnd==True:
return True
else:
return False
return False
N, M = map(int, input().split())
myTrie = Trie() # 트리 생성
for _ in range(N):
word = input().strip()
myTrie.insert(word) # 단어 삽입
result = 0
for _ in range(M):
word = input().strip()
if myTrie.search(word): # 단어 찾기
result += 1
print(result)
- 트라이 자료구조를 생성한다. 현재 문자열을 가리키는 위치의 노드가 공백 상태라면 신규 노드를 생성하고, 아니라면 이동한다. 문자열의 마지막에 도달하면 리프 노드라고 표시한다(isEnd == True)
- 트라이 자료구조 검색으로 집합 S에 포함된 문자열을 센다. 부모-자식 관계 구조를 이용해 대상 문자열을 검색했을 때 문자열이 끝날 때까지 공백 상태가 없고, 현재 문자의 마지막 노드가 트라이의 리프 노드라면 집합 S에 포함된 문자열로 센다.
- 하지만 시간 초과 난다...
import sys
input = sys.stdin.readline
n, m = map(int, input().split())
textlist = set([input() for _ in range(n)]) # set 형태로 집합 S 문자열 저장
# print(textlist)
# {'codeplus\n', 'baekjoononlinejudge\n', 'sundaycoding\n', 'startlink\n', 'codingsh\n'}
count = 0
for _ in range(m): # set 형태의 집합 문자열에서 검사 문자열이 있는지 바로 확인
subtext = input()
if subtext in textlist:
count += 1
print(count)
- set 자료구조를 이용해 간단하게 풀 수 있다.
# 9-3. 이진 트리
이진 트리(binary tree)란?
- 각 노드의 자식 노드(차수)의 개수가 2 이하로 구성돼 있는 트리. 가장 많이 사용되는 형태이다.
이진 트리의 핵심 이론
- 이진 트리의 종류
- 편향 이진 트리 : 노드들이 한쪽으로 편향돼 생성된 이진 트리
- 포화 이진 트리 : 트리의 높이가 모두 일정하며 리프 노드가 꽉찬 이진 트리
- 완전 이진 트리 : 마지막 레벨을 제외하고 완전하게 노드들이 채워져 있고, 마지막 레밸을 왼쪽부터 채워진 트리
- 일반적으로 완전 이진 트리의 형태로 데이터를 담는다.
- 이진 트리의 순차 표현
- 이진 트리는 1차원 리스트의 형태로 표현할 수 있다. 왼쪽부터 차례대로 노드에 인덱스(1부터)를 붙이고 해당하는 노드의 value를 저장한다. 빈 노드도 리스트는 생성한다.
- 트리의 노드와 리스트의 인덱스 사이 상관 관계(제약 조건)
- 루트 노드 : index=1
- 부모 노드 : index = index/2 (현재 노드가 루트 노드가 아닌경우)
- 왼쪽 자식 노드 : index = index*2 (index*2 <=N)
- 오른쪽 자식 노드 : index = index*2 + 1 (index*2 + 1 <=N)
- 인덱스 연산 방식은 세그먼트 트리 혹은 LCA 알고리즘에서 기본적으로 사용된다.
https://www.acmicpc.net/problem/1991
import sys
input = sys.stdin.readline
N = int(input())
tree = {} # 딕셔너리 형태로 선언
for _ in range(N):
root, left, right = input().split()
tree[root] = [left, right]
def preOrder(now):
if now == '.':
return
print(now, end = '') # 1. 현재 노드
preOrder(tree[now][0]) # 2. 왼쪽 탐색
preOrder(tree[now][1]) # 3. 오른쪽 탐색
def inOrder(now):
if now == '.':
return
inOrder(tree[now][0]) # 1. 왼쪽 탐색
print(now, end ='') # 2. 현재 노드
inOrder(tree[now][1]) # 3. 오른쪽 탐색
def postOrder(now):
if now == '.':
return
postOrder(tree[now][0]) # 1. 왼쪽 탐색
postOrder(tree[now][1]) # 2. 오른쪽 탐색
print(now, end='') # 3. 현재 노드
preOrder('A')
print()
inOrder('A')
print()
postOrder('A')
- 딕셔너리 자료형에 트리 데이터를 저장한다.
- 전위 순회 함수(preOrder), 중위 순회 함수(inOrder), 후위 순회 함수(postOrder)를 구현해 실행한다.
# 9-4. 세그먼트 트리
세그먼트 트리(segement tree)란?
- 데이터의 구간 합과 데이터 업데이트를 빠르게 수행하기 위해 고안해낸 자료구조의 형태. '인덱스 트리' 라고도 한다.
세그먼트 트리의 핵심 이론
- 세그먼트 트리의 정류는 구간 합, 최대/최소 구하기로 나눌 수 있고, 구현 단계는 트리 초기화 하기, 질의값 구하기(구간합 or 최대/최소), 데이터 업데이트 하기로 나눌 수 있다.
- 트리 초기화 하기
- 리프 노드의 개수가 데이터의 개수(N) 이상이 되도록 트리 리스트를 만든다.
- 트리 리스트의 크리 : 2^k >= N을 만족하는 k의 최솟값을 구한 후 2^k * 2를 틀 리스트의 크기로 정의한다.
- 리프 노드에 원본 데이터를 입력한다. 이때 리프 노드의 시작 위치를 트리 리스트의 인덱스로 구해야 하는데, 2^k를 시작 인덱스로 취한다.
- 리프 노드를 제외한 나머지 노드의 값을 채운다.(2^k-1부터 1번 쪽으로) 자신의 자식 노드를 이용해 해당 값을 채울 수 있다. 자식 노드의 인덱스는 2N, 2N+1이 된다.
- 질의값 구하기
- 주어진 질의 인덱스를 세그먼트 트리의 리프 노드에 해당하는 인덱스로 변경한다. 기존 샘플을 기준으로 한 인덱스 값과 세그먼트 트리 리스트에서의 인덱스 값이 다르기 때문에 인덱스를 변경해야 한다.
- 세그먼트 트리 index = 주어진 질의 index + 2^k - 1
- 질의에서 시작 인덱스와 종료 인덱스에 관래 부모 노드로 이동하면서 주어진 질의에 해당하는 값을 구한다.
- start_index % 2 ==1일 때 해당 노드 선택
- end_index % 2 ==1일때 해당 노드 선택
- start_index depth 변경 : start_index = (start_index + 1)/2 연산 실행
- end_index depth 변경 : end_index = (endx_index -1)/2 연산 실행
- 위 과정을 반복하다 end_index < start_index가 되면 종료한다.
- 구간 합은 선택된 노드들을 모두 더하고, 최댓값/최솟값은 선택된 노드들 중 MAX/MIN값을 선택해 출력한다.
- 데이터 업데이트 하기
- 자신의 부모 노드로 이동하면서 업데이트 한다.
- 구간 합에서는 원래 데이터와 변경 데이터의 차이만큼 부모 노드로 올라가면서 변경한다.
- 최댓값/최솟값 찾기에서는 변경 데이터와 자신과 같은 부모를 지니고 있는 다른 자식 노드와 비교해 더 큰 값/작은 값으로 업데이트 한다. 업데이트가 일어나지 않으면 종료한다.
https://www.acmicpc.net/problem/2042
- 데이터의 변경이 많이 일어나므로 합 배열 자료구조가 아닌 트리 자료구조를 이용한다.
import sys
input = sys.stdin.readline
# 수의 개수, 변경이 일어나는 횟수, 구간 합을 구하는 횟수
N, M, K = map(int, input().split())
treeHeight = 0
length = N
while length!=0: # 트리 높이 구하기
length//=2
treeHeight += 1
treeSize = pow(2, treeHeight+1)
leftNodeStartIndex = treeSize // 2 -1 # 리프 노드 시작 인덱스
tree = [0]*(treeSize+1)
# 데이터를 리프 노드에 저장
for i in range(leftNodeStartIndex+1, leftNodeStartIndex+N+1):
tree[i] = int(input())
# 인덱스 트리 생성 함수
def setTree(i):
while i!=1:
tree[i//2]+=tree[i]
i-=1
setTree(treeSize-1) # 초기 트리 생성
# 값 변경 함수
def changeVal(index, value):
diff = value - tree[index]
while index > 0:
tree[index] = tree[index]+diff
index = index//2
# 구간 합 개선 함수
def getSum(s, e):
partSum = 0
while s<= e:
if s%2 == 1:
partSum += tree[s]
s+=1
if e%2 == 0:
partSum += tree[e]
e-=1
s = s//2
e = e//2
return partSum
for _ in range(M+K):
question, s, e = map(int, input().split())
if question==1:
changeVal(leftNodeStartIndex+s, e)
elif question==2:
s = s+leftNodeStartIndex
e = e+leftNodeStartIndex
print(getSum(s, e))
- 1차원 리스트를 이용해 트리의 값을 초기화한다. 트리 리스트 크기 N=5 이므로 2^k >=N을 만족하는 k의 값은 3이고, 리스트의 크기는 2^3*2 = 16이 된다. 시작 인덱스는 2^3=start_index=8이다.
- 질의값 연산 함수와 데이터 업데이트 함수를 수행하고, 질의와 관련된 결괏값을 출력한다.
https://www.acmicpc.net/problem/10868
import sys
input = sys.stdin.readline
# 수의 개수, 최솟값을 구하는 횟수
N, M = map(int, input().split())
treeHeight = 0
length = N
while length!=0:
length//=2
treeHeight+=1
treeSize = pow(2, treeHeight+1)
leftNodeStartIndex = treeSize // 2 -1 # 리프 노드 시작 인덱스
tree = [sys.maxsize]*(treeSize+1)
# 데이터를 리프 노드에 저장
for i in range(leftNodeStartIndex+1, leftNodeStartIndex+N+1):
tree[i] = int(input())
# 인덱스 트리 생성 함수
def setTree(i):
while i!=1:
if tree[i//2]>tree[i]:
tree[i//2]=tree[i]
i-=1
setTree(treeSize-1)
#최솟값 계산 함수
def getMin(s,e):
Min = sys.maxsize
while s<=e:
if s%2==1:
Min=min(Min, tree[s])
s+=1
if e%2==0:
Min = min(Min, tree[e])
e-=1
s=s//2
e=e//2
return Min
for _ in range(M):
s, e = map(int, input().split())
s = s+leftNodeStartIndex
e = e+leftNodeStartIndex
print(getMin(s, e))
- 1차원 리스트로 트리의 값을 최솟값 기준으로 초기화한다. 트리 리스트 크기가 N=10이므로 2^k>=N을 만족하는 k=4이고, 리스트의 크기는 2^4*2=32가 된다. 시작 index=start_index=2^4=16이다.
- 질의값 연산 함수를 수행하고, 결괏값을 출력한다.
- 질의가 (1,10)인 경우 Tree[1]값 출력(5), 질의가 (3,5)인 경우 tree[9], tree[20] 중 최솟값 출력 등
https://www.acmicpc.net/problem/11505
import sys
input = sys.stdin.readline
# 수의 개수, 변경이 일어나는 횟수, 구간 곱을 구하는 횟수
N, M, K = map(int, input().split())
treeHeight = 0
length = N
MOD = 1000000007
while length!=0: # 트리 높이 구하기
length//=2
treeHeight+=1
treeSize = pow(2, treeHeight+1)
leftNodeStartIndex = treeSize // 2 -1 # 리프 노드 시작 인덱스
tree = [1]*(treeSize+1) # 곱하기 이므로 초기값ㅇ은 1 설정
# 데이터를 리프 노드에 저장
for i in range(leftNodeStartIndex+1, leftNodeStartIndex+N+1):
tree[i] = int(input())
# 인덱스 트리 생성 함수
def setTree(i):
while i!=1:
tree[i//2]=tree[i//2]*tree[i]%MOD
i-=1
setTree(treeSize-1)
# 값 변경 함수
def changeVal(index, value):
tree[index] = value
while index>1:
index=index//2
tree[index]=tree[index*2]%MOD*tree[index*2+1]%MOD
# 구간 곱 계산 함수
def getMul(s,e):
partMul = 1
while s<=e:
if s%2==1:
partMul = partMul*tree[s]%MOD
s+=1
if e%2==0:
partMul=partMul*tree[e]%MOD
e-=1
s=s//2
e=e//2
return partMul
for _ in range(M+K):
question, s, e = map(int, input().split())
if question ==1:
changeVal(leftNodeStartIndex+s, e)
elif question==2:
s = s+leftNodeStartIndex
e = e+leftNodeStartIndex
print(getMul(s, e))
- 1차원리스트로 트리의 값을 초기화 한다. 트리 리스트 크기 N=5, 2^k>=N을 만족하는 k=3이다. 따라서 리스트의 크기는 2^3*2=16이고, 시작 인덱스는 2^3=8이 된다.
- 곱셈 연산이므로 초깃값을 1로 저장해주고, 부모 노드를 양쪽 자식 노드의 곱으로 표현한다. MOD 연산을 수행해 주어진 범위를 넘지 않도록 한다.
- 질의값 연산 함수와 데이터 업데이트 함수를 수행하고 결괏값을 출력한다. 이때 연산마다 모두 MOD 연산을 수행한다.
- (A*B)%C = (A%C)*(B%C)%C 성질을 이용
# 최소 공통 조상
최소 공통 조상(LCA, Lowest Common Ancestor)이란?
- 트리 그래프에서 임의의 두 노드를 선택했을 때 두 노드가 각각 자신을 포함해 거슬러 올라가면서 부모 노드를 탐색할 때 처음 공통으로 만나게 되는 부모 노드
최소 공통 조상 핵심 이론
- 일반적인 최소 공통 조상 구하기
- 트리의 높이가 크지 않을 때, 루트 노드에서 탐색(DFS/BFS)을 시작해 각 노드의 부모 노드와 깊이를 저장한다.
- 선택된 두 노드의 깊이가 다른 경우, 더 깊은 노드의 노드를 부모 노드로 1개씩 올려주면서 같은 깊이로 맞춘다. 이때 두 노드가 같으면 해당 노드가 LCA이므로 탐색을 종료한다.
- 깊이가 같은 상태에서는 동시에 부모 노드로 올라가면서 같은 노드가 될 때까지 반복한다.
- 이 방식은 트리의 높이가 커질 경우 시간 제약 문제에 직면할 수 있다.
- 최소 공통 조상 빠르게 구하기
- 서로의 깊이를 맞춰주거나 같아지는 노드를 찾을 때 기존에 한 단계씩이 아닌 2^k씩 올라가 비교하는 방식.
- 따라서 기존에 자신의 부모 노드만 저장해 놓던 방식에서 2^k번째 위치의 부모 노드까지 저장해야 한다.
- 먼저 부모 노드 저장 리스트를 만든다.
- 정의 : P[K][N] = N번 노드의 2^K번째 부모의 노드 번호
- 점화식 : P[K][N] = P[K-1][P[K-1][N]]
- 리스트에서 K는 트리의 깊이 > 2^K를 만족하는 최댓값이다.
- 리스트 값을 채운 후 선택된 두 노드의 깊이를 맞춘다.(높이 차가 0이 될 때까지)
- 최소 공통 조상을 찾는다. 이 작업 역시 2^k 단위로 점프하며 맞춘다.
- 먼저 부모 노드 저장 리스트를 만든다.
https://www.acmicpc.net/problem/11437
import sys
from collections import deque
input = sys.stdin.readline
print = sys.stdout.write
N = int(input())
tree = [[] for _ in range(N+1)]
for _ in range(0, N-1): # 인접 리스트에 트리 데이터 저장
s, e = map(int, input().split())
tree[s].append(e)
tree[e].append(s)
depth = [0]*(N+1)
parent = [0]*(N+1)
visited = [False]*(N+1)
def BFS(node):
queue = deque()
queue.append(node)
visited[node]=True
level = 1
now_size = 1
count = 0
while queue:
now_node = queue.popleft()
for next in tree[now_node]:
if not visited[next]:
visited[next] = True
queue.append(next)
parent[next] = now_node # 부모 노드 저장
depth[next] = level # 노드 depth 저장
count += 1
if count == now_size:
count = 0
now_size=len(queue)
level+=1
BFS(1)
def executeLCA(a,b):
if depth[a]<depth[b]:
a, b = b, a
while depth[a]!=depth[b]: # depth 맞추기
a = parent[a]
while a!=b: # 공통 조상 찾기
a = parent[a]
b = parent[b]
return a
M = int(input())
for _ in range(M):
a,b = map(int, input().split())
print(str(executeLCA(a,b)))
print("\n")
- 인접 리스트로 트리 데이터를 구현한다.
- 탐색 알고리즘(BFS)를 이용해 각 노드의 깊이를 구한다.
- 깊이를 맞추기 위해 더 깊은 노드를 같은 깊이가 될 때까지 부모 노드로 이동한다.
- 부모 노드로 계속 올라가면서 LCA를 찾는다. 두 노드가 같아질 때의 노드가 LCA가 된다.
- Python으로 제출하면 아슬아슬하게 시간 초과가...난다. PyPy3으로 제출함.
https://www.acmicpc.net/problem/11438
import sys
from collections import deque
input = sys.stdin.readline
print = sys.stdout.write
N = int(input())
tree = [[0] for _ in range(N+1)]
for _ in range(0, N-1): # 인접 리스트에 트리 데이터 저장
s, e = map(int, input().split())
tree[s].append(e)
tree[e].append(s)
depth = [0]*(N+1)
visited = [False]*(N+1)
temp = 1
kmax = 0
while temp<=N: # 최대 가능 depth 구하기
temp<<=1 # 비트 단위 연산자 입니다..temp=temp*2^1
kmax+=1
parent = [[0 for j in range(N+1)] for i in range(kmax+1)]
def BFS(node):
queue = deque()
queue.append(node)
visited[node]=True
level = 1
now_size = 1
count = 0
while queue:
now_node = queue.popleft()
for next in tree[now_node]:
if not visited[next]:
visited[next] = True
queue.append(next)
parent[0][next] = now_node # 부모 노드 저장
depth[next] = level # 노드 depth 저장
count += 1
if count == now_size:
count = 0
now_size=len(queue)
level+=1
BFS(1)
for k in range(1, kmax+1):
for n in range(1, N+1):
parent[k][n] = parent[k-1][parent[k-1][n]]
def executeLCA(a,b):
if depth[a]>depth[b]: # 더 깊은 depth가 b가 되도록
temp=a
a=b
b=temp
for k in range(kmax, -1, -1): # depth 빠르게 맞추기
if pow(2, k) <= depth[b]-depth[a]:
if depth[a]<=depth[parent[k][b]]:
b = parent[k][b]
for k in range(kmax, -1, -1): # 조상 빠르게 찾기
if a==b: break
if parent[k][a]!=parent[k][b]:
a = parent[k][a]
b = parent[k][b]
LCA = a
if a!=b:
LCA = parent[0][LCA]
return LCA
M = int(input())
for _ in range(M):
a,b = map(int, input().split())
print(str(executeLCA(a,b)))
print("\n")
- 이전 문제보다 노드의 개수와 질의가 커졌으므로 제곱수 형태를 이용해 LCA를 구한다.
- 먼저 인접 리스트로 트리 데이터를 구한다.
- 탐색 알고리즘(BFS)을 이용해 각 노드의 깊이를 구한다.
- 점화식을 이용해 Parent 리스트(부모 노드 리스트)를 구한다.
- P[K][N] = P[K-1][P[K-1][N]]
- 부모 노드로 올라가며 최소 공통 조상을 찾는다. 이때 Parent 리스트를 이용해 2^k만큼 넘어가면서 찾으며, k는 depth의 최댓값에서 1씩 감소한다.
# 회고
어렵..어떻게 1차원 리스트에 저렇게 트리를 저장하고...풀이 공식을 만들 수 있는지 신기하다.
개인적으로 이 책 공부하기 전에 python 개념을 좀 완벽하게 하는게 좋지 않을까 생각했다.
갑작스러운 python 내장 함수의 사용이 있따. 이번에는 pow나 비트 연산자. pow도 오랜만에 봐서 뭔가 했다...제곱 함수...
그리고 이런 내장 함수는 잘 사용하다가 python의 다른 편리한 기능들은 안쓴다. 왜지?
'코딩테스트' 카테고리의 다른 글
[Python] 트리 문제 풀이 정리 (0) | 2024.05.21 |
---|---|
[Python] 그래프 문제 풀이 정리(1) (0) | 2024.05.19 |
[Python] 그래프(2) (0) | 2024.05.12 |
[Python] 그래프(1) (1) | 2024.05.10 |
[Python] 그리디 문제 풀이 정리(2) (0) | 2024.05.07 |