유니온 파인드란?

 

- 그래프 알고리즘 중 한 가지

- 분리 집합(상호배타적 집합)(Disjoing-set) 과 같은 알고리즘

- 여러 노드가 존재할 경우, 선택한 두 노드가 서로 같은 그래프에 속하는지 판별하는 알고리즘

- 위 과정이 Find, Union 두 가지 연산으로 이루어짐

 Find : 노드 x 가 어떤 집합에 속하는지 확인하는 연산

 Union : 노드 x와 y가 포함되어 있는 집합을 병합하는 연산

 

 

init함수

void init() {
	for (int i = 1; i <= N; i++)
		root[i] = i;
}

- 모든 노드에 대하여 자기 자신을 자신의 root 로 초기화

 

Find함수

int find(int i) {
	if (i == root[i])
		return i;
	else
		return root[i] = find(root[i]); //경로 압축
}

- 파라미터로 들어간 노드의 루트 노드를 반환하는 find 함수

- 어느 트리(집합)에 속해있는지를 확인하는 연산

- 한 쪽으로 트리가 치우쳐진 경우 트리로 구현하는 이점이 사라지기 때문에 경로 압축 최적화를 한다.

- 경로 압축 최적화 : i 에 대하여 루트 노드인 root[i]를 찾은 경우 i의 부모를 바로 루트 노드를 바꾸어 트리의 높이를 압축하여 find 함수의 효율을 높인다.

 

Union함수

void merge(int a, int b) {
	int x = find(a);
	int y = find(b);

	//루트가 같다면 union과정 생략
	if (x == y)
		return;    
    if (rank[x] > rank[y])
        root[y] = x;
	else
		root[x] = y;
        
    //두 트리의 깊이가 같은 경우 y의 깊이를 늘림    
    if(rank[x] == rank[y])
        ++rank[y];
}

- 집합을 함칠 때 높이가 더 높은 트리가 낮은 트리 밑으로 들어갈 경우 트리가 점점 깊어져 효율성이 떨어진다.

- rank 배열을 사용하여 트리의 높이가 낮은 트리가 높은 트리 밑으로 들어가도록 한다.

 

 

트리의 개념

트리는 그래프의 한 종류로서 정점(node)들과 정점들을 연결하는 간선(edge) 들로 구성된다. 

사이클이 없는 하나의 연결 그래프이자 DAG(방향성이 있는 비순환 그래프) 의 한 종류이다.

트리는 하나의 루트 노드를 갖으며 부모 노드 밑에 여러 자식 노드가 연결되고, 자식 노드 각각에 다시 자식 노드가 연결되는 재귀적 형태의 자료구조이다. 

 

트리 관련 용어

 

  • 루트 노드(root node) : 부모가 없는 노드. 트리는 하나의 루트 노드만을 가진다.

  • 단말 노드(leaf node) : 자식이 없는 노드이다.

  • 내부(internal) 노드 : 리프 노드가 아닌 노드.

  • 링크(link) : 노드를 연결하는 선 (edge, branch 라고도 부름).

  • 형제(sibling) : 같은 부모를 가지는 노드.

  • 노드의 크기(size) : 자신을 포함한 모든 자손 노드의 개수.

  • 노드의 깊이(depth) : 루트에서 어떤 노드에 도달하기 위해 거쳐야 하는 간선의 수

  • 노드의 레벨(level) : 트리의 특정 깊이를 가지는 노드의 집합

  • 노드의 차수(degree) : 부(하위) 트리 갯수/간선수 (degree) = 각 노드가 지닌 가지의 수

  • 트리의 차수(degree of tree) : 트리의 최대 차수

  • 트리의 높이(height) : 루트 노드에서 가장 깊숙히 있는 노드의 깊이

 

트리의 종류

 

1. 이진 트리 (Binary tree)

  : 부모 노드가 자식 노드를 최대 2개씩만 갖고 있는 트리

 

2. 이진 검색 트리 (Binary Search Tree)

 : left child 와 그 이하 노드들의 데이터가 현재 노드의 데이터보다 작아야 하고

 : right child 와 그 이하 노드들의 데이터가 현재 노드의 데이터보다 커야 한다.

 

