신장 트리

 

 

주어진 방향성이 없는 그래프의 부분 그래프들 중에서 모든 정점을 포함하는 트리

반드시 연결 그래프여야만 한다

 

최소 신장 트리(Minimum spanning Tree)

 

크루스칼 알고리즘

 

  1. 간선의 크기를 오름차순으로 정렬하고 제일 낮은 비용의 간선을 선택한다
  2. 현재 선택한 건선이 정점 u, v를 연결하는 간선이라고 할 때 만약 u와 v가 같은 그룹이라면 아무것도 하지 않고 넘어간다. u와 v가 다른 그룹이라면 같은 그룹으로 만들고 현재 선택한 간선을 최소 신장 트리에 추가한다
  3. 최소 신장 트리에 V-1개의 간선을 추가시켰다면 과정을 종료하고 그렇지 않다면 그 다음으로 비용이 적은 간선을 선택한 후 2번 과정을 반복한다

 

간선의 가중치가 낮은순으로 정렬 후 Union-Find를 이용하여 간선을 추가해나간다.

간선의 개수가 v-1개가 됐다면 최소 신장 트리가 만들어진다.

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

더보기
int find_root(int x)
{
	if (p[x] < 0) return x; // 루트는 자기 자신이 부모노드이다
	return p[x] = find_root(p[x]);
}

int union_root(int a, int b)
{
	a = find_root(a);
	b = find_root(b);
	if (a == b) return false;
	if (p[a] == p[b]) p[a]--; // union by rank
	if (p[a] < p[b]) p[b] = a;
	else p[a] = b;
	return true;
}

 

프림 알고리즘

 

알고리즘의 흐름

  1. 임의의 정점을 선택해 최소 신장 트리에 추가한다
  2. 최소 신장 트리에 포함된 정점과 최소 신장 트리에 포함되지 않은 정점을 연결하는 간선 중 비용이 가장 작은 것을 최소 신장 트리에 추가한다
  3. 최소 신장 트리에 V-1개의 간선이 추가될 때 까지 2번을 반복한다

 

구현

  1. 임의의 정점을 선택해 최소 신장 트리에 추가하고 해당 정점과 연결된 모든 간선을 우선순위 큐에 추가한다
  2. 우선순위 큐에서 비용이 가장 작은 간선을 선택한다
  3. 만약 해당 간선이 최소 신장 트리에 포함된 두 정점을 연결한다면 아무것도 하지 않고 넘어간다. 해당 간선이 최소 신장 트리에 포함된 정점 u와 포함되지 않은 정점 v를 연결한다면 해당 간선과 정점v를 최소 신장 트리에 추가 후 정점 v와 최소 신장 트리에 포함되지않는 정점을 연결하는 모든 간선을 우선순위 큐에 추가한다
  4. 최소 신장 트리에 V-1개의 간선이 추가될 때 까지 2, 3번을 반복한다

 

시간복잡도는 크루스칼 알고리즘과 마찬가지로 O(ElogE)이다.

더보기
#define X first
#define Y second

int v, e;
vector<pair<int, int>> adj[10005]; // 비용, 정점의 번호
bool chk[10005];
int cnt = 0;
priority_queue<	tuple<int, int, int>,
	vector<tuple<int, int, int>>,
	greater<tuple<int, int, int>>> pq;

chk[1] = 1;
for (auto& nxt : adj[1])
	pq.push({ nxt.X, 1, nxt.Y });
while (cnt < v - 1) {
	int cost, a, b;
	tie(cost, a, b) = pq.top(); pq.pop();
	if (chk[b]) continue;
	cout << cost << ' ' << a << ' ' << b << '\n';
	chk[b] = 1;
	cnt++;
	for (auto& nxt : adj[b]) {
		if (!chk[nxt.Y])
			pq.push({ nxt.X, b, nxt.Y });
	}
}

 

 

연습 문제

 

1197: 최소 스패닝 문제

더보기
#include <bits/stdc++.h>
using namespace std;

vector<int> p(10001, -1);
tuple<int, int, int> edges[100001];
int v, e, ans, cnt;

int find_root(int x)
{
	if (p[x] < 0) return x; // 루트는 자기 자신이 부모노드이다
	return p[x] = find_root(p[x]);
}

int union_root(int a, int b)
{
	a = find_root(a);
	b = find_root(b);
	if (a == b) return false;
	if (p[a] == p[b]) p[a]--; // union by rank. 트리의 깊이를 조정해줌
	if (p[a] < p[b]) p[b] = a;
	else p[a] = b;
	return true;
}

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	
	cin >> v >> e;
	for (int i = 0; i < e; ++i)
	{
		int a, b, c;
		cin >> a >> b >> c;
		edges[i] = {c, a, b};
	}
	sort(edges, edges + e);
	for (int i = 0; i < e; ++i)
	{
		int a, b, c;
		tie(c, a, b) = edges[i];
		if (!union_root(a, b)) continue;
		ans += c;
		cnt++;
		if (cnt >= v - 1) break;
	}
	cout << ans;

	return 0;
}

 

