서로소 집합(Disjoint Set) : 공통 원소가 없는 두 집합

서로소 부분 집합들로 나누어진 원소들을 데이터를 처리하기 위한 자료구조이다.

union과 find 2개의 연사으로 조작할 수 있다.

트리 자료구조로 표현한다.

 

✔️union-find 방식

1. union(합집합)

   1) A와 B의 부모노드 A', B'을 각각 찾는다.

   2) A'를 B'의 부모 노드로 설정한다. (if. A' < B') 

 

2. find(부모노드 찾기)

    루트 노드 탐색

 

<1번째 소스 : 수정 전>

#include <iostream>
#include <vector>

using namespace std;

// 노드의 개수(v)와 간선(e)의 개수
// 노드의 개수는 최대 100000개라고 가정
int v, e;
int parent[100001];

// 특정 원소가 속한 집합을 찾기
int findParent(int x) {
    if (parent[x] != x) {
        return findParent(parent[x]);
    }
    return x;
}

// 두 원소가 속한 집합을 합치기
void unionParent(int x, int y) {
    x = findParent(x);
    y = findParent(y);

    if (x < y) {
        parent[y] = x;
    }
    else {
        parent[x] = y;
    }
}

int main(void) {

    cin >> v >> e;

    // 부모테이블 상에서, 부모를 자기 자신으로 초기화
    for (int i = 1; i <= v; i++) {
        parent[i] = i;
    }

    // union 연산을 각각 수행
    for (int i = 0; i < e; i++) {
        int a, b;
        cin >> a >> b;
        unionParent(a, b);
    }

    // 각 원소가 속한 집할 출력
    cout << "각 원소가 속한 집합: ";
    for (int i = 1 ; i <= v; i++) {
        cout << findParent(i) << " ";
    }
    cout << endl;

    // 부모 테이블 내용 출력
    cout << "부모 테이블: ";
    for (int i = 1 ; i <= v; i++) {
        cout << parent[i] << " ";
    }
    cout << endl;

    return 0 ;
}

 

위의 소스는 노드의 개수(V)이고 union연산의 개수(M)일때, 시간복잡도가 O(VM)이다.

따라서 findParent 함수 부분을 다음과 같이 변경해준다.

int findParent(int x) {
    if (parent[x] != x) {
        parent[x] = findParent(parent[x]);
    }
    return parent[x];
}

수정 후에는 시간복잡도가    

이다.

해당 부분의 시간복잡도 증명은 다음에 다루겠다

즉, 최종 소스는 다음과 같다.

<2번째 소스 : 수정 후>

#include <iostream>
#include <vector>

using namespace std;

// 노드의 개수(v)와 간선(e)의 개수
// 노드의 개수는 최대 100000개라고 가정
int v, e;
int parent[100001];

// 특정 원소가 속한 집합을 찾기
int findParent(int x) {
     if (parent[x] != x) {
         parent[x] = findParent(parent[x]);
     }
     return parent[x];
 }

// 두 원소가 속한 집합을 합치기
void unionParent(int x, int y) {
    x = findParent(x);
    y = findParent(y);

    if (x < y) {
        parent[y] = x;
    }
    else {
        parent[x] = y;
    }
}

int main(void) {

    cin >> v >> e;

    // 부모테이블 상에서, 부모를 자기 자신으로 초기화
    for (int i = 1; i <= v; i++) {
        parent[i] = i;
    }

    // union 연산을 각각 수행
    for (int i = 0; i < e; i++) {
        int a, b;
        cin >> a >> b;
        unionParent(a, b);
    }

    // 각 원소가 속한 집할 출력
    cout << "각 원소가 속한 집합: ";
    for (int i = 1 ; i <= v; i++) {
        cout << findParent(i) << " ";
    }
    cout << endl;

    // 부모 테이블 내용 출력
    cout << "부모 테이블: ";
    for (int i = 1 ; i <= v; i++) {
        cout << parent[i] << " ";
    }
    cout << endl;

    return 0 ;
}