3. 균형 트리 (Balanced Tree)

 : O(logN) 의 시간에 insert와 find를 할 수 있을 정도로 left노드와 right노드의 갯수가 균형적으로 이루어져 있는 트리

 

4. 완전 이진 트리 (Complete Binary Tree)

 : 트리의 모든 높이에서 노드가 꽉 차 있는 이진 트리(마지막 레벨을 제외하고 모든 레벨이 완전히 채워져 있음)

 : 마지막 레벨은 완전히 채워져 있지 않아도 되지만 노드가 왼쪽에서 오른쪽 순으로 채워져야 한다.

 

5. 정 이진트리 (Full Binary Tree)

 : 모든 노드가 0개 또는 2개의 자식 노드를 갖는 트리

 

6. 포화 이진 트리 (Perfect Binary Tree)

 : 정 이진트리면서 완전 이진트리인 경우

 : 모든 말단 노드는 같은 높이에 있어야 하며, 마지막 단계에서는 노드의 개수가 최대가 되어야 한다.

 : 모든 내부 노드가 두 개의 자식 노드를 가진다.

 : 노드의 개수가 2^(k-1) 개여야 한다. (k: 트리 높이)

 

 

Dijkstra Algorithm

음의 가중치가 없는 그래프의 한 정점(頂點, Vertex)에서 모든 정점까지의 최단거리를 각각 구하는 알고리즘(최단 경로 문제, Shortest Path Problem)이다.

 

- 다익스트라 알고리즘의 최단 경로 문제 해결 과정

 

노드(Node) : 0번 ~ 6번

링크(Link) : 각 노드를 연결하는 선

비용(Cost) :링크 위에 표시된 숫자, 해당 링크를 지나갈때에 소요되는 경비, 비용

 

다익스트라 알고리즘을 통해 0번 노드를 시작접으로 1~6번 노드를 목적지로 하는 최단 경로 6개를 구하는 과정

S : 이미 처리가 완결된 노드의 집합

- 시작 노드인 0번 부터 처리를 시작

D : 0번 ~ 6번 노드에 대한, 시작노드로부터의 소요되는 비용

- 0번 -> 1번 노드 비용 : 5 // 0번 -> 3번 노드 비용 : 1

T : 해당 노드로 가는데 연결된 노드의 번호

- 상단 : 노드 번호 // 하단 : 연결된 노드 번호

 

S 집합에 3번 노드 추가

- D 저장소에 3번 노드에 대한 비용값이 가장 최소이기 때문

3번 노드와 링크로 연결된 2번 노드와 5번 노드에 대한 비용값을 계산

- D 저장소에 기록

시작 노드 0번 노드로부터 2번 노드까지 소요되는 비용 : 3

- 0번 -> 3번 -> 2번 이동 루트에 있는 비용값의 합 = 1 + 2 = 3

시작 노드 0번 노드로부터 5번 노드까지 소요되는 비용 : 2

- 0번 -> 3번 -> 5번 이동 루트에 있는 비용값의 합 = 1 + 1 = 2

 

S 집합에 5번 노드 추가

- 이미 처리가 완료된 노드 이외의 노드 중 5번 노드의 비용이 2로 최소이기 때문

- S 집합에 존재하는지 확인하여 이미 처리가 완료된 노드인지 확인

5번 노드와 링크로 연결된 2번 노드, 3번 노드, 6번 노드에 대한 비용값을 계산

- 2번 노드 : 2 + 2 = 4 (전 단계에서 계산된 비용값 3보다 크므로 무시)

- 3번 노드 : 이미 처리 완료

- 6번 노드 : 2 + 3 = 5 (D 저장소에 기록)

 

S 집합에 2번 노드 추가

- 이미 처리가 완료된 노드 이외의 노드 중 2번 노드의 비용이 2로 최소이기 때문

2번 노드와 링크로 연결된 1번 노드, 3번 노드, 5번 노드, 6번 노드에 대한 비용값을 계산