크루스칼 알고리즘 버전

이코테에서 습득했던 구현과 조금 달라서 매치시키는데 이해가 좀 많이걸렸음

이코테 및 다른사람들의 코드를 보면 find의 조건문이 p[x] < 0이 아닌 p[x] != x (혹은 p[x] == x)로 되어있는데 무슨차이가 있냐면 전자는 각 정점의 루트노드를 선언과 동시에 -1로 초기화 시켜서 0 미만인 경우 해당 정점이 루트라는걸 알아내는 것이고 후자는 중간에 각 노드의 루트노드를 자기 자신으로 초기화 시키는 부분이 추가되어있다.

구현상의 차이가 있을뿐 결과는 똑같이 나온다.

 

추가로 union에서의 if(p[a] == p[b]) p[a]--; 이부분은 트리가 균등하지 않게 깊어지는 경우 성능 하락이 발생하는것을 방지해주는 코드이다. 없어도 시간복잡도는 로그스케일로 나오긴 한다

 

 

#include <bits/stdc++.h>
using namespace std;

#define X first
#define Y second

int v, e, ans;
vector<pair<int, int>> adj[10005]; // 비용, 정점의 번호
bool chk[10005];
int cnt = 0;
priority_queue<	tuple<int, int, int>,
	vector<tuple<int, int, int>>,
	greater<tuple<int, int, int>>> pq;

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	
	cin >> v >> e;
	for (int i = 0; i < e; ++i)
	{
		int a, b, c;
		cin >> a >> b >> c;
		adj[a].push_back({ c, b });
		adj[b].push_back({ c, a });
	}
	chk[1] = true;
	for (auto& nxt : adj[1])
		pq.push({ nxt.X, 1, nxt.Y });

	while (cnt < v - 1)
	{
		int a, b, c;
		tie(c, a, b) = pq.top(); pq.pop();
		if (chk[b]) continue;
		chk[b] = true;
		ans += c;
		cnt++;
		for (auto& nxt : adj[b]) {
			if (!chk[nxt.Y])
				pq.push({ nxt.X, b, nxt.Y });
		}
	}

	cout << ans;

	return 0;
}

 

프림 알고리즘 버전

 

'알고리즘 > 바킹독의 실전 알고리즘' 카테고리의 다른 글

[0x1D] 다익스트라 알고리즘  (0) 2022.08.10
[0x1C] 플로이드-워셜 알고리즘  (0) 2022.08.10
[0x1A] 위상 정렬  (0) 2022.08.08
[0x19] 트리  (0) 2022.08.08
[0x18] 그래프  (0) 2022.08.07

방향 그래프에서 간선으로 주어진 정점 간 선후관계를 위배하지 않도록 나열하는 정렬

하나의 그래프에서 여러개의 위상 정렬 결과가 나올 수 있다.

DAG 에서만 사용할 수 있다.

 

구현의 편의를 위한 성질

 

  1. 정점과 간선을 실제로 지울 필요 없이 미리 indegree의 값을 저장했다가 매번 뻗어나가는 정점들의 indegree값만 1 감소시켜도 과정을 수행 가능
  2. indegree가 0인 정점을 구하기 위해 매번 모든 정점들을 다 확인하는 대신 목록을 따로 저장하고 있다가 직전에 제거한 정점에서 연결된 정점들만 추가

 

구현 방법

 

  1. 맨 처음 모든 간선을 읽으며 indegree 테이블을 채운다
  2. indegree가 0인 정점들을 모두 큐에 넣는다
  3. 큐에서 정점을 꺼내어 위상 정렬 결과에 추가한다
  4. 해당 정점으로부터 연결된 모든 정점의 indegree값을 1 감소시킨다. 이 때, indegree가 0이 되었다면 그 정점을 큐에 추가한다
  5. 큐가 빌 때 까지 3, 4번을 반복한다

 

더보기
vector<int> adj[10];
int deg[10];
int n;

queue <int> q;
vector<int> result;
for (int i = 1; i <= n; ++i)
{
    if (deg[i] == 0) q.push(i);
}
while (!q.empty())
{
    int cur = q.front(); q.pop();
    result.push_back(cur);
    for (int nxt : adj[cur])
    {
        deg[nxt]--;
        if (deg[nxt] == 0) q.push(nxt);
    }
}
if (result.size() != n)
    cout << "cycle exists";
else
{
    for (auto& x : result) cout << x << ' ';
}

 

연습 문제

 

2252: 줄 세우기

더보기
#include <bits/stdc++.h>
using namespace std;

