TIl
백트래킹
아크몽
2024. 4. 1. 14:02
백트래킹
완전탐색을 가지치기 하는 것
가능성이 없는(볼 필요 없는) 경우의 수를 제거하는 기법
중복된 순열 등 풀때 필수
재귀함수 ⇒ 특정 시점으로 돌아오는 게 핵심
재귀 함수 팁
파라미터 : 바로 작성 X
구조를 먼저 잡으면 자연스럽게 필요한 변수들이 보임
def dfs(level):
# 기저 조건
# 이 문제에서는 3개를 뽑아을 때 까지 반복
if level == 3:
return
# 들어가기 전 다음 재귀 호출 갔다와서 할 로직
# 기본 코드
'''
path[level] = arr[0]
dfs(level + 1)
path[level] = arr[2]
dfs(level + 1)
'''
# 기본 코드를 반복문으로 변경
# 갈 수 있는 후보군
for i in range(len(arr)):
# 여기는 못가 ! (가지치기)
# 갈 수 없을 때 조건을 거는 것이 조건이 까다로울 수록 좋음
# 조건 1~~~
if arr[i] in path:
continue
if 조건 2:
continue
path[level] = arr[i]
dfs(level+1)
# 갔다와서 할 로직
# 기존 방문을 초기화
path[level] = 0
dfs(0)
- 완전 탐색 경우의 수를 계산해본다.
- 가지치기를 했을 때 대략적인 감소 예측
- 시간 초과가 발생하네? 기저조건, 가지치기를 더 고민하자
트리
싸이클이 없는 무향 연결 그래프
- 싸이클
- 방문했던 노드로 다시 돌아오는 다른 경로가 있는 경우
- 무향
- 간선에 방향이 없다(양방향)
- 연결 그래프
- 모든 꼭지점이 서로 갈 수 있는 경로가 있다.
형제 노드끼리는 연결 되지 않는다 ⇒ 싸이클이 발생하기 때문
노드의 차수 = 후보군의 수
높이는 왜 계산해야될까? 시간복잡도 계산 때문에
⇒ 최악의 경우 차수와 높이가 어떻게 될까 생각해봐야 된다.
이진트리
자식 노드(서브 트리)가 0,1,2인 특별한 형태의 트리
가장 많이 쓰이는 형태
노드의 개수가 N개 일때,
최악의 경우 : 높이 h = N
최선의 경우 : 높이 h = logN
N = $2^{k+1} -1$
완전 이진 트리
높이가 h이고 노드 수가 n개일 때 (단, $2^h <= n <= 2^{h+1}$)
이진 탐색 트리 BST
- 탐색작업을 효율적으로 하기 위한 자료구조
- 모든 원소는 서로 다른 유일한 키를 갖는다.
- key (왼쪽 서브 트리) < key(루트 노드) < key(오른쪽 서브 트리)
- 왼쪽 서브 트리와 오른쪽 서브 트리도 이진 탐색 트리다.
- 중위 순회하면 오름차순으로 정렬된 값을 얻을 수 있다.
탐색 연산
- 루트에서 탐색 시작
- 탐색할 키 값 x를 루트 노드의 키 값 k와 비교
- x=k: 탐색 성공
- x<k : 루트 노드의 왼쪽 서브 트리에 대해서 탐색 연산 수행
- x>k : 루트 노드의 오른쪽 서브 트리에 대해서 탐색 연산 수행
- 서브 트리에 대해서 순환적으로 탐색 연산을 반복한다.
- 탐색 수행할 서브 트리가 없으면 탐색 실패
삽입 연산
- 먼저 탐색 연산을 수행
- 삽입할 원소와 같은 원소가 트리에 있으면 삽입할 수 없으므로, 같은 원소가 트리에 있는지 탐색하여 확인한다.
- 탐색에서 탐색 실패가 결정되는 위치가 삽입 위치가 된다.
- 탐색 실패한 위치에 원소를 삽입한다.
삭제 연산 - 까다로움
- 삭제할 노드가 차수가 0인 리프 노드 : 그냥 찾아서 지운다.
- 리프 노드가 아니고 차수가 1인 경우 : 탐색을 해서 해당 노드를 지워주고, 지운 노드의 자식을 부모에다가 연결시킨다.
- 리프노드가 아니고 차수가 2인 경우 : 왼 쪽 서브 트리 (작은 것들) 중에서 가장 큰 것을 바꾼다.
성능
- 탐색, 삽입, 삭제 시간은 트리의 높이만큼 시간이 걸림 O(h)
- 평균적으로 O(logN)
- 최악의 경우(편향 이진 트리) O(n)
- 순차탐색과 시간복잡도 같음
힙트리 Heap
우선순위큐에 많이 활용된다
형제 끼리는 정렬 보장 되지 않는다.
최대 힙
- 키 값이 가장 큰 노드를 찾기 위한 완전 이진 트리
- 부모 노드의 키 값 > 자식 노드의 키 값
- 루트 노드 : 키 값이 가장 큰 노드
최소 힙
- 키 값이 가장 작은 노드 찾기
- 부모 노드 < 자식 나도
- 루트 노드 : 키 값이 가장 낮은 노드
삽입 $logN 또는 O(n)$
일단 마지막에 넣는다. → 순서 변경하면서 자리 찾아간다
삭제
- 힙에서는 루트 노드의 원소만을 삭제 할 수 있다.
- 루트 노드의 원소를 삭제해서 반환한다. ⇒ 하지만 루트 노드를 갑자기 삭제하면 서브 트리가 두 개가 나와버림 = 리스트 두 개를 합치는 형식 = 자료구조 관리 어려움
- 일단 루트 노드와 마지막 노드를 바꾸고, 그리고 마지막 노드를 삭제 시킨다. = pop() + 위치 찾기
- 힙의 종류에 따라 최대 값 or 최소 값을 구할 수 있다.
힙의 활용
- 힙을 활용하는 대표적인 2가지 예는 특별한 큐의 구현과 정렬
- 우선순위 큐를 구현하는 가장 효율적인 방법이 힙을 사용하는 것이다.
- 노드 하나의 추가 / 삭제가 시간 복잡도가 $O(logN)$ 이고 최대값/최소값을 O(1)에 구할 수 있다.
- 완전 정렬보다 관리 빙용이 적다.
힙 트리를 다룰 때 조심해야 되는 것
- 형제 노드끼리는 정렬이 보장 되지 않음 ⇒ 디버깅시 리스트를 잘 봐야함
- 삽입 시 완전이진트리의 형태를 유지하기 위해 마지막에 삽입 → 순서 변경하면서 자리 찾아감