- 1번 노드 : 3 + 1 = 4 (전 단계에서 계산된 비용값 5 보다 작으므로 D 저장소 값을 변경)

- 3번 노드 : 이미 처리 완료( S 집합 포함 )

- 5번 노드 : 이미 처리 완료( S 집합 포함 )

- 6번 노드 : 3 + 8 = 11 (전 단계에서 계산된 비용값 5보다 크므로 무시)

 

S 집합에 1번 노드 추가

- 이미 처리가 완료된 노드 이외의 노드 중 1번 노드의 비용이 최소이기 때문

1번 노드와 링크로 연결된 0번 노드, 2번 노드, 4번 노드에 대한 비용값을 계산

- 0번 노드 : 이미 처리 완료( S 집합 포함 )

- 2번 노드 : 이미 처리 완료( S 집합 포함 )

- 4번 노드 : 4 + 3 = 7 (D 저장소에 기록)

S 집합에 6번 노드 추가

- 이미 처리가 완료된 노드 이외의 노드 중 6번 노드의 비용이 최소이기 때문

6번 노드와 링크로 연결된 1번 노드, 2번 노드, 4번 노드, 5번 노드에 대한 비용값을 계산

- 1번 노드 : 이미 처리 완료( S 집합 포함 )

- 2번 노드 : 이미 처리 완료( S 집합 포함 )

- 4번 노드 : 5 + 1 = 6 (전 단계에서 계산된 비용값 7 보다 작으므로 D 저장소 값을 변경)

- 5번 노드 : 이미 처리 완료( S 집합 포함 )

 

S 집합에 4번 노드 추가

- 유일하게 처리되지 않은 노드

4번 노드와 링크로 연결된 1번 노드, 6번 노드에 대한 비용값을 계산

- 1번 노드 : 이미 처리 완료( S 집합 포함 )

- 6번 노드 : 이미 처리 완료( S 집합 포함 )

-> 더이상 진행하지 않고 종료

 

T 저장소를 통해 출발 노드인 0번 노드에서 각 노드에 대한 최단 경로 파악

 

  1. 0번 노드에서 1번 노드 : (1번 노드)←(2번 노드)←(3번 노드)←(0번 노드)
  2. 0번 노드에서 2번 노드 : (2번 노드)←(3번 노드)←(0번 노드)
  3. 0번 노드에서 3번 노드 : (3번 노드)←(0번 노드)
  4. 0번 노드에서 4번 노드 : (4번 노드)←(6번 노드)←(5번 노드)←(3번 노드)←(0번 노드)
  5. 0번 노드에서 5번 노드 : (5번 노드)←(3번 노드)←(0번 노드)
  6. 0번 노드에서 6번 노드 : (6번 노드)←(5번 노드)←(3번 노드)←(0번 노드)

 

 

- 수도 코드

 

 

- 시간 복잡도

  • n 개의 정점(node)이 있다고 했을 경우
  • 주 반복문을 n번 반복하고
  • 내부 반복문을 2n 번 반복하므로
  • O(n^2) 의 시간복잡도를 가진다.

수학에서 에라토스테네스의 체는 소수를 찾는 방법이다. 고대 그리스 수학자 에라토스테네스가 발견하였다.

 

 

에라토스테네스의 체는 소수를 구하기 위한 방법 중 하나로 프로그래밍 언어를 통해 알고리즘을 구현할 수 있다.

 

  1. 2부터 소수를 구하고자 하는 구간의 모든 수를 나열한다. 그림에서 회색 사각형으로 두른 수들이 여기에 해당한다.
  2. 2는 소수이므로 오른쪽에 2를 쓴다. (빨간색)
  3. 자기 자신을 제외한 2의 배수를 모두 지운다.
  4. 남아있는 수 가운데 3은 소수이므로 오른쪽에 3을 쓴다. (초록색)
  5. 자기 자신을 제외한 3의 배수를 모두 지운다.
  6. 남아있는 수 가운데 5는 소수이므로 오른쪽에 5를 쓴다. (파란색)
  7. 자기 자신을 제외한 5의 배수를 모두 지운다.
  8. 남아있는 수 가운데 7은 소수이므로 오른쪽에 7을 쓴다. (노란색)
  9. 자기 자신을 제외한 7의 배수를 모두 지운다.
  10. 위의 과정을 반복하면 구하는 구간의 모든 소수가 남는다.

 

 