vector<int> adj[32001];
queue<int> q;
vector<int> ans;
int ind[32001];
int n, m;

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> n >> m;
	for (int i = 1; i <= m; ++i)
	{
		int a, b;
		cin >> a >> b;
		adj[a].push_back(b);
		ind[b]++;
	}

	for (int i = 1; i <= n; ++i)
	{
		if (!ind[i]) q.push(i);
	}

	while (!q.empty())
	{
		int cur = q.front();
		q.pop();
		ans.push_back(cur);
		for (auto& nxt : adj[cur])
		{
			ind[nxt]--;
			if (!ind[nxt]) q.push(nxt);
		}
	}

	for (auto& i : ans) cout << i << ' ';

	return 0;
}

 

따로 ans에 결과를 저장하여 출력할 필요 없이 q의 루프 내에서 바로 출력해주면 된다

 

기본 문제

 

2623: 음악프로그램

더보기
#include <bits/stdc++.h>
using namespace std;

vector<int> adj[1001];
vector<int> ans;
queue<int> q;
int ind[1001];
int n, m;

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> n >> m;
	for (int i = 1; i <= m; ++i)
	{
		int k;
		queue<int> tq;
		cin >> k;
		while (k--)
		{
			int a;
			cin >> a;
			tq.push(a);
		}
		int t = tq.front(); tq.pop();
		while (!tq.empty())
		{
			int nxt = tq.front(); tq.pop();
			adj[t].push_back(nxt);
			ind[nxt]++;
			t = nxt;
		}
	}

	for (int i = 1; i <= n; ++i)
	{
		if (!ind[i]) q.push(i);
	}

	while (!q.empty())
	{
		int cur = q.front();
		q.pop();
		ans.push_back(cur);
		for (auto& nxt : adj[cur])
		{
			ind[nxt]--;
			if (!ind[nxt]) q.push(nxt);
		}
	}

	if (ans.size() != n)
		cout << 0;
	else
	{
		for (auto& i : ans) cout << i << '\n';
	}

	return 0;
}

 

21276: 계보 복원가 호석

더보기
#include <bits/stdc++.h>
using namespace std;

int n, m;
map<string, int> h;
vector<int> adj[1001];
vector<string> sv;
vector<string> ans[1001];
int ind[1001];

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> n;
	sv.push_back("");
	for (int i = 1; i <= n; ++i)
	{
		string s;
		cin >> s;
		sv.push_back(s);
	}
	sort(sv.begin()+1, sv.end());
	for(int i = 1; i <= n; ++i) h.insert({sv[i], i});

	cin >> m;
	while (m--)
	{
		string s1, s2;
		cin >> s1 >> s2;
		adj[h[s2]].push_back(h[s1]);
		ind[h[s1]]++;
	}

	vector<string> t;
	queue<int> q;
	for (int i = 1; i <= n; ++i)
	{
		if (!ind[i])
		{
			q.push(i);
			t.push_back(sv[i]);
		}
	}
	sort(t.begin(), t.end());
	cout << t.size() << '\n';
	for (auto& i : t) cout << i << ' ';
	cout << '\n';

	while (!q.empty())
	{
		int cur = q.front(); q.pop();
		for (auto& nxt : adj[cur])
		{
			ind[nxt]--;
			if (!ind[nxt])
			{
				q.push(nxt);
				ans[cur].push_back(sv[nxt]);
			}
		}
	}

	for (int i = 1; i <= n; ++i)
	{
		cout << sv[i] << ' ' << ans[i].size() << ' ';
		if (ans[i].size() > 0)
		{
			t.clear();
			for (auto& k : ans[i]) t.push_back(k);
			sort(t.begin(), t.end());
			for (auto& k : t) cout << k << ' ';
		}
		cout << '\n';
	}

	return 0;
}

 

그래프를 그려보면 진출차수가 0인게 루트가 되지만 그래프 방향을 반대로 바꾸어서 진입차수를 비교하는걸로 바꿈

사전순 출력이 필요해서 컨테이너들이 추가로 여러개 쓰임

단순히 자식의 숫자만 출력하면 최초 진입차수가 2 이상인 자식들까지 포함되기 때문에 진입차수가 1인 자식들만 따로 ans에 추가해서 출력해준다

'알고리즘 > 바킹독의 실전 알고리즘' 카테고리의 다른 글

[0x1C] 플로이드-워셜 알고리즘  (0) 2022.08.10
[0x1B] 최소 신장 트리  (0) 2022.08.08
[0x19] 트리  (0) 2022.08.08
[0x18] 그래프  (0) 2022.08.07
[0x17] 우선순위 큐  (0) 2022.08.07

트리의 정의

 

무방향이면서 사이클이 없는 연결 그래프

= 연결 그래프이면서 임의의 간선을 제거하면 연결 그래프가 아니게 되는 그래프

