Longest Common Subsequence. 두 수열이 주어졌을 때, 모두의 부분 수열이 되는것 중에 가장 긴 것을 찾는 문제이다.

최장 공통 문자열의 개념에서 조금 확장시키면 어렵지 않게 구할 수 있다.

점화식을 사용하는 DP의 일종이다.

 

우선 최장 공통 문자열을 구하는 과정이다.

  1. 두 문자열을 한글자씩 비교한다.
  2. 두 문자가 다르면 LCS[i][j]에 0을 표시한다.
  3. 두 문자가 같으면 LCS[i-1][j-1]에 1을 더한다.
  4. 위의 과정을 반복한다.

 

// i, j는 1부터
if (str1[i-1] == str2[j-1])
    LCS[i][j] = LCS[i-1][j-1] + 1;
else
    LCS[i][j] = 0;

점화식을 코드로 작성하면 위와 같다.

 

 

이번에는 최장 공통 부분 수열을 구할 차례이다.

위의 과정에서 두 문자가 다를때의 식만 다르다.

 

if (str1[i-1] == str2[j-1])
    LCS[i][j] = LCS[i-1][j-1] + 1;
else
    LCS[i][j] = max(LCS[i][j-1], LCS[i-1][j]);

두 문자가 다를때의 식이 저렇게 나오는 이유는 부분 수열은 연속된 값이 아니기 때문이다.

LCS[i][j-1], LCS[i-1][j]가 현재 문자를 비교하는 과정의 이전 과정인 것이다.

길이만 구하는 경우라면 LCS[str1.size()][str2.size()]로 구할 수 있고 실제 문자열이 필요한 것이라면 LCS배열의 가장 끝에서부터 0을 만날때까지 역순으로 추적한 뒤에 문자열을 뒤집으면 된다.

'알고리즘 > 공부' 카테고리의 다른 글

PS 활용 문법들  (0) 2022.12.29
코딩 테스트에서 자주 출제되는 기타 알고리즘  (0) 2022.07.22
위상정렬  (0) 2022.07.17
토막지식/짧은내용들 정리  (0) 2022.07.16
이코테 개념 정리  (0) 2022.07.14

문자열 내의 특정 문자나 문장들 일괄 변경

#include <regex>

regex_replace("대상 문자열", regex("찾는 문자/문장"), "바꿀 문자/문장");
// regex_replace("aaa-bba-ccd-daf", regex("a"), "z");
// zzz-bbz-ccd-dzf

 

누적합

#include <numeric>

vector<int> v; // 1~10
accumulate(v.begin(), v.end(), "초기값", "임의 함수");
// accumulte(v.begin(), v.end(), 0);
// 55

 

'알고리즘 > 공부' 카테고리의 다른 글

최장 공통 부분 수열 (LCS)  (0) 2023.05.20
코딩 테스트에서 자주 출제되는 기타 알고리즘  (0) 2022.07.22
위상정렬  (0) 2022.07.17
토막지식/짧은내용들 정리  (0) 2022.07.16
이코테 개념 정리  (0) 2022.07.14

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

 

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

 

  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

+ Recent posts