1. 구간 합 VS 부분 합

- Prefix sum(구간 합) : 구간 합이란 수들의 나열에서 특정 구간의 합을 의미한다. 구간 합 알고리즘은 보통 1차원배열에서 i~k 인덱스 사이의 값들의 합을 구하는데 사용한다.

 

- Partial sum(부분 합) : 부분 합이란 구간 합과 달리 처음부터 특정인덱스까지의 합을 의미한다. 즉 0~k 인덱스 사이의 값들의 합을 의미한다.

 

2. 구간 합 알고리즘

  •  반복문을 사용하여 i~k사이의 값을 더하는 알고리즘의 시간복잡도는 O(n)이다.
  •  이 같은 알고리즘을 사용할 경우 n의 값이 클 경우 이를 정해진 시간 내에 해결할 수 없다.
  •  하지만 구간 합 알고리즘을 사용하여 구간합을 구하는 경우 O(1)의 성능을 갖는다.
  •  구간 합 알고리즘은 현재 진행단계까지의 인덱스까지 값의 합을 저장하는 sum[]배열을 사용한다.
  •  j번째 바로 앞까지의 총합에 arr[j] 값을 더하면 j번째까지의 총합을 의미하므로 sum[j] = sum[j-1] + arr[j] 로 표현할 수 있다.

 

3. 구현

 

다음은 백준 11659번 문제 구간합 구하기4 의 풀이이다.

 

-구간합 구하기 4

 

11659번: 구간 합 구하기 4

첫째 줄에 수의 개수 N (1 ≤ N ≤ 100,000), 합을 구해야 하는 횟수 M (1 ≤ M ≤ 100,000)이 주어진다. 둘째 줄에는 N개의 수가 주어진다. 수는 1,000보다 작거나 같은 자연수이다. 셋째 줄부터 M개의 줄에

www.acmicpc.net

- 문제 : 첫째 줄에 수의 개수 N (1 ≤ N ≤ 100,000), 합을 구해야 하는 횟수 M (1 ≤ M ≤ 100,000)이 주어진다. 둘째 줄에는 N개의 수가 주어진다. 수는 1,000보다 작거나 같은 자연수이다. 셋째 줄부터 M개의 줄에는 합을 구해야 하는 구간 i와 j가 주어진다.

- 총 M개의 줄에 입력으로 주어진 i번째 수부터 j번째 수까지 합을 출력한다.

 

 

* 풀이

다음과 같은 문제에서 처음 입력받은 N개의 숫자에 대하여 미리 구간합 (sum[i+1] = sum[i] + num)을 구함으로 인해

M번의 연산에서 이전에 sum[i] 에 저장되어 있는 0~i번째 수까지의 구간 합을 사용함으로써 문제를 정해진 시간 내에 풀 수 있다.

 

다음과 같은 문제를 미리 구간합을 구하지 않고 합을 구할 때 마다 반복문 내에서 index까지의 합을 구하여 중첩 반복문을 사용하여 풀 경우 M(연산횟수) 에 따라 전체 시간이 증가하게 되어 시간 복잡도가 O(M*N*N)으로 주어진 시간을 초과하게 된다.

 

#include <iostream>
#define MAX 100000

int sum[MAX];

int main(void)

	int N, M;
	cin >> N >> M;
	for (int i = 0; i < N; i++)
		{
			int num;
			cin >> num;
			sum[i+1] = sum[i] + num;
		}

	for (int i = 0; i < M; i++)
	{
		int x, y;
		cin >> x >> y;
		cout << sum[y] - sum[x-1] << endl;
	}
	return 0;
}

 