= 임의의 두 점을 연결하는 정점이 중복해서 나오지 않는 경로가 유일한 그래프

= V개의 정점을 가지고 V-1개의 간선을 가지는 연결 그래프

= 사이클이 없는 연결 그래프이면서 임의의 간선을 추가하면 사이클이 생기는 그래프

= V개의 정점을 가지고 V-1개의 간선을 가지는 사이클이 없는 그래프

 

정점이 1개이고 간선이 없는 그래프도 트리에 속한다.

 

트리의 BFS와 DFS

 

BFS/DFS의 과정 속에서 자신의 자식 정점들을 전부 넣어주기만 하면 된다.

더보기
// 부모 배열 채우기
vector<int> adj[10];
int p[10];
void bfs(int root) {
	queue<int> q;
	q.push(root);
	while (!q.empty()) {
		int cur = q.front();
		q.pop();
		cout << cur << ' ';
		for (int nxt : adj[cur]) {
			if (p[cur] == nxt) continue;
			q.push(nxt);
			p[nxt] = cur;
		}
	}
}

 

// 부모와 depth 배열 채우기
#include <bits/stdc++.h>
using namespace std;

vector<int> adj[10];
int p[10];
int depth[10];
void bfs(int root) {
	queue<int> q;
	q.push(root);
	while (!q.empty()) {
		int cur = q.front();
		q.pop();
		cout << cur << ' ';
		for (int nxt : adj[cur]) {
			if (p[cur] == nxt) continue;
			q.push(nxt);
			p[nxt] = cur;
			depth[nxt] = depth[cur] + 1;
		}
	}
}

 

DFS는 스택으로 바꿔주기만 하면 된다

 

이진 트리의 순회

 

 

레벨 순회

 

높이 순으로 방문하는 순회이다.

BFS로 구현하면 되지만 이진 트리에 맞게 현재 보는 노드의 자식만 push 할 수 있게 코드를 살짝 수정해 주어야 한다.

 

1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 순으로 방문한다

 

전위 순회 (Pre-order)

 

  1. 현재 정점을 방문한다
  2. 왼쪽 서브트리를 전위 순회한다
  3. 오른쪽 서브트리를 전위 순회한다

 

1 - 2 - 4 - 5 - 8 - 3 - 6 - 7 순으로 방문한다

DFS와 방문 순서가 동일하다. 재귀로 간단히 구현할 수 있다.

 

중위 순회 (In-order)

 

  1. 왼쪽 서브트리를 중위 순회한다
  2. 현재 정점을 방문한다
  3. 오른쪽 서브트리를 중위 순회한다

 

4 - 2 - 5 - 8 - 1 - 6 - 3 - 7 순으로 방문한다

트리의 모양에서 가장 왼쪽에 있는 것 부터 방문한다.

만약 트리가 이진 탐색 트리였다면 자연스럽게 크기 순으로 방문하게 된다

 

후위 순회 (Post-order)

 

  1. 왼쪽 서브트리를 후위 순회한다
  2. 오른쪽 서브트리를 후위 순회한다
  3. 현재 정점을 방문한다

 

4 - 8 - 5 - 2 - 6 - 7 - 3 - 1 순으로 방문한다.

딱히 쉽게 비유할 만한게 없다

 

연습 문제

 

11725: 트리의 부모 찾기

더보기
vector<int> tree[100001];
int p[100001];
int n;

/*
void dfs(int v)
{
	for (auto& nxt : tree[v])
	{
		if (nxt == p[v]) continue;
		p[nxt] = v;
		dfs(nxt);
	}
}
*/

void bfs(int v)
{
	queue<int> q;
	q.push(v);

	while (!q.empty())
	{
		int cur = q.front();
		q.pop();
		for (auto& nxt : tree[cur])
		{
			if (p[cur] == nxt) continue;
			p[nxt] = cur;
			q.push(nxt);
		}
	}
}

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> n;
	for (int i = 1; i < n; ++i)
	{
		int a, b;
		cin >> a >> b;
		tree[a].push_back(b);
		tree[b].push_back(a);
	}

	dfs(1);

	for (int i = 2; i <= n; ++i)
		cout << p[i] << '\n';

	return 0;
}

 

dfs도 재귀방식으로 구현해두었다.

 

1991: 트리 순회

더보기
#include <bits/stdc++.h>
using namespace std;

int n;
char lc[27], rc[27];

void preorder(int p)
{
	cout << char(p + 64);
	if (lc[p] != 0) preorder(lc[p]);
	if (rc[p] != 0) preorder(rc[p]);	
}

void inorder(int p)
{
	if (lc[p] != 0) inorder(lc[p]);
	cout << char(p + 64);
	if (rc[p] != 0) inorder(rc[p]);
}