문제풀이 전략

 -  다익스트라    : 한 지점에서 다른 특정 지점까지의 최단 경로를 구해야 하는 경우

     1) 시간복잡도 O(E*logV), 다익스트라를 플로이드처럼 모든 지점에서 모든지점까지의 경로를 구할 경우 더 좋은 시간 성능으로 사용할     수 있을 것이라 예상 O(E*logV*V)

     2) 그래프 표현방법 : 인접리스트

     3) 문제 접근 방식 : 그리드 알고리즘

 - 플로이드 워셜 : 모든 지점에서 다른 모든 지점까지의 최단 경로를 모두 개야 하는 경우

     1) 시간복잡도 O(N^3)

     2) 그래프 표현방법 : 인접행렬

     3) 문제 접근 방식 : 다이나믹 프로그래밍

 

플로이드 워셜 알고리즘은 코드가 간결하다.

따라서 N 작고 모든 지점에서 다른 모든 지점까지의 최단경로를 모두 구해야 하는경우 사용하자.

#include <iostream>
#include <vector>
#include <algorithm>

#define INF 1e9

using namespace std;

// 노드의 개수(n), 간선의 개수(m)
int n, m;
int graph[501][501];

int main(void)
{
    cin >> n >> m;

    // 최단거리 테이블 전부 무한대로 초기화
    //fill(graph, graph + 500*500, INF);
    for (int i = 1; i <= 500; i++) {
        fill(graph[i], graph[i]+500, INF);
    }

    // 자기자신으로 가는 거리 0
    for (int i = 1; i <= 500; i++) {
        graph[i][i] = 0;
    }

    // 간선 입력 받아서 세팅
    int s, e, d;
    for (int i = 0; i < m; i++) {
        cin >> s >> e >> d;
        graph[s][e] = d;
    }

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            for (int k = 1; k <= n; k++) {
                graph[j][k] = min(graph[j][k], graph[j][i] + graph[i][k]);
            }
        }
    }

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            if (graph[i][j] == INF) {
                cout << "INF ";
            }
            else {
                cout << graph[i][j] << " ";
            }
        }
        cout << endl;
    }

    return 0;
}

 

1.

Dijkstra 알고리즘을 이론 그대로 구현한 코드다.

시간복잡도는 O(V^2)이다.

#include <iostream>
#include <vector>
#include <algorithm>

#define INF 1e9	// 무한을 의마하는 값으로 10억 설정

using namespace std;

int N, M, start;
vector<pair<int, int> > graph[10001];
int sd[10001];
bool visited[10001] = {false};

int getSmallestNode() {
	int min_value = INF;
	int min_index = 0;
	for (int i = 0; i < N; i++) {
		if (!visited[i] && sd[i] < min_value) {
			min_index = i;
			min_value = sd[i];
		}
	}
	return min_index;
}

void dijkstra(int start) {
	// 시작 노드에 대해서 초기화
	sd[start] = 0;
	visited[start] = true;

	for (int i = 0; i < graph[start].size(); i++) {
		sd[graph[start][i].first] = sd[start] + graph[start][i].second;
	}

	for (int i = 0; i < N-1; i++) {
		//현재 노드와 연결된 다른 노드를 꺼내서, 방문 처리
		int now= getSmallestNode();	
		visited[now] = true;

		// 현재 노드의 연겱된 다른 노드를 확인
		for (int j = 0; j < graph[now].size(); j++) {
			int cost = sd[now] + graph[now][j].second;
			
			sd[graph[now][j].first] = min(sd[graph[now][j].first], cost);
		}
	}
	
}

int main(void)
{
	cin >> N >> M;
	cin >> start;

	for (int i = 0; i < M; i++) {
		int s, e, d;
		cin >> s >> e >> d;
		
		graph[s].push_back(make_pair(e, d));
	}	

	// 최단 거리 테이블을 모두 무한으로 초기화
	fill_n(sd, 10001, INF);
	
	// 다익스트라 알고리즘 수행
	dijkstra(start);

	for (int i = 1; i <= N; i++) {
		// 도달할 수 없는 경우, 무한(INFINITY)이라고 출력
		if (sd[i] == INF)
			cout << "INFINITY" << endl;
		else
			cout << sd[i] << endl;
	}

	return 0;
}

