Competitive Programming Summary - 2

Competitive Programming Summary - 1

Dynamic Programming

예를 들어 동전 coins = {1, 3, 4}로 x = 5를 표현하는 경우의 수는 다음과 같이 계산할 수 있다.

solve(x)=solve(x1)+solve(x3)+solve(x4)

유명한 문제는 다음과 같다.

Graph Basics

사용되는 용어

그래프 표현 방법

// directed
vector<int> adj[N];
adj[1].push_back(2);
adj[2].push_back(3);
adj[2].push_back(4);
adj[3].push_back(4);
adj[4].push_back(1);
// undirected
vector<pair<int,int>> adj[N];
adj[1].push_back({2,5});
adj[2].push_back({3,7});
adj[2].push_back({4,6});
adj[3].push_back({4,5});
adj[4].push_back({1,2});
# python 예제
N = 10
adj = [[] for _ in range(N)]  # (이웃, 가중치)

adj[1].append((2, 5))
adj[2].append((3, 7))

만약 weight가 존재한다면, 함께 적정한 순서로 집어넣으면 된다.

int adj[N][N];
[041003100]

Weight는 숫자로 표현할 수 있다.

vector<tuple<int,int,int>> edges;

edges.push_back({1,2,3}); // 1 - 2, weights: 3
edges.push_back({2,3,2}); // 2 - 3, weights: 2
edges = []  # (a, b, w)
edges.append((1, 2, 3))
edges.append((2, 3, 2))

Graph Traversal

DFS

bool visited[N];
void dfs(int s) {
	if (visited[s]) return;
	visited[s] = true;
	// process node s
	for (auto u: adj[s]) {
		dfs(u);
	}
}
N = 100_005
adj = [[] for _ in range(N)]
visited = [False]*N

def dfs(s):
    if visited[s]:
        return
    visited[s] = True
    # process s
    for u in adj[s]:
        dfs(u)

BFS

#include <queue>

queue<int> q;
bool visited[N];
int distance[N];

// node x에서 시작함
visited[x] = true;
distance[x] = 0;
q.push(x);

while (!q.empty()) {
	int s = q.front(); q.pop();
	// process node s
	for (auto u : adj[s]) {
		if (visited[u]) continue;
		visited[u] = true;
		distance[u] = distance[s]+1;
		q.push(u);
	}
}
from collections import deque

dq = deque([x])
visited[x] = True
dist[x] = 0
while dq:
	s = dq.popleft()
	# process s
	for u in adj[s]:
		if visited[u]:
			continue
		visited[u] = True
		dist[u] = dist[s] + 1
		dq.append(u)

둘 다 자주 사용하는 알고리즘이지만 구현하기에는 DFS가 더 쉬워서 더 좋은 선택이다.
유명한 문제들은 다음과 같다.

Shortest Path

Bellman-Ford Algorithm

메인 아이디어는 n1라운드동안 모든 엣지들을 돌면서 distance의 최소값을 업데이트를 하는 것이다. 만약 이를 넘어간다면 Negative Cycle이 존재한다는 것을 알 수 있다.
이 알고리즘은 edge list를 이용하여 간선을 표현한다.

for (int i = 1; i <= n; i++) {
	distance[i] = INF;
}
distance[x] = 0;
for (int i = 1; i <= n-1; i++) {
	for (auto e : edges) {
		int a, b, w;
		tie(a, b, w) = e;
		distance[b] = min(distance[b], distance[a]+w);
	}
}
dist = [INF]*(n+1)
dist[src] = 0
for _ in range(n-1):
	for a, b, w in edges:
		if dist[a] == INF:
			continue
		if dist[b] > dist[a] + w:
			dist[b] = dist[a] + w

해당 알고리즘의 시간 복잡도는 O(nm)이다. (n: 노드의 수, m: 엣지의 수) 느린 편이지만 음수 간선이 가능하다는 것이 장점이다.
변종으로는 the SPFA algorithm ("Shortest Path Faster Algorithm")이 있다.

대표 문제들은 다음과 같다.

Dijkstra's Algorithm

메인아이디어는 아직 처리되지 않았고 거리가 가능한 한 작은 노드를 계속해서 선택하는 것이다. 시간복잡도는 O(n+mlogm)으로 표현할 수 있어 효과적이다. 다만 '거리가 점점 커진다'를 가정으로 하고 있으므로 음수 weight의 간선은 불가능하다.

구현은 Priority Queue로 distance를 지속적으로 계산하여 관리한다. adjacency lists를 쓰자. (-d, x)로 써서 distance를 오름차순으로 관리한다.

priority_queue<pair<int,int>> q;
// ... q를 채움
for (int i = 1; i <= n; i++) {
distance[i] = INF;
}
// x부터 시작
distance[x] = 0;
q.push({0,x});
while (!q.empty()) {
	int a = q.top().second; q.pop();
	if (processed[a]) continue;
		processed[a] = true;
	for (auto u : adj[a]) {
		int b = u.first, w = u.second;
		if (distance[a]+w < distance[b]) {
			distance[b] = distance[a]+w;
			q.push({-distance[b],b});
		}
	}
}
import heapq

dist = [INF]*(n+1)
dist[src] = 0
pq = [(0, src)]
visited = [False]*(n+1)
while pq:
	d, a = heapq.heappop(pq)
	if visited[a]:
		continue
	visited[a] = True
	for b, w in adj[a]:
		nd = d + w
		if nd < dist[b]:
			dist[b] = nd
			heapq.heappush(pq, (nd, b))

Floyd-Warshall Algorithm

Bellman-Ford와 Dijkstra's Algorithm은 모두 '한 노드'에서의 다른 모든 노드들의 최소 거리를 구하는 알고리즘이다. 다만 '모든 노드'에서 각자 모든 노드들의 최소 거리를 구하는 알고리즘이 필요할 수 있다. 이때 Floyd-Warshall를 이용한다.
메인 아이디어는 각 라운드마다 '중간다리' 역할을 하는 노드를 선택한다. 이 노드를 통해 최소로 만들 수 있는 경로가 생긴다면 업데이트한다.
구현은 간단하다. adjacency matrix를 이용하여 구현한다.

// 초기화: 알고있는 distance만 먼저 초기화한다.
for (int i = 1; i <= n; i++) {
	for (int j = 1; j <= n; j++) {
		if (i == j) dist[i][j] = 0;
		else if (adj[i][j]) dist[i][j] = adj[i][j];
		else dist[i][j] = INF;
	}
}
// Floyd-Warshall Algorithm
for (int k = 1; k <= n; k++) { // 이때 k가 중간 다리
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++) {
			dist[i][j] = min(dist[i][j],dist[i][k]+dist[k][j]);
		}
	}
}
INF = 10**18

# dist: n x n matrix, dist[i][i]=0, 없으면 INF로 초기화
n = len(dist)
for k in range(n):
	dk = dist[k]
	for i in range(n):
		if dist[i][k] == INF:
			continue
		dik = dist[i][k]
		for j in range(n):
			if dk[j] == INF:
				continue
			nd = dik + dk[j]
			if nd < dist[i][j]:
				dist[i][j] = nd

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

Reference