void postorder(int p)
{
	if (lc[p] != 0) postorder(lc[p]);
	if (rc[p] != 0) postorder(rc[p]);
	cout << char(p + 64);
}

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> n;
	for (int i = 0; i < n; ++i)
	{
		char a, b, c;
		cin >> a >> b >> c;
		if (b != '.') lc[a - 64] = b - 64;
		if (c != '.') rc[a - 64] = c - 64;
	}

	preorder(1);
	cout << '\n';
	inorder(1);
	cout << '\n';
	postorder(1);	

	return 0;
}

 

A=1로 관리하는게 편하다

 

기본 문제

 

4803: 트리

더보기
#include <bits/stdc++.h>
using namespace std;

int v, e;
vector<int> graph[501];
vector<bool> vis(501);

void init_graph(int& n)
{
	for (int i = 1; i <= n; ++i)
	{
		graph[i].clear();
		vis[i] = false;
	}
}

bool bfs(int& t)
{
	queue<int> q;
	q.push(t);
	vis[t] = true;
	int vc = 0, ec = 0;
	while (!q.empty())
	{
		int cur = q.front();
		q.pop();
		vc++;
		ec += graph[cur].size();
		for (auto& nxt : graph[cur])
		{
			if (vis[nxt]) continue;
			q.push(nxt);
			vis[nxt] = true;
		}
	}
	return vc*2 > ec;
}

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int idx = 1;
	while (true)
	{
		cin >> v >> e;
		if (v + e == 0) return 0;

		init_graph(v);

		while (e--)
		{
			int a, b;
			cin >> a >> b;
			graph[a].push_back(b);
			graph[b].push_back(a);
		}

		int ans = 0;
		for (int i = 1; i <= v; ++i)
		{
			if (!vis[i])
			{
				if (bfs(i)) ans++;
			}
		}
		// 트리가 없으면
		if (ans == 0) cout << "Case " << idx << ": " << "No trees." << '\n';
		// 트리가 한개면
		else if (ans == 1) cout << "Case " << idx << ": " << "There is one tree." << '\n';
		// 트리가 여러개면
		else cout << "Case " << idx << ": " << "A forest of " << ans << " trees." << '\n';
		idx++;
	}

	return 0;
}

 

정석적으로 푼 문제는 아니다. 다른 사람들의 코드를 보고 이해할 필요가 있다.

'알고리즘 > 바킹독의 실전 알고리즘' 카테고리의 다른 글

[0x1B] 최소 신장 트리  (0) 2022.08.08
[0x1A] 위상 정렬  (0) 2022.08.08
[0x18] 그래프  (0) 2022.08.07
[0x17] 우선순위 큐  (0) 2022.08.07
[0x16] 이진 검색 트리  (0) 2022.08.06

정점과 간선으로 이루어진 자료구조

 

차수 : 각 정점에 대해서 간선으로 연결된 이웃한 정점의 개수

방향 그래프 : 두 정점 사이에 진출차수(indegree)와 진입차수(outdegree)가 존재하는 그래프

무방향 그래프 : 두 정점 사이에 방향이 존재하지 않는 그래프

순환 그래프 : 사이클이 존재하는 그래프

비순환 그래프 : 사이클이 존재하지 않는 그래프

완전 그래프 : 모든 정점끼리 간선으로 연결된 그래프

연결 그래프 : 임의의 두 정점 사이에 경로가 항상 존재하는 그래프

단순 그래프 : 두 정점 사이의 간선이 1개 이하이고 루프가 존재하지 않는 그래프

 

그래프는 꼭 연결되어 있을 필요도 없고, 두 정점 사이의 간선이 반드시 1개 이하일 필요도 없으며 간선이 반드시 두 정점을 연결해야 할 필요도 없다.

 

그래프의 표현법

 

정점이 V개, 간선이 E개일 때,

 

인접 행렬 : 어떤 두 정점이 연결되어 있는지를 O(1)에 알 수 있다. 2차원 배열을 사용하므로 O(V²)의 공간을 필요로 한다.

어떤 정점에 연결되어 있는 모든 정점의 목록을 알아내고 싶을 때도 개수와 무관하게 O(V)의 시간복잡도를 가진다.

연결여부를 많이 확인할 때, 혹은 간선의 개수가 V²에 가까울 때 효율적이다.

 

인접 리스트 : O(V+E)의 공간을 필요로 한다.

특정 정점에 연결된 모든 정점들을 자주 확인할 때, 혹은 간선의 개수가 V²보다 훨씬 적을 때 효율적이다.

 

일반적인 그래프 문제에서 두 정점이 연결되어있는지를 반복적으로 확인해야 하는 경우는 잘 없다.

 

BFS

 