2.

Dijkstra 알고리즘을 우선순위 큐를 이용해 시간복잡도를 줄인 코드다.

우선순위 큐에 pair를 넣으면 pair의 first 값으로 내림차순 정렬을 하므로 pair의 first 값으로 노드간 거리 * -1 (-cost)을 넣어서 정렬한다. 그리고 pair의 second 값으로 도착 노드를 넣는다.

우선순위 큐에서 최단거리 노드를 뽑아 최단거리를 탐색한다.

시간복잡도는 O(Elog(V))이다.

 

1번보다 2번의 시간복잡도가 좋으므로 2번 코드만 보면 될 것 같다.

#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
#include <utility>

#define INF 1e9

using namespace std;

int n, m, start;
vector<pair<int, int> > graph[10001];
int sd[100001];

void dijkstra(int start) {
    priority_queue<pair<int, int> > pq;

    // 시작 노드 거리 0 으로 우선순위 큐에 삽입
    pq.push(make_pair(0, start));
    sd[start] = 0;
    while(!pq.empty()) {
        // 가장 최단거리가 짧은 노드 뽑기
        int dist = -pq.top().first;
        int now = pq.top().second;
        pq.pop();

        // 처리된 적이 있는 노드 무시
        if (sd[now] < dist) continue;
        
        // 현재 노드와 연결된 다른 인접한 노드들을 확인
        for (int i = 0; i < graph[now].size(); i++) {
            int cost = dist + graph[now][i].second; // sd[now] == dist
            // 현재 노드를 거쳐서, 다른 노드로 가는 거리가 더 짧은 경우
            if (cost < sd[graph[now][i].first]) {
                sd[graph[now][i].first] = cost;
                pq.push(make_pair(-cost, graph[now][i].first));
            }
        }
    }
}

int main(void) 
{
    cin >> n >> m >> start;

    int s, e, d;
    for (int i = 0; i < m; i++) {
        cin >> s >> e >> d;
        graph[s].push_back(make_pair(e, d));
    }
       // 최단 거리 테이블을 모두 무한으로 초기화
    fill(sd, sd + 100001, INF);
    
    // 다익스트라 알고리즘을 수행
    dijkstra(start);

    // 모든 노드로 가기 위한 최단 거리를 출력
    for (int i = 1; i <= n; i++) {
        // 도달할 수 없는 경우, 무한(INFINITY)이라고 출력
        if (sd[i] == INF) {
            cout << "INFINITY" << '\n';
        }
        // 도달할 수 있는 경우 거리를 출력
        else {
            cout << sd[i] << '\n';
        }
    }

    return 0;
}

set

키들을 이진 탐색 트리를 기반으로 저장한다. 따라서 자동 정렬되는 컨테이너이고 탐색 시간은 O( log n ) 입니다. 삽입과 제거가 빈번해지면 느리다.

 

undorderd_set

해쉬 테이블 기반으로 저장한다. 어떤 해쉬를 쓰느냐에 따라 다르겠지만, 같은 해쉬값을 중복적으로 저장한다면 최대 O(n)의 시간복잡도를 갖는다. 따라서 탐색시간이 기본적으론 O(1) 이지만 최악의 경우 O(n) 입니다. 자동 정렬되지 않는 set이다.  버킷 때문에 메모리 사용량이 증가할 수 있다.

 

결론 : 알고리즘 문제의 경우 최악의 복잡도가 중요하므로 기본 set을 이용하여 풀자. (이진탐색의 경우 set을 쓰면 좋다.)

 

'C++ STL' 카테고리의 다른 글

[C++][STL] fill, fill_n  (0) 2022.03.01
[C++][STL] Hash Set  (0) 2021.10.21
[C++][STL] sort  (0) 2021.10.04
[C++][STL] tuple, tie  (0) 2021.09.26
[C++][STL] Queue  (0) 2021.09.26

Set은 중복을 찾기에 최적화된 자료구조다.

#include <unordered_set>                // 0. include the library

