알고리즘
- 완전 탐색(Exhaustive Search)
- 가능한 경우를 모두 구해서 문제의 해결 방법을 찾는 것
- 실행시간이 너무 길어 제한 시간 내에 문제를 해결할 수 없는 경우가 많다
- Brute-force search
- N 중 반복문을 이용하는 방법
- 큐를 이용하는 방법
- 순열을 이용하는 방법 (재귀 호출로 가능)
- 재귀 호출을 이용하는 방법
- 완전 탐색을 구현하기 용이
- 다른 여러 알고리즘에서 사용
- 코드가 간결하고 구현이 직관적임
- 특징: 쉬운 문제를 어렵게 풀어야 한다
- 특징: 어려운 문제를 쉽게 풀 수 있다
- 설계
- 1. 문제해결을 위해 수행해야 하는 작업을
- 2. 유사한 형태의 여러 조각으로 쪼갠다
- 3. 그 중 한 조각을 수행
- 4. 나머지는 자기 자신을 호출해서 해결
- 5. 기저사례(base case)를 재귀 함수에 추가
- 기저 사례(base case)
- 더 이상 쪼갤 수 없는 마지막 조각에 도달한 경우
- 재귀 호출을 종료하는 조건
- 예: 1 부터 N 까지의 합, f(n) = f(n-1) + n;
- 탐색 공간의 배제
- 전체 탐색 알고리즘을 구현하는 데 있어서 더 이상 탐색하지 않더라도 해를 구하는데 문제가 없는 부분을 판단하여 이 부분에 대해서 탐색을 하지 않도록 하여 탐색의 효율을 높이고자 하는 방법
- 수학적 배제
- 이분탐색 알고리즘
- 수학적으로 공간을 배제해 가는 이 방법은 일종의 탐욕법(greedy)
- 경험적 배제
- 처음 시작은 전체탐색과 마찬가지로 해가 될 수 있는 모든 공간을 탐색해 간다. 차이점은 특정 조건을 두고, 이 조건을 기준으로 다음 상태를 계속 탐색할지의 여부를 결정
- 특정 조건이란 더 이상 탐색하더라도 해를 구할 수 없음을 판단할 수 있는 조건을 말한다
- 알고리즘이 시작될 때는 이 조건을 설정할 수 없고, 탐색을 진행하는 중에 조건을 설정하고, 상황에 따라서 조건이 갱신된다. 탐색한 정보, 즉 경험한 정보를 이용해서 배제할 조건을 정하기 때문에 경험적 배제라고 한다
- 일반적으로 가지치기(branch & bound)라고 한다
- 어떤 조건에 의해서 더 탐색할 공간이 있음에도 불구하고 돌아오는 흐름을 바운딩(bounding) 혹은 커팅(cutting)이라고 한다
- 구조적 배제
- 해결 전략에 따른 구현법
- 백트래킹(Backtracking)
- 주어진 답이 나올 때까지 검색하다가 중간에 확실히 이 방향이 아니면 다시 이전 상태로 돌아가서 경로를 찾아보는 방법
- 검색에 제한을 걸 수 있는 조건이 문제에 주어져야 함
- 특정 알고리즘이 아닌 문제를 해결하는 하나의 방법
- 가지치기(Pruning) 이라는 방법으로 경우의 수를 줄인다
- 재귀(DFS), 큐(BFS)
- 분할정복(Divide and Conquer)
- 주어진 문제를 더 작은 문제로 나누어 푸는 방법
- 문제를 더 이상 나누지 않고 풀 수 있을 때까지 나눈다
- 부분문제의 답을 이용하여 원래 문제의 답을 구한다
- DP 와의 차이점은 중복 호출을 하지 않는다
- DP 는 중복호출이 일어나 Memoization 을 필요로 하는 경우를 말한다
- 요소: 분할(divide) / 병합(merge) / 기저 케이스(base case)
- 루프, 재귀
- 병합 정렬(Merge Sort)
- 구현하기 쉬운 대표적인 정렬
- 최악의 경우에도 O(NlgN) 복잡도
- N만큼의 버퍼공간이 추가로 필요
- 병합용도
- 분할 정복으로 구현
- 분할 정복 문제는 어떻게 함수를 만들어야 할지 결정해야 한다
- 함수->문제를 푸는함수
- 작은 문제를 어떻게 호출해야 할지 결정
- 부분 문제의 정답을 합쳐야 하는 경우에는 합치는 것을 어떻게 해야 할지 결정
- 이진탐색
- 정렬된 구조에서 목적값의 위치를 찾는 방법
- 탐색 범위를 반으로 줄여가면서 찾는다
- 분할 정복의 한 갈래
- N/2 로 쪼개 둘 중 하나만 호출한다
- DFS (2차원 좌표계에서 DFS 하기)
- 최적화 문제(Optimization Problem)
- 문제의 답이 하나가 아니라 여러 개이고, 그 중에서 어떤 기준에 따라 가장 '좋은' 답을 찾아 내는 문제들
- 해결하기 위한 여러 방법들 중에서 가장 기초적인 것이 완전 탐색
- 시간 안에 답을 구할 수 있는지 확인하는 것이 먼저
- 문제의 특성을 이용해 단순화할 필요가 있다
- 가장 유명한 문제
- 여행하는 외판원 문제(Traveling Sales-man Problem, TSP)
- 이분탐색
- 일반적으로 최솟값의 최대화, 최댓값의 최소화 등의 문제에 이용할 수 있는 경우가 많다
- 일반적으로 최적화 문제를 결정 문제로 바꿔서 생각
- 결정 문제의 참/거짓의 결과를 이용하여 이분탐색으로 가장 최적의 해를 탐색하는 방법으로 해를 구한다
- Binary Search
- 정렬되어 있는 리스트에서 어떤 값을 빠르게 찾는 알고리즘
- 리스트의 크기를 N 이라고 할 때 lgN 의 시간이 필요 (절반으로 나누기 때문)
- Merge Sort
- N 개를 정렬하는 경우
- N개를 N/2, N/2 개로 나누고
- 왼쪽과 오른쪽을 정렬하고
- 두 정렬한 결과를 하나로 합친다
- 다이나믹 프로그래밍(Dynamic Programming)
- DP, 동적 계획법
- Overlapping Subproblem
- 큰 문제와 작은 문제를 같은 방법으로 풀 수 있다
- 문제를 작은 문제로 쪼갤 수 있다
- Optimal Substructure
- 문제의 정답을 작은 문제의 정답에서 구할 수 있다
- 두 번 이상 반복 계산되는 부분 문제들의 답을 미리 저장함으로써 속도의 향상을 꾀하는 알고리즘 설계 기법
- 기본 전략은 문제를 작은 부분 문제로 나누는 것에서 부터 시작한다
- 작은 부분 문제를 해결한 결과로 큰 문제를 해결할 수 있다
- 특징은 중복되는 부분 문제들이 있다는 것
- 적용 방법
- 문제를 더 작은 부분 문제로 나눈다
- 중복되는 작은 부분 문제는 한 번만 계산해서 그 결과를 저장
- 중복되는 작은 부분 문제들은 여러 번 계산하지 않고 저장한 값을 이용
- 메모이제이션(memoization)
- 예) 재귀 함수 + 메모이제이션
- 같은 입력에 대해 결과가 항상 같아야 한다
- 같은 입력의 재귀 함수가 (매우 많이) 중복해서 호출된다
- 예, 피보나치 수
- 구현 방법
- 재귀 호출을 이용하는 방법
- Top Down
- 문제를 작은 문제로 쪼개고, 그 중 한 조각을 해결한 다음 나머지 작은 문제를 같은 방법으로 해결한다
- 장단점
- 구현이 직관적이고 이해하기 쉽다
- 전체 부분 문제 중 일부의 답만 필요할 경우 더 빠르게 동작한다
- Sliding window 를 사용할 수 없다
- 스택 오버플로우를 조심해야 한다
- Code pattern (문제해결전략 참조)
- 반복문을 이용하는 방법
- Bottom Up
- 문제 해결에 필요한 가장 작은 문제를 해결하고, 문제를 조금씩 크게 만들어 가면서 해결한다
- 장단점
- 구현이 대개 더 짧다
- 재귀 호출 보다 조금 더 빠르다
- Sliding window 를 사용할 수 있다
- 구현이 비직관적이고, 이해하기 어렵다
- 부분 문제들을 해결하는 순서에 신경을 써야 한다
- 가장 유명한 예는 이항 계수(binomial coefficient)의 계산
- 이항 계수 C(n, r) 는 n 개의 서로 다른 원소 중에 r 개의 원소를 순서 없이 골라내는 방법의 수를 나타내는 것
- 점화식: C(n, k) = C(n - 1, k - 1) + C(n-1, k)
- 함수의 반환 값이 그 입력 값만으로 결정되는 참조적 투명 함수(referential transparent function)의 경우에만 메모이제이션이 적용가능하다
- 그래프
- 대상(object)사이의 관계(relation)를 나타내는 자료구조
- 대상 -> 정점(Vertex or node)
- 관계 -> 간선(Edge)
- 인접(Adjacent)
- 간선으로 연결되어 있는 두 정점을 일컫는 말 (이웃 관계)
- 정점의 차수(degree) -> 정점에 연결되어 있는 간선의 개수 / 인접하는 정점의 수
- 경로(Path)
- 경로 길이 -> 경로를 구성하는 간선의 수
- 싸이클(Cycle)
- 두 정점간의 경로에서 동일한 정점을 두 번 이상 거치는 경로
- 연결성(Connectivity)
- 무방향성 그래프 내에서 두 정점 사이에 경로가 존재
- 그래프는 정점의 모음과 이 정점을 잇는 간선의 모음간의 결합
- 정점의 집합을 V, 간섭의 집합을 E, 그래프를 G 라고 했을 때
- G = (V, E) 이다.
- 간선의 종류에 따라
- 무방향 그래프(undirected graph) : 양방향 (A, B) = (B, A)
- 방향 그래프(directed graph) : 방향성이 존재 <A, B> ≠ <B, A>
- 가중치 그래프(weighted graph), 네트워크(network) : 비용이나 가중치
- 그래프의 표현 방식에 따라 알고리즘의 시간 복잡도가 바뀔 수 있기 때문에 신중하게 선택
- 간선이 적은 희소 그래프(sparse graph): 인접 리스트
- 간선이 많은 밀집 그래프(dense graph): 인접 행렬
- 인접 리스트로 그래프를 표현
- 그래프 내의 각 정점의 인접 관계를 표현하는 리스트
- 리스트 보다는 vector 와 같이 길이를 변경할 수 있는 배열로 구현
- 인접 행렬로 그래프를 표현
- 정점의 개수가 N개이면 N*N 행렬을 이용
- i 번째 정점과 j 번째 정점의 관계를 G[i][j]에 저장
- 문제에서 주어지는 번호대로 저장
- 배열이라고 0 부터 하지 말자
- 그래프에서 간선의 개수가 따로 주어지지 않을 경우 최대 개수는 V^2
- 그래프 공간 복잡도
- 인접행렬
- 간선 정보를 행렬에 저장
- 2 차원 배열 이용
- O(N^2)
- 인접리스트
- 정점과 연결된 간선들을 리스트로 연결
- 링크드 리스트 이용
- O(E)
- 간선 리스트
- 간선을 배열에 순서대로 나열
- 1차원 배열, 정렬, 구간합 이용
- O(V+E)
- DFS (Depth First Search)
- BFS (Breadth First Search)
- 그래프 순회
- 그래프 탐색은 그래프의 가장 기본적인 연산
- 하나의 정점으로부터 시작하여 차례대로 모든 정점을 한번씩 방문
- 많은 문제들이 단순히 그래프의 정점을 탐색하는 것으로 해결
- 특정한 정점에서 다른 정점으로 갈 수 있는지 없는지
- 전자 회로에서 특정 단자와 단자가 서로 연결되어 있는지
- DFS / BFS 이용
- 스패닝트리(Spanning Tree)
- 그래프 내의 모든 정점을 포함하는 트리
- 싸이클을 포함해서는 안 된다
- 통신 네트워크 구축: 최소의 링크를 사용하여 네트워크를 구축하고 싶은 경우
- 최소비용 스패닝트리(MST: Minimum Spanning Tree)
- 네트워크에 있는 모든 정점들을 가장 적은 수의 간선과 비용으로 연결하는 트리
- 간선에 가중치 속성을 부여, 각 간선이 갖고 있는 가중치의 합이 최소가 되는 트리
- 응용
- 도로 건설 - 도시들을 모두 연결하면서 도로의 길이가 최소가 되도록 하는 문제
- 전기 회로 - 단자들을 모두 연결하면서 전선의 길이가 최소가 되도록 하는 문제
- 통신 - 전화선의 길이가 최소가 되도록 전화 케이블 망을 구성하는 문제
- 배관 - 파이프를 모두 연결하면서 파이프의 총 길이가 최소가 되도록 연결하는 문제
- MST 알고리즘
- Kruskal 알고리즘
- 그래프 내의 모든 간선을 가중치의 오름차순으로 정렬
- 싸이클이 형성되지 않으면서 간선을 최소 스패닝 트리에 추가
- 싸이클 검사에는 union-find 알고리즘이 사용
- 집합들의 합집합을 구하고 집합의 원소가 어떤 집합에 속하는지를 계산하는 알고리즘
- Prim 알고리즘
- 그래프와 노드가 하나도 없는 최소 스패닝 트리에서 시작
- 그래프의 임의의 정점을 시작 정점으로 선택하여 최소 스패닝 트리의 루트 노드로 삽입
- MST 에 삽입되어 있는 정점들과 이 정점들의 모든 인접 정점 사이에 있는 간선의 가중치를 조사하여 가장 작은 것을 MST 에 삽입한다. 단, 싸이클을 형성해서는 안 된다.
- 이 과정을 반복하다가 모든 정점이 연결되면 종료
- 최단 경로(Shortest path)
- 최단 경로 문제
- 네트워크에서 정점 i와 정점 j를 연결하는 경로 중에서 간선들의 가중치 합이 최소가 되는 경로를 찾는 문제
- 간선의 가중치는 비용, 거리, 시간 등
- 최단 거리
- 한 정점에서 다른 정점으로 가는 최단 거리를 구하는 문제
- 간선의 가중차기 모두 1 인 경우
- BFS 를 이용해서 구할 수 있다
- 소스에서 step 이 바로 시작정점에서 거리
- 간선의 가중치가 다른 경우
- 시작정점에서 특정 목표 정점까지 최단거리 (단일 시작점)
- 다익스트라(Dijkstra) 알고리즘
- 시작 정점 V로부터의 최단 경로가 이미 발견된 정점들의 집합 S
- distance 배열: 최단 경로를 알려진 정점만을 통하여 각 정점까지 가는 최단 경로의 길이
- 매 단계에서 가장 distance 값이 적은 정점을 S에 추가한다
- 벨만포드 알고리즘: 간선에 음수가 있을 경우
- 모든 쌍에 대해 최단거리
- 플로이드(Floyd) 알고리즘
- 모든 정점사이의 최단 경로를 한 번에 찾아주는 알고리즘
- 정점 k를 거쳐서 가지 않는 경우
- 정점 i 에서 j 로 가는 최단 거리는 weight[i][j]
- 정점 k를 거쳐서 가는 경우
- weight[i][k] + weight[k][j]
- 최단 거리는 둘 중의 작은 값
- 시간복잡도: O(V^3)
- 네트워크 유량
- 최대 유량 문제
- 각 간선에 대해 길이 뿐만이 아니라 용량을 정의했을 때 풀 수 있는 문제
- 각 간선이 용량을 갖는 그래프에서 두 정점 사이에 얼마나 많은 '흐름' 혹은 유량(flow)을 보낼 수 있는지를 계산하는 문제를 네트워크 유량 문제
- 그래프와 별 상관이 없어 보이는 다양한 최적화 문제들을 푸는 데도 이용
- 유량 네트워크(flow network)란 각 간선에 용량(capacity)이라는 추가 속성이 존재하는 방향 그래프
- 정점 u 에서 v 로 가는 간선의 용량 c(u, v), 실제 흐르는 유량 f(u, v)
- 용량 제한 속성 : f(u, v) ≤ c(u, v)
- 유량의 대칭성 : f(u, v) == -f(v, u) 음수의 유량
- 유량의 보존 : 각 정점에 들어오는 유량과 나가는 유량의 양은 같음
- 소스(source): 유량이 시작되는 정점
- 싱크(sink): 유량이 도착하는 정점
- 정리: 네트워크 유량 문제는 이렇게 소스와 싱크 사이에 흐를 수 있는 최대 유량을 계산하는 문제
- 포드-풀커슨(Ford-Fulkerson) 알고리즘
- 위상 정렬(Topological sort)
- 방향성 그래프에서 간선 <u, v>가 있다면 정점 u는 정점v 를 선행한다고 말하며, 각 정점들의 선행 순서를 위배하지 않으면서 모든 정점을 나열하는 것을 말한다
- 모든 방향 간선이 왼쪽에서 오른쪽으로 향할 수 있도록 수평선을 따르는 정점의 나열
- 깊이 우선 탐색을 이용한 위상 정렬
- 리스트를 하나 준비한다
- 그래프에서 진입 간선이 없는 정점에 대해 깊이 우선 탐색을 수행하고, 탐색 중에 더 이상 옮겨갈 수 있는 인접 정점이 없는 정점을 만나면 이 정점을 리스트의 새로운 헤드로 입력한다
- 더 이상 방문할 정점이 없을 때까지 반복하고 깊이 우선 탐색을 종료하면, 리스트에는 위상 정렬된 리스트가 남는다
- 트리
- 계층 관계를 갖는 객체들을 표현하기 위해 만들어진 자료 구조
- 실제 계층 관계가 없는 자료들을 트리로 표현해서 같은 연산을 더 빠른게 할 수 있는 경우가 많아서 다른 용도로 더 많이 사용된다 (탐색형 자료구조)
- 뿌리(Root), 가지(Branch), 잎(Leaf)으로 이루어진 자료구조
- 부모-자식 관계의 노드들로 구성(부모, 자식, 형제(sibling))
- 경로
- "A, B, C" 는 A 에서 C 까지의 경로
- 깊이(Depth)
- 루트 노드에서 해당 노드까지의 경로의 길이
- 레벨(Level) - 같은 깊이를 가지는 노드의 집합
- 높이(Height) - "가장 깊은 곳"에 있는 잎 노드까지의 깊이
- 차수(Degree)
- 자식 노드의 개수
- 트리의 차수는 트리 내에 있는 노드 중에 중에서 자식 노드가 가장 많은 노드의 차수를 말함
- 노드(Node)
- 트리의 구성 요소
- 단말 노드: 자식이 없는 노드
- 비단말 노드: 적어도 하나의 자식을 가지는 노드
- 루트(Root): 부모가 없는 노드
- 서브 트리: 하나의 노드와 그 자손 노드들로 이루어진 트리
- 트리의 표현
- 배열표현법
- 모든 이진트리를 포화이진트리라고 가정하고, 각 노드에 번호를 붙여서 그 번호를 배열의 인덱스로 삼아 노드의 데이터를 배열에 저장하는 방법
- 왼쪽에서 오른쪽으로 순차적으로 각 노드 번호를 부여한다. 루트의 번호를 1 로 하고, 레벨 n 에 있는 노드에 대해서 왼쪽부터 오른쪽으로 2n 부터 2n+1 - 1 까지 번호를 부여한다
- 루트 1 은 레벨 0
- 다음 2 3 은 레벨 1
- 다음 4 5 6 7 은 레벨 2
- 다음과 같은 규칙이 생성이 된다
- 노드 번호가 i 인 노드의 부모 노드 번호 => i/2
- 노드 번호가 i 인 노드의 왼쪽 자손 노드 번호 => i*2
- 노드 번호가 i 인 노드의 오른쪽쪽 자손 노드 번호 => i*2 + 1
- 레벨 n 의 노드들의 시작 번호 => 2n
- 레벨 n 의 최대 노드 수 => 2n
- 높이가 h 인 이진 트리를 위한 배열의 크기 => 2h+1 - 1
- 링크표현법
- 이진 트리(Binary Tree)
- 모든 노드가 2 개의 서브 트리를 가지고 있는 트리
- 서브 트리는 공집합일 수 있다
- 서브 트리간의 순서가 존재
- 이진 트리의 성질
- 노드의 개수가 n 이면, 간선의 개수는 n-1
- 높이가 h인 이진트리의 경우, 최소 h개의 노드를 가지며 최대 2h-1개의 노드를 가진다
- n개의 노드를 가지는 이진트리의 높이는 최대 n이거나 최소 log2(n+1)
- 포화 이진 트리(Full Binary Tree)
- 모든 노드가 대대손손이 자식을 둘 씩 가지고 있는 이진 트리
- 완전 이진 트리(Complete Binary Tree)
- 잎 노드들이 트리의 왼쪽부터 차곡차곡 채워진 트리
- 높이 구현 트리(Height Balanced Tree)
- 루트 노드를 기준으로 왼쪽 하위 트리와 오른쪽 하위 트리의 높이가 1 이상 차이 나지 않는 이진 트리
- 완전 높이 균형 트리(Completely Height Balanced Tree)
- 루트 노드를 기준으로 왼쪽 하위 트리와 하위 트리의 높이가 같은 이진 트리
- 순회(Traversal)
- 트리의 노드들을 체계적으로 방문하는 것
- 전위 순회(Preorder traversal) : VLR
- 루트 => 왼쪽 자손 => 오른쪽 자손
- 깊이 우선 순회(Depth First Traversal)
- 중위 순회(Inorder traversal) : LVR
- 왼쪽 자손 => 루트 => 오른쪽 자손
- 대칭 순회(Symmetric Traversal)
- 후위 순회(Postorder traversal) : LRV
- 왼쪽 자손 => 오른쪽 자손 => 루트
- 레벨 순서 순회(Level Order)
- 모든 노드를 낮은 레벨부터 차례대로 순환
- 너비 우선 순회(Breadth First Traversal)
- 예)
- 전위 순회: F, B, A, D, C, E, G, I, H (root, left, right)
- 중위 순회: A, B, C, D, E, F, G, H, I (left, root, right)
- 후위 순회: A, C, E, D, B, H, I, G, F (left, right, root)
- 레벨 순서 순회: F, B, G, A, D, I, C, E, H
- 수식 트리
- 수식 이진 트리(Expression Binary Tree)
- 수식을 표현하는 이진 트리
- 연산자는 루트 노드이거나 가지 노드
- 비단말노드: 연산자(Operator)
- 단말노드: 피연산자(Operand)
- 피연산자는 모두 잎 노드
- 가장 아래에 있는 하위 수식 트리(잎노드)로부터 수 또는 계산 결과를 병합해 올라가는 과정을 반복하여 계산을 수행
- 후위 순회를 사용
- 수식 트리의 구축
- 중위 표기식보다는 후위 표기식을 이용해 트리를 구축하는 것이 수월
- 이진 탐색 트리
- 탐색 작업을 효율적으로 처리가 위한 자료구조로 모든 원소는 서로 다른 유일한 키를 가지고 있다. 탐색(Searching), 삽입(Insertion), 삭제(Deletion) 시간은 트리의 높이만큼 시간이 걸리며, 시간복잡도는 O(h) 이다.
- 힙(Heap)은 "완전 이진트리"에 있는 노드 중에서 키값이 가장 큰 노드나 키값이 가장 작은 노드를 찾기 위해서 만든 자료구조이다. 최대 힙과 최소 힙이 있고, 힙의 키를 우선순위로 활용하여 우선순위 큐를 구현할 수 있다
- 최대 힙(Max Heap)
- 키값이 가장 큰 노드를 찾기 위한 완전 이진트리
- 부모노드의 키값 > 자손노드의 키값
- 루트노드는 키값이 가장 큰 노드
- 최소 힙(Min Heap) 은 최대 힙의 반대
- 힙의 삽입과 삭제 연산이 있다
- 가장 큰(작은) 원소를 찾는 데 최적화된 형태의 이진 트리
- 우선순위 큐를 쉽게 구현
- 부모 노드가 가진 원소는 항상 자식 노드가 가진 원소 이상
- 가장 큰(작은) 원소는 항상 트리의 루트
- 모양 규칙
- 마지막 레벨을 제외한 모든 레벨에 노드가 꽉 차 있어야 한다
- 마지막 레벨에 노드가 있을 때는 항상 가장 왼쪽부터 순서대로 채워져야 한다
- 배열을 이용하여 구현 가능
- 0 이 루트
- A[i] 노드의 왼쪽 자손: A[2*i + 1]
- A[i] 노드의 오른쪽 자손: A[2*i + 2]
- A[i] 노드의 부모: A[(i-1)/2]
- 삽입
- 맨 끝에 추가후 부모와 계속 비교
- 삭제
- 루트 삭제 후 맨 마지막 원소를 루트에 덮어 씌우고, 대소 관계 조건을 만족시킨다(자식과 비교)
- 최소는 가능한 자식 중에 더 작은 값과 교환
- 최대는 가능한 자식 중에 더 큰 값과 교환
- 구간 트리(Segment Tree)
- 저장된 자료들을 적절히 전처리해 그들에 대한 질의들을 빠르게 대답 가능
- 일차원 배열의 특정 구간에 대한 질문을 빠르게 대답하는데 사용
- 기본: 주어진 배열의 구간들을 표현하는 이진 트리를 만드는 것(전처리)
- 루트: 항상 배열의 전체 구간 [0, n-1]
- 왼쪽 자식과 오른쪽 자식은 각각 해당 구간의 왼쪽 반과 오른쪽 반을 표현
- 대표 예: 특정 구간의 최소치를 찾는 문제
- 구간 최소 쿼리(RMQ, Range Minimum Query)
- 루트 노드: 배열의 1 번 원소
- 노드 i 의 왼쪽 자손: 2 * i
- 노드 i 의 오른쪽 자손: 2 * i + 1 번
- 배열의 길이: 가장 가까운 2의 거듭제곱으로 n을 올림 후 2 를 곱한 값 혹은 4n
- LCA(Lowest Common Ancestor)문제
- 트리에서 두 노드의 공통조상 중 가장 가까운 조상 노드를 구하는 문제
- 한 노드가 다른 하나의 조상인 경우 최소 공통 조상은 둘 중 위 노드가 된다는 데 유의
- BIT: Binary Index Tree 는 일반적으로 누적합만 활용할 수 있다
- IDT: Index Tree 는 Complete Binary Tree 로 구성된 Segment Tree 를 말한다
- 정해진 구간의 합, 최댓값, 최솟값 등을 lg n 의 시간에 구할 수 있으며, 갱신 또한 lg n 시간에 처리할 수 있는 매우 효율적인 자료구조
- 기본적인 구조는 원본 데이터를 complete binary tree 의 단말노드에 배치하고, 그 값의 부모노드를 특정한 의미를 가지는 index로 활동하는 방법이다
- 가장 효율적인 표현 방법은 일차원 배열을 이용하고, 원본 데이터가 n개인 경우에 n이상인 가장 작은 2k값을 기준으로 설정하고, 이 값의 2 배에서 1 을 뺀 개수의 원소를 가지는 배열로 구성
- 최대 LIS (Longest Increasing Subsequence) 를 O(lg n) 만에 빠르게 찾아낼 수 있다
- 트립(treap) - A Randomized Binary Search Tree (Tree + Heap)
- 구현이 간단한 이진 검색 트리
- 트리의 형태가 추가 순서가 아닌 난수에 의해 임의대로 결정
- 추가나 삭제되더라도 트리 높이의 기대치는 항상 일정
- 트립의 루트는 N 개의 노드 중 최대의 우선순위를 갖는 노드
- 트립의 조건
- 이진 검색 트리의 조건: 모든 노드에서 왼쪽 서브트리의 원소는 작고, 오른쪽 서브트리의 원소는 크다
- 힙(heap)의 조건: 모든 노드의 우선순위는 각자의 자식 노드보다 크거나 같다
- 유니온-파인드(Union-Find) 자료 구조
- 상호 배타적 집합(disjoint set)을 표현
- 공통 원소가 없는, 상호 배타적인 부분 집합들로 나눠진 원소들에 대한 정보를 저장하고 조작하는 자료 구조
- 초기화
- 합치기(Union/Merge) 연산
- 찾기(Find) 연산
- 알고리즘 문제 해결 전략: 코드 25.2
- 그리디(Greedy)
- 알고리즘 설계에 있어서 중요한 기법중의 하나
- 결정을 해야 할 때마다 그 순간에 가장 좋다고 생각되는 것을 해답으로 선택함으로써 최종적인 해답에 도달
- 항상 최적의 답을 해주는지를 반드시 검증해야 한다
- 최적의 해답을 주는 것으로 증명된 알고리즘
- Kruskal 알고리즘
- 완전 탐색과 동적 계획법 알고리즘
- 같은 점
- 여러 개의 조각으로 쪼개고, 각 단계마다 답의 한 부분을 만들어 간다는 점
- 다른 점
- 모든 선택지를 고려해 보고 그중 전체 답이 가장 좋은 것을 찾음
- 각 단계마다 지금 당장 가장 좋은 방법만을 선택
- 지금의 선택이 앞으로 남은 선택에 어떤 영향을 끼칠지는 고려하지 않는다
- 두 가지 경우로 제한
- 탐욕법을 사용해도 항상 최적해를 구할 수 있는 문제를 만난 경우
- 시간이나 공간적 제약으로 다른 방법으로 최적해를 찾기 너무 어려운 경우
- 최적은 아니지만 임의의 답보다는 좋은 답을 구하는 용도
- 유용하게 사용되는 문제 중 유명한 예
- 활동 선택 문제(activity selection problem)
- 회의실 예약 문제의 경우에 가장 먼저 끝나는 회의부터 선택하는 것
- 정당성의 증명
- 탐욕적 선택 속성(Greedy choice property)
- 동적 계획법처럼 답의 모든 부분을 고려하지 않고 탐욕적으로만 선택하더라도 최적해를 구할 수 있다는 것
- 최적 부분 구조(Optimal substructure)
- 부분 문제의 최적해에서 전체 문제의 최적해를 만들 수 있음을 보인다
- 메타휴리스틱(Metaheuristic) 알고리즘
- 최적화 알고리즘의 한 종류
- 좋은 답을 빠르게 찾아낼 수 없는 문제의 근사해를 구하기 우해 주로 사용
- 국소 탐색(Local search)이나 시뮬레이티드 어닐링(Simulated annealing)이 구현과 튜닝이 가장 간단해 자주 사용
- KMP 알고리즘 (참고: http://bowbowbow.tistory.com/6)
- 커누스-모리스-프랫Knuth-Morris-Pratt) 알고리즘
- 문자열 검색 문제
- 텍스트의 길이 N, 검색 문자를 M 이라고 할 때, O(M+N) 으로 처리가능
- 불일치가 일어났을 때 지금까지 일치한 글자의 수를 이용해 다음으로 시도해야 할 시작 위치를 빠르게 찾아냄
- 전처리 과정에서 배열 pi[] 를 계산
- pi[i] = N[...i]의 접두사도 되고 접미사도 되는 문자열의 최대 길이
- 부분 일치 테이블(partial number table)
자료구조
- 자료구조
- 프로그램이 자료를 저장하는 방식
- 두 가지 목적을 위해서 고안된 것들
- 추상화
- 자료를 좀 더 알아보기 쉽고, 이해하기 쉬운 형태로 저장하기 위한 고민의 결과물
- 최적화
- 프로그램의 동작 속도를 빠르게 하기 위한 것
- 탐색 자료 구조의 경우 표현방식과 더불어 다루는 방법(알고리즘)도 같이 정의
- 선형 자료구조
- 왼쪽에서 오른쪽 혹은 위에서 아래로 나열된 형태를 하고 있는 것
- 정적 배열 (C++ 자료형[])
- 동적 배열 (C++ STL vector)
- 더 나은 성능을 위해서 reserve() 와 resize() 를 사용
- 정렬과 검색
- O(n2) 정렬 알고리즘: 버블(Bubble), 선택(Selection), 삽입(Insertion) 등
- O(nlogn) 정렬 알고리즘: 병합(Merge), 힙(Heap), 퀵(Quick) 등
- STL algorithm: sort, partial_sort, stable_sort
- 특수 목적 정렬 알고리즘: O(n) 계수(Counting), 기수(Radix), 버킷(Bucket) 등
- O(n) 선형 탐색
- O(logn) 이진 탐색: C++ STL algorithm 내의 lower_bound, upper_bound, binary_search
- O(1) 해싱
- 불린(Boolean) 배열: C++ STL bitset
- 불린 값들로 이루어진 작은 집합을 가볍게 다루기 위한 비트마스크
- 비선형 자료구조 (내장 라이브러리)
- 균형 잡힌 이진 검색 트리
- C++ STL map
- 페어로 된(예, 키 -> 값) 자료의 모음을 동적으로 처리
- 삽입, 검색, 삭제 작업을 O(logn)
- C++ STL set
- 키만을 저장
- 보통, 특정한 키가 존재하는지 여부를 효율적으로 판별하려 할 때만 유용
- 힙(Heap)
- 완전 트리
- 힙은 우선순위 큐를 모델링할 때 유용한 자료구조
- O(logn) 으로 가장 높은 우선순위를 갖는 원소(최대 혹은 최소)를 큐에서 꺼내는 작업과 새로운 원소 v를 큐에 추가하는 작업을 지원
- MST 를 구하기 위한 프림(및 크루스칼) 알고리즘
- 단일 시작점 최단 경로 문제를 위한 다익스트라 알고리즘
- A* 탐색 알고리즘
- C++ STL priority_queue
- 해시 테이블
- C++11 STL unordered_map
- 잘 안쓰임
- 추상형 자료구조
- 스택
- 큐
- BFS (Breadth First Search) 너비 우선 탐색
- 탐색형 자료구조
- 트리
- 이진 탐색 트리
- 힙
- 구간 트리
- 해시
- 분류
- 동적 배열
- 연결 리스트
- 문자열 탐색
- KMP 알고리즘
- 문자열 처리
- 접미사 배열(suffix array)
- 트리
- 그래프
- 비트마스크(bitmask)
- 엄밀하게 자료구조는 아니지만, 많이 쓰인다
- 정수의 이진수 표현을 자료 구조로 쓰는 기법
- 비트 연산자
- a & b: 비트별로 AND 연산
- a | b: 비트별로 OR 연산
- a ^ b: 비트별로 XOR 연산
- ~a: 비트별 NOT 연산 결과
- a << b: 왼쪽으로 b 비트 시프트
- a >> b: 오른쪽으로 b 비트 시프트
- 집합의 구현
- 가장 중요한 사용 사례(예, 20 개를 이용)
- 공집합
- int a = 0;
- 꽉 찬 집합
- int a = (1<<20) - 1; // 1 을 20 bit 이동하고 1 을 뺀다
- 원소 추가 p (0 <= p < 20)
- a |= (1 << p);
- 원소의 포함 여부
- if (a & (1 << p)) ~~
- 원소의 삭제
- a &= ~(1 <<p);
- 원소의 토글
- a ^= (1 << p);
- 두 집합의 연산
- 집합간의 연산을 빠르게 할 수 있다는 것이 큰 장점
- 합집합
- added = (a | b);
- 교집합
- intersection = (a & b);
- 차집합
- removed = (a & ~b);
- 하나에만 포함된 원소들의 집합
- toggled = (a ^ b);
- 집합의 크기
- 최소 원소 찾기
- int first = (a & -a);
- 최소 원소 지우기
- a &= (a - 1);
- 모든 부분 집합 순회하기
- for (int subset = a; subset; subset = ((subset-1) & a)) {}