진행 순서

  1. 시작하는 정점을 큐에 넣고 방문했다는 표시를 남긴다
  2. 큐에서 정점을 꺼내어 그 정점과 연결된 모든 정점들에 대해 3번을 진행한다
  3. 해당 정점을 이전에 방문했다면 아무 것도 하지 않고, 처음으로 방문했다면 방문했다는 표시를 남기고 해당 칸을 큐에 삽입한다
  4. 큐가 빌 때 까지 2번을 반복한다

 

모든 정점이 큐에 최대 한번씩 들어가므로 인접 리스트에서 O(V+E), 인접 행렬에서 O(V²)의 시간복잡도를 가진다.

 

더보기
// 연결 그래프에서의 순회
vector<int> adj[10];
bool vis[10];
void bfs() {
	queue<int> q;
	q.push(1);
	vis[1] = true;
	while (!q.empty()) {
		int cur = q.front();
		q.pop();
		cout << cur << ' ';
		for (auto& nxt : adj[cur]) {
			if (vis[nxt]) continue;
			q.push(nxt);
			vis[nxt] = true;
		}
	}
}

 

// 연결 그래프에서 1번 정점과의 거리
vector<int> adj[10];
int dist[10];
void bfs() {
	fill(dist, dist + 10, -1);
	queue<int> q;
	q.push(1);
	dist[1] = 0;
	while (!q.empty()) {
		int cur = q.front();
		q.pop();
		cout << cur << ' ';
		for (auto& nxt : adj[cur]) {
			if (dist[nxt] != -1) continue;
			q.push(nxt);
			dist[nxt] = dist[cur]+1;
		}
	}
}

 

// 연결 그래프가 아닐 때 순회
vector<int> adj[10];
bool vis[10];
int v = 9; // 정점의 수
void bfs() {
	queue<int> q;
	for (int i = 1; i <= v; ++i)
	{
		if (vis[i]) continue;
		q.push(1);
		vis[1] = true;
		while (!q.empty()) {
			int cur = q.front();
			q.pop();
			cout << cur << ' ';
			for (auto& nxt : adj[cur]) {
				if (vis[nxt]) continue;
				q.push(nxt);
				vis[nxt] = true;
			}
		}
	}
}

 

1926: 그림 문제를 해결할 때 사용한 방법을 그대로 가져오면 된다.

 

DFS

 

진행 순서

  1. 시작하는 정점을 스택에 넣고 방문했다는 표시를 남긴다
  2. 스택에서 정점을 꺼내어 그 정점과 연결된 모든 정점들에 대해 3번을 진행한다
  3. 해당 정점을 이전에 방문했다면 아무 것도 하지 않고, 처음으로 방문했다면 방문햤다는 표시를 남기고 해당 칸을 스택에 삽입한다
  4. 스택이 빌 때 까지 2번을 반복한다

 

모든 정점이 스택에 최대 한번씩 들어가므로 인접 리스트에서 O(V+E), 인접 행렬에서 O(V²)의 시간복잡도를 가진다.

 

더보기

비재귀적으로 구현하는것은 bfs에서 큐를 스택으로 바꾸면 된다.

 

// 연결 그래프에서의 순회, 재귀
vector<int> adj[10];
bool vis[10];
void dfs(int cur) {
	vis[cur] = true;
	cout << cur << ' ';
	for (auto& nxt : adj[cur])
	{
		if (vis[nxt]) continue;
		dfs(nxt);
	}
}

 

스택 메모리의 제한이 별도로 작게 설정된 곳에서는 재귀 대신 스택을 써서 구현해야 한다.

 

비재귀 DFS는 순회를 잘 수행하지만 엄밀히 말해서 관념적으로 생각하는 DFS와 방문 순서가 다르다.

재귀 DFS와 방문순서를 같게 하려면 방문 전에 방문표시를 남기고 push를 수행해야 한다

더보기
// 연결 그래프에서의 순회, 비재귀 (재귀와 같은 방문순서)
vector<int> adj[10];
bool vis[10];
void dfs() {
	stack<int> s;
	s.push(1);
	while (!s.empty())
	{
		int cur = s.top();
		s.pop();
		if (vis[cur]) continue;
		vis[cur] = true;
		cout << cur << ' ';
		for (auto& nxt : adj[cur])
		{
			if (vis[nxt]) continue;
			s.push(nxt);
		}
	}
}

 

코딩테스트 레벨에서는 bfs대신 dfs를 써야하는 상황이 별로 없다.

 

연습 문제

 

11724: 연결 요소의 개수

더보기
#include <bits/stdc++.h>
using namespace std;