int main() {
    // 1. initialize a hash set
    unordered_set<int> hashset;   
    // 2. insert a new key
    hashset.insert(3);
    hashset.insert(2);
    hashset.insert(1);
    // 3. delete a key
    hashset.erase(2);
    // 4. check if the key is in the hash set
    if (hashset.count(2) <= 0) {
        cout << "Key 2 is not in the hash set." << endl;
    }
    // 5. get the size of the hash set
    cout << "The size of hash set is: " << hashset.size() << endl; 
    // 6. iterate the hash set
    for (auto it = hashset.begin(); it != hashset.end(); ++it) {
        cout << (*it) << " ";
    }
    cout << "are in the hash set." << endl;
    // 7. clear the hash set
    hashset.clear();
    // 8. check if the hash set is empty
    if (hashset.empty()) {
        cout << "hash set is empty now!" << endl;
    }
}

'C++ STL' 카테고리의 다른 글

[C++][STL] fill, fill_n  (0) 2022.03.01
[C++][STL] set과 unordered_set의 차이  (0) 2021.11.11
[C++][STL] sort  (0) 2021.10.04
[C++][STL] tuple, tie  (0) 2021.09.26
[C++][STL] Queue  (0) 2021.09.26

분할정복 template

Type divide_and_conquer( S ) {
    // (1). Divide the problem into a set of subproblems.
    [S1, S2, ... Sn] = divide(S)

    // (2). Solve the subproblem recursively,
    //   obtain the results of subproblems as [R1, R2... Rn].
    rets = [divide_and_conquer(Si) for Si in [S1, S2, ... Sn]]
    [R1, R2,... Rn] = rets

    // (3). combine the results from the subproblems.
    //   and return the combined result.
    return combine([R1, R2,... Rn])

}

 

Backtracking 전용 template

Type backtrack(candidate) {

    if find_solution(candidate):
        output(candidate)
        return
    
    // iterate all possible candidates.
    for next_candidate in list_of_candidates:
        if is_valid(next_candidate):
            // try this partial candidate solution
            place(next_candidate)
            // given the candidate, explore further.
            backtrack(next_candidate)
            // backtrack
            remove(next_candidate)

}

Trie를 c++로 구현해봤다.

 

원문 : https://leetcode.com/problems/implement-trie-prefix-tree/

  • 트라이(Trie) : 문자열을 저장하고 효율적으로 탐색하기 위한 트리 형태의 자료구조이다.
  • 자식으로 a-z까지 TreeNode들과 문자의 끝을 표시하는 end flag를 갖는다.
class TrieNode {
private:
    bool endFlag;
    TrieNode* next[27 + 'a'];
public:
    TrieNode() {
        endFlag = false;
        memset(next, NULL, sizeof(next));
    }

    TrieNode* getNext(char c) {
        return next[c];
    }

    void setNext(char c) {
        next[c] = new TrieNode();
        // setEndFlag(false);
    }

    bool getEndFlag() {
        return endFlag;
    }

    void setEndFlag(bool endFlag) {
        this->endFlag = endFlag;
    }
};

class Trie {
private:
    TrieNode* root;
public:
    Trie() {
        root = new TrieNode();
    }

    void insert(string word) {
        TrieNode* node = root;
        for (char c : word) {
            if (node->getNext(c)) {
            }
            else {
                node->setNext(c);
            }
            node = node->getNext(c);
        }
        node->setEndFlag(true);
    }

    bool search(string word) {
        TrieNode* node = root;
        for (char c : word) {
            if (node->getNext(c)) {
                node = node->getNext(c);
            }
            else {
                return false;
            }
        }
        return node->getEndFlag();
    }

    bool startsWith(string prefix) {
        TrieNode* node = root;
        for (char c : prefix) {
            if (node->getNext(c)) {
                node = node->getNext(c);
            }
            else {
                return false;
            }
        }
        return true;       
    }
};

/**
 * Your Trie object will be instantiated and called as such:
 * Trie* obj = new Trie();
 * obj->insert(word);
 * bool param_2 = obj->search(word);
 * bool param_3 = obj->startsWith(prefix);
 */

 

+ Recent posts