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

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

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

트리의 정의

 

정점(=노드) : 각각의 원소

루트 : 가장 꼭대기에 있는 정점

리프 : 자식이 없는 정점

간선 : 정점끼리 연결하는 선

서브트리 : 어떤 한 정점에 대해 그 밑에 있는 정점과 간선만을 모은 트리

이진트리 : 각 노드의 자식이 2개 이하인 트리

 

이진 검색 트리

 

왼쪽 서브트리의 모든 값은 부모의 값보다 작고, 오른쪽 서브트리의 모든 값은 부모의 값보다 큰 이진 트리

이진 검색 트리를 이용하면 insert, erase, find, update 모두 O(logN)에 처리할 수 있다.

 

이진 검색 트리를 사용해야 하는 문제가 있다면 map/set을 사용하면 된다.

보통 unordered_map/set이 조금더 빠르지만 해시충돌이 빈번하게 일어난다면 얘기가 달라지므로 언제나 O(logN)을 보장해주는 map/set을 이용하는게 낫다.

하지만 O(logN)중에서도 느린편이다.

 

 

코딩 테스트에서 STL을 사용하지 못하는 경우 이진 검색 트리로 풀 수 있을것 같은 문제라 하더라도 해시나 이분 탐색과 같은 다른 방법을 이용하는 풀이를 고민해야 한다.

 

연습 문제

 

7662: 이중 우선순위 큐

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

int main(void)
{
	int tc;
	cin >> tc;
	while (tc--)
	{
		multiset<int> h;
		int k;
		cin >> k;
		while (k--)
		{
			char a;
			int b;
			cin >> a >> b;
			if (a == 'I')
				h.insert(b);
			else
			{
				if (h.empty()) continue;
				if (b == 1)	h.erase(prev(h.end()));
				else h.erase(h.begin());
			}
		}
		if (!h.empty()) cout << *h.rbegin() << ' ' << *h.begin();
		else cout << "EMPTY";
		cout << '\n';
	}
}

 

삭제할 때 prev(h.end())가 아니라 h.rbegin()을 넣어봤더니 reverse_iterator는 erase 할 수 없었다.

iterator로 변환해주는 base()를 쓰게되면 해당 iterator 그대로 변환되는게 아니라 이전 iterator가 반환된다.

또한 iterator가 아닌 참조한 값을 지우게 되면 중복값이 싸그리 지워지기 때문에 이 문제에서는 적합하지 않다.

 

1202: 보석 도둑

더보기

아직 내가 쉽게 이해할만한 문제가 아니라 패스함

 

기본 문제

 

21939: 문제 추천 시스템 Version 1

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

int n, m;
map<int, set<int>> h;

int main(void)
{
	cin >> n;
	while (n--)
	{
		int a, b;
		cin >> a >> b;
		h[b].insert(a);
	}

	cin >> m;
	while (m--)
	{
		string s;
		cin >> s;
		if (s == "recommend")
		{
			int a;
			cin >> a;
			if (a == 1)	cout << *(prev(h.end())->second.rbegin()) << '\n';
			else cout << *h.begin()->second.begin() << '\n';
		}
		else if (s == "add")
		{
			int p, l;
			cin >> p >> l;
			h[l].insert(p);
		}
		else
		{
			int a;
			cin >> a;
			for (auto& it : h)
			{
				if (it.second.find(a) != it.second.end())
				{
					it.second.erase(a);
					if (it.second.empty()) h.erase(it.first);
					break;
				}
			}
		}
	}
}

 

map에 set을 쳐박아놔서 find->remove를 수행할 때 이점을 다 날려버리고 선형탐색을 시도해서 시간이 오래걸린다.

정답 코드는 set과 배열 두개를 사용해서 따로 관리하는 방법을 썼음

 

23326: 홍익 투어리스트

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

int n, q;
set<int> p;

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

	cin >> n >> q;
	for (int i = 1; i <= n; ++i)
	{
		int a;
		cin >> a;
		if (a) p.insert(i);
	}

	int cur = 1;
	while (q--)
	{
		int a;
		cin >> a;
		if (a == 1)
		{
			cin >> a;
			if (p.find(a) != p.end()) p.erase(a);
			else p.insert(a);
		}
		else if (a == 2)
		{
			cin >> a;
			cur = (cur + a - 1) % n + 1;
		}
		else
		{
			if (p.empty()) cout << -1 << '\n';
			else
			{
				auto it = p.lower_bound(cur);
				if (*it >= cur) cout << *it - cur << '\n';
				else cout << n - cur + *p.begin() << '\n';
			}
		}
	}
}

 

입출력 버퍼 부분 쥐도새도 모르게 지워져서 계속 시간초과 뜨길래 뇌정지옴

해답코드랑 비교해보니 쿼리가 2일때 cur값 주는 부분만 조건식을 썼냐 안썼냐 차이였음

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

[0x18] 그래프  (0) 2022.08.07
[0x17] 우선순위 큐  (0) 2022.08.07
[0x15] 해시  (0) 2022.08.06
[0x14] 투 포인터  (0) 2022.08.05
[0x13] 이분탐색  (0) 2022.08.05

+ Recent posts