vector<int> adj[1002];
bool vis[1002];
int v, e, ans;

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> v >> e;
	while (e--)
	{
		int a, b;
		cin >> a >> b;
		adj[a].push_back(b);
		adj[b].push_back(a);
	}

	for (int i = 1; i <= v; ++i)
	{
		if (vis[i]) continue;
		queue<int> q;
		q.push(i);
		vis[i] = true;
		while (!q.empty())
		{
			int cur = q.front();
			q.pop();
			for (auto& elem : adj[cur])
			{
				if (vis[elem]) continue;
				q.push(elem);
				vis[elem] = true;
			}
		}
		ans++;
	}
	cout << ans;

	return 0;
}

 

무방향 그래프이므로 양쪽에 push_back을 해주어야 한다.

 

1260: DFS와 BFS

더보기
#include <bits/stdc++.h>
using namespace std;

vector<int> adj[1002];
bool bvis[1002];
bool dvis[1002];
int n, m, v;

void dfs(int s)
{
	dvis[s] = true;
	cout << s << ' ';
	for (auto& elem : adj[s])
	{
		if (dvis[elem]) continue;
		dfs(elem);
	}
}

void bfs(int& s)
{
	queue<int> q;
	q.push(s);
	bvis[s] = true;
	while (!q.empty())
	{
		int cur = q.front();
		q.pop();
		cout << cur << ' ';
		for (auto& elem : adj[cur])
		{
			if (bvis[elem]) continue;
			q.push(elem);
			bvis[elem] = true;
		}
	}
	cout << '\n';
}

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> n >> m >> v;
	while (m--)
	{
		int a, b;
		cin >> a >> b;
		adj[a].push_back(b);
		adj[b].push_back(a);
	}
	
	for (int i = 1; i <= n; ++i)
		sort(adj[i].begin(), adj[i].end());

	dfs(v);
	cout << '\n';
	bfs(v);

	return 0;
}

 

스택은 재귀방식으로 편하게 구현해봤다

 

기본 문제

 

2606: 바이러스

더보기
#include <bits/stdc++.h>
using namespace std;

vector<int> adj[101];
bool vis[101];
int v, e, ans;

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> v >> e;
	while (e--)
	{
		int a, b;
		cin >> a >> b;
		adj[a].push_back(b);
		adj[b].push_back(a);
	}

	queue<int> q;
	q.push(1);
	vis[1] = true;
	while (!q.empty())
	{
		int cur = q.front();
		q.pop();
		for (auto& nxt : adj[cur])
		{
			if (vis[nxt]) continue;
			q.push(nxt);
			vis[nxt] = true;
			ans++;
		}
	}
	cout << ans;

	return 0;
}

 

기초적인 bfs 문제

 

5567: 결혼식

더보기
#include <bits/stdc++.h>
using namespace std;

vector<int> adj[501];
int vis[501];
int v, e, ans;

void bfs(int st)
{
	fill(vis + 1, vis + v + 1, -1);
	queue<int> q;
	q.push(st);
	vis[st] = 0;
	while (!q.empty())
	{
		int cur = q.front();
		q.pop();
		for (auto& nxt : adj[cur])
		{
			if (vis[nxt] != -1) continue;
			vis[nxt] = vis[cur] + 1;
			q.push(nxt);
			if (vis[nxt] > 2) return;
			ans++;
		}
	}
}

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> v >> e;
	while (e--)
	{
		int a, b;
		cin >> a >> b;
		adj[a].push_back(b);
		adj[b].push_back(a);
	}
	bfs(1);
	cout << ans;

	return 0;
}

 

차수가 2 이하인 간선과 연결된 정점만 카운트

 

11403: 경로 찾기

더보기
#include <bits/stdc++.h>
using namespace std;

int adj[101][101];
int ans[101][101];
int v;

void bfs(int& st)
{
	bool vis[101] = {};
	queue<int> q;
	q.push(st); // 순환할수도 있으므로 여기서 방문처리를 하지 않는다
	while (!q.empty())
	{
		int cur = q.front();
		q.pop();
		for (int i = 1; i <= v; ++i)
		{
			if (vis[i] && cur == i)
			{
				ans[st][i] = 1;
				continue;
			}
			if (adj[cur][i] && !vis[i])
			{
				q.push(i);
				vis[i] = true;
				ans[st][i] = 1;
			}
		}
	}
}

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> v;

	for (int i = 1; i <= v; ++i)
	{
		for (int j = 1; j <= v; ++j)
		{
			cin >> adj[i][j];
		}
	}

	for (int i = 1; i <= v; ++i) bfs(i);
	for (int i = 1; i <= v; ++i)
	{
		for (int j = 1; j <= v; ++j) cout << ans[i][j] << ' ';
		cout << '\n';
	}

	return 0;
}

 

2660: 회장뽑기

더보기
#include <bits/stdc++.h>
using namespace std;