퇴각검색(영어: backtracking, 한국어: 백트래킹)은 한정 조건을 가진 문제를 풀려는 전략이다. "퇴각검색(backtrack)"이란 용어는 1950년대의 미국 수학자 D. H. 레머가 지었다.
 문제가 한정 조건을 가진 경우 원소의 순서는 해결 방법과 무관하다. 이런 문제는 변수 집합으로 이뤄지는데, 한정 조건을 구성하려면 각각의 변수들은 값이 있어야 한다. 퇴각검색은 모든 조합을 시도해서 문제의 해를 찾는다. 이것이 장점이 될 수 있는 이유는 퇴각검색 구현 방법들이 많은 부분 조합들을 배제하기 때문이다. 결국 풀이 시간이 단축된다.

1. 한정 조건을 가진 문제를 풀려는 전략

 - 해를 찾기 위해, 해의 후보를 전짐적으로 확인하여, 해당 후보가 제약 조건을 만족할 수 없다고 판단한 경우 backtrack하여, 다음 후보로 넘어가 최적의 해를 찾는 전략

 

2. backtrack

- 제약 조건을 만족할 수 없다고 판단한 후보에 대하여 다음 단계에서 다시 확인하지 않도록 표기하는 방법

 

3. 모든 경우의 수

- 각 후보의 모든 경우의 수를 상태공간트리를 통해 표현하고 각 후보를 DFS 방식으로 확인한다.

 

4. Promising

- 확인 단계에서 해당 루트가 조건에 맞는지를 검사하는 기법

 

5. Pruning

- 조건에 맞지 않는 루트를 포기하고 다른 루트로 방향을 옮겨 탐색 시간을 절약하는 기법

 

 

백트래킹의 과정

 

1. 문제 상황을 상태 공간 트리로 표현한다.

2. 루트 부터 DFS를 하여 불필요한 탐색을 하지 않고 한정 함수를 만족하는 (x[1], x[2], ..., x[n])를 찾는다.

3. 각 노드 s 에 도착할 때마다 루트에서 s까지의 경로 예를 들어 k번째 선택에선 (x[1], x[2], ..., x[k])의 튜플이 한정함수를 만족하는지 판단한다.
4-1.  (x[1], x[2], ..., x[k])의 튜플이 한정 함수를 만족한다면 x[k] 노드를 live node이자 e-node로 변경한다.
4-2. 한정함수를 만족하지 않는 다면 T(x[1], ... x[k])의 노드들을 더이상 확장하지 않으며 dead node로 만든다.
5.  k < n일때 x[k]의 자식 노드들인 x[k+1] 들 즉, T(x[1], ... x[k])을 탐색한다.
6. 모든 자식 노드를 탐색했다면 현재 노드를 dead node로 변경한다.
7. 진행 도중 한정함수를 만족하는 (x[1], x[2], ..., x[n])에 도달하면 정답을 출력한다.
8. 정답이 도출되었거나 모든 노드가 dead 노드가 되었다면 탐색을 종료한다. 이때 정답이 여러개 인 경우에 모든 정답을 찾고 싶다면 모든 노드가 dead 노드가 될때까지 탐색해야 한다.

DFS는 그래프를 탐색하는 방식 중에 하나이다. 

깊이 우선 탐색( - 優先探索, 영어: depth-first search, DFS)은 맹목적 탐색방법의 하나로 탐색트리의 최근에 첨가된 노드를 선택하고, 이 노드에 적용 가능한 동작자 중 하나를 적용하여 트리에 다음 수준(level)의 한 개의 자식노드를 첨가하며, 첨가된 자식 노드가 목표노드일 때까지 앞의 자식 노드의 첨가 과정을 반복해 가는 방식이다.

1. 그래프

노드(node)와 그 노드를 연결하는 간선(edge)을 하나로 모아 놓은 비선형 자료 구조

그래프 : G = (V,E) 이고, V,E는 다음과 같다.

V(G) : 정점(set of vertices)

