하나의 시작점으로부터 다른 모든 정점까지의 최단 거리를 구하는 알고리즘

 

우선순위 큐를 이용한 다익스트라 알고리즘의 구현

 

  1. 우선순위 큐에 (0, 시작점)을 추가한다
  2. 우선순위 큐에서 거리가 가장 작은 원소를 선택하고 해당 거리가 최단 거리 테이블에 있는 값과 다른 경우 넘어간다
  3. 원소가 가리키는 정점을 v라고 할 때, v와 이웃한 정점들에 대해 최단 거리 테이블 값보다 v를 거쳐가는 것이 더 작은 값을 가질 경우 최단 거리 테이블의 값을 갱신하고 우선순위 큐에 (거리, 이웃한 정점의 번호)를 추가한다
  4. 우선순위 큐가 빌 때 까지 2, 3번을 반복한다

 

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

const int INF = (int)1e9;
vector<int> dist(20001, INF);
vector<pair<int, int>> adj[20001];
int n, m, k;

void dijkstra(int st)
{
	priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> q;
	q.push({ 0, st });
	dist[st] = 0;

	while (!q.empty())
	{
		auto cur = q.top(); q.pop();
		if (cur.first > dist[cur.second]) continue; // 예제 코드에서는 !=로 처리하던데..

		for (auto& nxt : adj[cur.second])
		{
			int cost = nxt.first + dist[cur.second];
            int dest = nxt.second;
			if (cost < dist[dest])
			{
				dist[dest] = cost;
				q.push({ cost, nxt.second });
			}
		}
	}
	return;
}

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin >> n >> m >> k;
	while (m--)
	{
		int u, v, w;
		cin >> u >> v >> w;
		adj[u].push_back({w, v});
	}

	dijkstra(k);
	for (int i = 1; i <= n; ++i)
	{
		if (dist[i] != INF) cout << dist[i] << '\n';
		else cout << "INF" << '\n';
	}

	return 0;
}

 

구현의 핵심 내용

  1. 최단 거리 테이블 사용 (INF로 초기화)
  2. 우선순위 큐 사용
  3. 이미 처리된 간선은 continue로 회피하여 시간복잡도 줄이기

 

경로 복원

 

최단거리 테이블을 갱신할 때 pre 테이블도 같이 갱신한다.

이후 도착 정점부터 출발 정점까지 역추적하여 정점들의 방문 순서를 알아낸다.

역추적이기 때문에 역순으로 뒤집어야 출발 정점-도착 정점의 올바른 순서가 된다.

 

연습 문제

 

1753: 최단경로

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

const int INF = (int)1e9;
vector<int> dist(20001, INF);
vector<pair<int, int>> adj[20001];
int n, m, k;

void dijkstra(int st)
{
	priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> q;
	q.push({ 0, st });
	dist[st] = 0;

	while (!q.empty())
	{
		auto cur = q.top(); q.pop();
		if (cur.first > dist[cur.second]) continue;

		for (auto& nxt : adj[cur.second])
		{
			int cost = nxt.first + dist[cur.second];
            int dest = nxt.second;
			if (cost < dist[dest])
			{
				dist[dest] = cost;
				q.push({ cost, nxt.second });
			}
		}
	}
	return;
}

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin >> n >> m >> k;
	while (m--)
	{
		int u, v, w;
		cin >> u >> v >> w;
		adj[u].push_back({w, v});
	}

	dijkstra(k);
	for (int i = 1; i <= n; ++i)
	{
		if (dist[i] != INF) cout << dist[i] << '\n';
		else cout << "INF" << '\n';
	}

	return 0;
}

 

다익스트라의 구현 코드와 동일하다

 

11779: 최소비용 구하기2

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

const int INF = (int)1e9;
vector<int> dist(1001, INF);
vector<int> path;
vector<pair<int, int>> adj[1001];
int pre[1001];
int n, m, st, en;

void dijkstra(int k)
{
	priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> q;
	q.push({ 0, k });
	dist[k] = 0;

	while (!q.empty())
	{
		auto cur = q.top(); q.pop();
		if (cur.first > dist[cur.second]) continue;

		for (auto& nxt : adj[cur.second])
		{
			int cost = nxt.first + dist[cur.second];
			int dest = nxt.second;
			if (cost < dist[dest])
			{
				dist[dest] = cost;
				q.push({ cost, nxt.second });
				pre[nxt.second] = cur.second;
			}
		}
	}
	return;
}

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin >> n >> m;
	while (m--)
	{
		int u, v, w;
		cin >> u >> v >> w;
		adj[u].push_back({w, v});
	}

	cin >> st >> en;
	dijkstra(st);

	int cur = en;
	while (cur != st)
	{
		path.push_back(cur);
		cur = pre[cur];
	}
	path.push_back(st);
	reverse(path.begin(), path.end());

	cout << dist[en] << '\n';
	cout << path.size() << '\n';
	for (auto& p : path) cout << p << ' ';

	return 0;
}

 

1-3-5, 1-4-5 둘다 비용이 4로 같기때문에 비교조건을 어떻게 주었냐에 따라 출력이 다를수는 있지만 모두 답으로 인정된다.

경로 추적은 도착지부터 역순으로 이뤄지고 마지막에 출발 정점을 추가하여 뒤집어 출력해야한다.

 

 

기본 문제

 

1238: 파티

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

const int INF = (int)1e9;
vector<pair<int, int>> adj[1001];
int n, m, x, mx;