const int INF = 1e9;
int adj[51][51];
int v;

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> v;

	for (int i = 1; i <= v; ++i)
	{
		fill(adj[i] + 1, adj[i] + v + 1, INF);
		adj[i][i] = 0;
	}

	while (true)
	{
		int a, b;
		cin >> a >> b;
		if (a + b < 0) break;
		adj[a][b] = 1;
		adj[b][a] = 1;
	}

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

	map<int, vector<int>> m;
	for (int i = 1; i <= v; ++i)
	{
		int mx = 0;
		for (int j = 1; j <= v; ++j) mx = max(mx, adj[i][j]);
		m[mx].push_back(i);
	}

	auto& t = m.begin()->second;
	sort(t.begin(), t.end());
	cout << m.begin()->first << ' ' << t.size() << '\n';
	for (auto& i : t) cout << i << ' ';

	return 0;
}

'알고리즘 > 바킹독의 실전 알고리즘' 카테고리의 다른 글

[0x1A] 위상 정렬  (0) 2022.08.08
[0x19] 트리  (0) 2022.08.08
[0x17] 우선순위 큐  (0) 2022.08.07
[0x16] 이진 검색 트리  (0) 2022.08.06
[0x15] 해시  (0) 2022.08.06

우선순위 큐

 

pop을 할 때 가장 먼저 들어온 원소가 나오는 대신 우선순위가 가장 높은 원소가 나오는 큐

원소의 추가 O(logN)

우선순위가 가장 높은 원소의 확인 O(1)

우선순위가 가장 높은 원소의 제거가 O(logN)

 

원소가 들어오면 정점의 왼쪽 자식부터 채워지기 때문에 항상 균형이 잘 맞는 이진 트리가 된다.

 

최대 힙, 최소 힙이 있다.

가장 크거나 작은값의 확인은 O(1)에 확인이 가능하지만 N번째의 크거나 작은값의 확인은 불가능하다.

 

STL priority_queue

 

기본적으로 최대 힙이다.

최소 힙으로 사용하려면 선언시 priority_queue<int, vector<int> greator<int>>로 선언하면 된다.

 

priority_queue에서 할 수 있는건 set에서도 할 수 있고 시간복잡도도 동일하지만 set보다 수행속도가 빠르고 공간을 적게 차지한다.

 

custom compare를 사용할 때 주의할점은 vector 등의 동적 배열에서의 sort custom compare는 컨테이너의 front가 의도한 대로의 최소/최대값이 되지만 우선순위 큐에서는 top이 컨테이너의 back이기 때문에 반대로 사용해야 한다.

pop을 하면 back이 튀어나오는 것이다.

그렇기 때문에 최대/최소값을 top에 두고싶으면 false를 반환해서 뒤로 계속 보내야 한다. (vector는 true)

내림차순 정렬시 앞의 원소보다 뒤의 원소보다 크다면 false, 오름차순 정렬시 true를 반환해야 한다.

 

sort와 우선순위 큐의 custom compare가 함수와 구조체로 다른 이유는 sort는 함수지만 우선순위 큐는 컨테이너이기 때문이다.

더보기
struct comp
{
	bool operator()(const int& a, const int& b)
	{
		// return a > b; 최소힙
    		// return a < b; 최대힙
	}
};

 

연습 문제

 

11286: 절댓값 힙

더보기
#include <bits/stdc++.h>
using namespace std;

struct comp
{
	bool operator()(const int& a, const int& b)
	{
		const int ta = abs(a);
		const int tb = abs(b);
		if (ta == tb) return a > b;
		return ta > tb;
	}
};

priority_queue<int, vector<int>, comp> q;
int n, x;

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> n;
	while (n--)
	{
		cin >> x;
		if (x != 0)	q.push(x);
		else
		{
			if (!q.empty())
			{
				cout << q.top() << '\n';
				q.pop();
			}
			else cout << 0 << '\n';
		}
	}

	return 0;
}

 

custom compare 함수의 조건을 잘 생각해야한다. sort의 custom compare와는 조건이 반대다.

 

1715: 카드 정렬하기

더보기
#include <bits/stdc++.h>
using namespace std;

priority_queue<int, vector<int>, greater<>> q;
int n, x, ans;

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> n;
	while (n--)
	{
		cin >> x;
		q.push(x);
	}
	while (q.size() > 1)
	{
		int a = q.top();
		q.pop();
		int b = q.top();
		q.pop();
		ans += a + b;
		q.push(a + b);
	}
	cout << ans;

	return 0;
}

 

의외로 간단한 문제였는데 너무 생각을 짧게했는지 해답에 도달하지 못했음

'알고리즘 > 바킹독의 실전 알고리즘' 카테고리의 다른 글

[0x19] 트리  (0) 2022.08.08
[0x18] 그래프  (0) 2022.08.07
[0x16] 이진 검색 트리  (0) 2022.08.06
[0x15] 해시  (0) 2022.08.06
[0x14] 투 포인터  (0) 2022.08.05

+ Recent posts