E(G) : 간선(set of edges), 정점을 연결하는 선, V X V의 부분집합

 

  • 그래프는 node(혹은 vertice)와 edge(혹은 arcs)로 구성되어 있으며 연결되어 있는 객체 간의 관계를 표현할 수 있는 자료구조로 네트워크 모델이다.
  • 간선이 방향을 갖는지 여부에 따라 무방향 그래프와 방향 그래프로 구분한다.
  • 인접 리스트/인접 행렬 두 가지 방식으로 그래프를 구현할 수 있다.

2. 그래프 탐색

- 그래프의 각 정점을 방문하는 것을 탐색(traversal)이라고 한다.

- 그래프에 관한 연산 중 가장 중요한 것이다.

- 탐색에서 노드의 방문 순서에 따라 다음과 같은 방법이 있다.

 

• 깊이우선탐색 – DFS (Depth First Search) – 갈수있는데 까지 가보는 방문 방법 -트리의 전위탐색 방법을 그래프에 적용한 것이다. (preorder tree traversal)

• 너비우선탐색 – BFS (Breath First Search) – 거리가 가까운 곳부터 가보는 방문 방법 - 트리의 레벨우선탐색을 그래프에 적용한 것이다. (level order tree traversal)

 

3. 깊이 우선 탐색

방법 : 자동차로 여행할 경우 방문지가 있으면 무조건 방문하는 방법이다. 즉 후진하지 않고 가는 방법이며 후진하는 경우는 길이 막히거나, 이미 방문했던 곳일 경우이다. 후진하여 방문할 곳은 마지막에 갈수 있었던 곳 중 가지 않았던 길이다. 이 방법을 깊이 우선 탐색이라고 하며 탐색 중 방문 가능한 곳은 스택 자료를 이용하여 저장해 둔다.

 

1단계 : 하나의 노드를 택한다.

2단계 : 노드를 방문하여 필요한 작업을 한 다음 연결된 다음 노드를 찾는다. 현재 방문 노드는 스택에 저장한다. 2단계를 반복하면서 방문을 계속한다. 막히면 3단계로 간다.

3단계 : 더 이상 방문할 노드가 없으면 스택에서 노드를 빼내 다음 방문 노드를 찾아 2)번 과정을 다시 반복한다.

 

-깊이 우선 탐색 알고리즘

/* 정점 v에서 시작하여 그래프의 깊이 우선 방문 */
void dfs(int v) {
		node_ptr w;
		visited[v] = TRUE; /* 1 방문지 표시 */
		printf(“%5d”, v);
		for(w = graph[v]; w; w = w->link) /* 2 연결리스트 탐색 */
			if(!visited[w->vertex])
				dfs(w->vertex);
}

 

 

4. 깊이 우선 탐색의 시간 복잡도

 

- DFS는 그래프(정점의 수 : N, 간선의 수: E)의 모든 간선을 조회함

* 인접 리스트로 표현된 그래프 : O(N+E)

* 인접 행렬로 표현된 그래프 : O(N^2)

 

 

 

이진 탐색 알고리즘이란?

오름차순으로 정렬된 리스트에서 특정한 값의 위치를 찾는 알고리즘이다. 처음 중간의 값을 임의의 값으로 선택하여, 그 값과 찾고자 하는 값의 크고 작음을 비교하는 방식을 채택하고 있다. 처음 선택한 중앙값이 만약 찾는 값보다 크면 그 값은 새로운 최댓값이 되며, 작으면 그 값은 새로운 최솟값이 된다. 검색 원리상 정렬된 리스트에만 사용할 수 있다는 단점이 있지만, 검색이 반복될 때마다 목표값을 찾을 확률은 두 배가 되므로 속도가 빠르다는 장점이 있다.

위 그림과 같이 정렬되어 있는 리스트 내에서 특정한 값을 찾을 때 중간의 값을 정하고 그 값을 기준으로 찾는 값에 맞는 범위에서 다시 반복한다.

 

 

알고리즘 

int BinarySearch(int key, int arr[])
{
	int left, right = arr.length() - 1, mid;
	while (left <= right) {
		mid = (left + right) / 2;
		if (iKey == arr[mid])
			break;
		if (iKey < arr[mid])
			right = mid - 1;
		else
			left = mid + 1;
	}
}

 

시간복잡도

 

+ Recent posts