int dijkstra(int st, int en)
{
	if (st == en) return 0;

	vector<int> dist(1001, INF);
	priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> q;
	q.push({ 0, st });
	dist[st] = 0;

	while (!q.empty())
	{
		auto cur = q.top(); q.pop();
		if (cur.second == en) return cur.first;
		if (cur.first > dist[cur.second]) continue;

		for (auto& nxt : adj[cur.second])
		{
			int cost = nxt.first + dist[cur.second];
			int dest = nxt.second;
			if (cost < dist[dest])
			{
				dist[dest] = cost;
				q.push({ cost, nxt.second });
			}
		}
	}
}

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin >> n >> m >> x;
	while (m--)
	{
		int u, v, w;
		cin >> u >> v >> w;
		adj[u].push_back({w, v});
	}
	for (int i = 1; i <= n; ++i)
		mx = max(mx, dijkstra(i, x) + dijkstra(x, i));
	cout << mx;

	return 0;
}

 

플로이드-워셜같이 출발-도착지를 각각 돌려서 값을 더하면 된다

 

1504: 특정한 최단 경로

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

const int INF = (int)1e9;
vector<pair<int, int>> adj[801];
int n, m, mn, cnt;

int dijkstra(int st, int en)
{
	vector<int> dist(1001, INF);
	priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> q;
	q.push({ 0, st });
	dist[st] = 0;

	while (!q.empty())
	{
		auto cur = q.top(); q.pop();
		if (cur.second == en) return cur.first;
		if (cur.first > dist[cur.second]) continue;

		for (auto& nxt : adj[cur.second])
		{
			int cost = nxt.first + dist[cur.second];
			int dest = nxt.second;
			if (cost < dist[dest])
			{
				dist[dest] = cost;
				q.push({ cost, nxt.second });
			}
		}
	}
	cnt++;
	return 0;
}

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin >> n >> m;
	while (m--)
	{
		int u, v, w;
		cin >> u >> v >> w;
		adj[u].push_back({ w, v });
		adj[v].push_back({ w, u });
	}
	int v1, v2;
	cin >> v1 >> v2;

	mn = min(dijkstra(1, v1) + dijkstra(v1, v2) + dijkstra(v2, n),
			 dijkstra(1, v2) + dijkstra(v2, v1) + dijkstra(v1, n));
	if (cnt) cout << -1;
	else cout << mn;

	return 0;
}

 

반례를 찾느라 머리가 터지는줄 알았다.

반례의 종류

  1. 간선이 아예 주어지지 않은 경우
  2. 두 정점이 연결되어있지 않은 경우 (ex:A,B,C,D에서 A-B, B-C는 간선이 있지만 C-D가 없는경우. 마지막의 경우에서 0이 반환되지만 이미 앞에서 간선들의 비용이 반환되어 합산되기때문에 0으로 처리하면 안됨)

 

long long을 써서 연결된 간선이 없으면 INF를 반환후 값이 INF 이상이면 -1을 출력하게 하면 쉽게 해결되지만 왠지 모르게 0 반환에 집착하게 되어서 너무 멀리 돌아왔다.

그래서 그냥 0을 반환하게 되는일이 생기면(=간선이 없으면) cnt를 증가시켜서 1이라도 증가되어있으면 -1을 출력하도록 해서 통과했다

 

 

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

[0x1C] 플로이드-워셜 알고리즘  (0) 2022.08.10
[0x1B] 최소 신장 트리  (0) 2022.08.08
[0x1A] 위상 정렬  (0) 2022.08.08
[0x19] 트리  (0) 2022.08.08
[0x18] 그래프  (0) 2022.08.07

모든 정점 쌍 사이의 최단 거리를 구하는 알고리즘

 

방향, 무방향에 상관없이 사용할 수 있고 간선의 값이 음수여도 사용 가능하지만 간선의 값이 음수인 사이클이 있으면 문제가 생긴다.

 

경로 복원

 

플로이드-워셜 알고리즘으로 생성된 최단거리 테이블만으로는 어떤 경로가 최단거리인지 바로 알 수 없다.

 

최단거리 테이블을 D, 경로 테이블을 nxt라고 할 때,

D[i][j]로 가는 경로보다 D[i][k] + D[k][j]의 경로가 더 짧다면 D[i][j]를 갱신함과 동시에 nxt[i][j]를 nxt[i][k]로 갱신한다.

 

연습 문제

 

11404: 플로이드

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

const int INF = (int)1e9;
int t[101][101];
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 <= n; ++i)
	{
		fill(t[i] + 1, t[i] + n + 1, INF);
		t[i][i] = 0;
	}
	while (m--)
	{
		int a, b, c;
		cin >> a >> b >> c;
		t[a][b] = min(t[a][b], c); // 같은 간선이 여러개일수도 있음
	}
	for (int k = 1; k <= n; ++k)
	{
		for (int i = 1; i <= n; ++i)
		{
			for (int j = 1; j <= n; ++j)
				t[i][j] = min(t[i][j], t[i][k] + t[k][j]);
		}
	}
	for (int i = 1; i <= n; ++i)
	{
		for (int j = 1; j <= n; ++j)
		{
			if (t[i][j] != INF) cout << t[i][j] << ' ';
			else cout << 0 << ' ';
		}
		cout << '\n';
	}
	return 0;
}

 

플로이드-워셜 알고리즘의 전형적인 구현 방법

 

11780: 플로이드 2

추가 작성 예정

 

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

[0x1D] 다익스트라 알고리즘  (0) 2022.08.10
[0x1B] 최소 신장 트리  (0) 2022.08.08
[0x1A] 위상 정렬  (0) 2022.08.08
[0x19] 트리  (0) 2022.08.08
[0x18] 그래프  (0) 2022.08.07

신장 트리

 

 

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

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

 

최소 신장 트리(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

+ Recent posts