다시 일주일만의 이코테 정리하기 게시글이다. 곧 벌써 8월이 다가오고 코딩테스트를 준비해야하는 기간이 얼마 남지 않은 것 같다. 이코테 정리를 최대한 마무리하고 문제 풀이 양을 늘리면서 알고리즘에 좀더 익숙해지고 파이썬을 활용한 코테 준비에 박차를 가해보고자 한다!!
이코테에 남은 유튜브 영상 내용들을 정리하고 앞으로는 velog에 개발, 알고리즘 관련 지식과 함께 기획적인 부분들(PM)을 정리하여 게시해보고자 노력할 예정이다.
참고 강의 링크! 👉 https://www.youtube.com/playlist?list=PLRx0vPvlEmdAghTr5mXQxGpHjWqSz0dgC
최단 경로 알고리즘 : 가장 짧은 경로를 찾는 알고리즘
문제 상황
각 지점은 그래포에서 노드로 표현 하고 연결된 도로는 간선으로 표현
다익스트라 : 특정한 노드에서 출발해서 다른 모든 노드로 가는 최단 경로
→ 음의 간선이 없을 때 정상적 동작(그리디 알고리즘으로 분류)
→ 매 상황에서 가장 비용이 적은 노드를 선택함!!
다익스트라 동작 과정
다익스트라 특징
우선 순위 큐 : 우선순위가 가장 높은 데이터를 가장 먼저 삭제하는 자료구조
힙 : 최소 힙과 최대 힙
→ 삽입 시간과 삭제 시간이 O(logN)
import heapq
# 최소 힙 (오름차순)
def minheapsort(iterable):
h = []
result = []
for value in iterable:
heapq.heappush(h, value)
for i in range(len(h)):
result.append(heapq.heappop(h))
return result
# 최대 힙 (내림차순)
def maxheapsort(iterable):
h = []
result = []
for value in iterable:
heapq.heappush(h, -value)
for i in range(len(h)):
result.append(-heapq.heappop(h))
return result
(다익스트라에서는 최소힙을 사용하여 구현한다!!)
import heapq
import sys
input = sys.stdin.readline
INF = int(1e9) # 무한을 의미하는 값으로 10억을 설정
# 노드의 개수, 간선의 개수를 입력받기
n, m = map(int, input().split())
# 시작 노드 번호를 입력받기
start = int(input())
# 각 노드에 연결되어 있는 노드에 대한 정보를 담는 리스트를 만들기
graph = [[] for i in range(n + 1)]
# 최단 거리 테이블을 모두 무한으로 초기화
distance = [INF] * (n + 1)
# 모든 간선 정보를 입력받기
for _ in range(m):
a, b, c = map(int, input().split())
# a번 노드에서 b번 노드로 가는 비용이 c라는 의미
graph[a].append((b, c))
def dijkstra(start):
q = []
# 시작 노드로 가기 위한 최단 경로는 0으로 설정하여, 큐에 삽입
heapq.heappush(q, (0, start))
distance[start] = 0
while q: # 큐가 비어있지 않다면
# 가장 최단 거리가 짧은 노드에 대한 정보 꺼내기
dist, now = heapq.heappop(q)
# 현재 노드가 이미 처리된 적이 있는 노드라면 무시
if distance[now] < dist:
continue
# 현재 노드와 연결된 다른 인접한 노드들을 확인
for i in graph[now]:
cost = dist + i[1]
# 현재 노드를 거쳐서, 다른 노드로 이동하는 거리가 더 짧은 경우
if cost < distance[i[0]]:
distance[i[0]] = cost
heapq.heappush(q, (cost, i[0]))
# 다익스트라 알고리즘을 수행
dijkstra(start)
# 모든 노드로 가기 위한 최단 거리를 출력
for i in range(1, n + 1):
# 도달할 수 없는 경우, 무한(INFINITY)이라고 출력
if distance[i] == INF:
print("INFINITY")
# 도달할 수 있는 경우 거리를 출력
else:
print(distance[i])
힙 자료구조를 이용하는 다익스트라 알고리즘 시간 복잡도는 O(ElogV)
노드를 하나씩 꺼내 검사하는 반복문은 노드의 개수 V이상의 횟수로는 처리되지 않음
결과적으로 현재 우선순위 큐에서 꺼낸 노드와 연결된 다른 노드들을 확인하는 총횟수는 최대 간선의 개수(E)만큼 연산 수행
플로이드 워셜 : 모든 노드에서 다른 모든 노드까지의 최단 경로 모두 계산
INF = int(1e9) # 무한을 의미하는 값으로 10억을 설정
# 노드의 개수 및 간선의 개수를 입력받기
n = int(input())
m = int(input())
# 2차원 리스트(그래프 표현)를 만들고, 모든 값을 무한으로 초기화
graph = [[INF] * (n + 1) for _ in range(n + 1)]
# 자기 자신에서 자기 자신으로 가는 비용은 0으로 초기화
for a in range(1, n + 1):
for b in range(1, n + 1):
if a == b:
graph[a][b] = 0
# 각 간선에 대한 정보를 입력 받아, 그 값으로 초기화
for _ in range(m):
# A에서 B로 가는 비용은 C라고 설정
a, b, c = map(int, input().split())
graph[a][b] = c
# 점화식에 따라 플로이드 워셜 알고리즘을 수행
for k in range(1, n + 1):
for a in range(1, n + 1):
for b in range(1, n + 1):
graph[a][b] = min(graph[a][b], graph[a][k] + graph[k][b])
# 수행된 결과를 출력
for a in range(1, n + 1):
for b in range(1, n + 1):
# 도달할 수 없는 경우, 무한(INFINITY)이라고 출력
if graph[a][b] == 1e9:
print("INFINITY", end=" ")
# 도달할 수 있는 경우 거리를 출력
else:
print(graph[a][b], end=" ")
print()
서로소 집합 : 공통 원소가 없는 두 집합
ex) {1, 2} {3, 4}
서로소 집합 자료구조
# 특정 원소가 속한 집합을 찾기
def find_parent(parent, x):
# 루트 노드가 아니라면, 루트 노드를 찾을 때까지 재귀적으로 호출
if parent[x] != x:
parent[x] = find_parent(parent, parent[x])
return parent[x]
# 두 원소가 속한 집합을 합치기
def union_parent(parent, a, b):
a = find_parent(parent, a)
b = find_parent(parent, b)
if a < b:
parent[b] = a
else:
parent[a] = b
# 노드의 개수와 간선(Union 연산)의 개수 입력 받기
v, e = map(int, input().split())
parent = [0] * (v + 1) # 부모 테이블 초기화하기
# 부모 테이블상에서, 부모를 자기 자신으로 초기화
for i in range(1, v + 1):
parent[i] = i
# Union 연산을 각각 수행
for i in range(e):
a, b = map(int, input().split())
union_parent(parent, a, b)
# 각 원소가 속한 집합 출력하기
print('각 원소가 속한 집합: ', end='')
for i in range(1, v + 1):
print(find_parent(parent, i), end=' ')
print()
# 부모 테이블 내용 출력하기
print('부모 테이블: ', end='')
for i in range(1, v + 1):
print(parent[i], end=' ')
서로소 집합을 활용한 사이클 판별 : 무방향 그래프 내에서의 사이클을 판별할 수 있음
# 특정 원소가 속한 집합을 찾기
def find_parent(parent, x):
# 루트 노드가 아니라면, 루트 노드를 찾을 때까지 재귀적으로 호출
if parent[x] != x:
parent[x] = find_parent(parent, parent[x])
return parent[x]
# 두 원소가 속한 집합을 합치기
def union_parent(parent, a, b):
a = find_parent(parent, a)
b = find_parent(parent, b)
if a < b:
parent[b] = a
else:
parent[a] = b
# 노드의 개수와 간선(Union 연산)의 개수 입력 받기
v, e = map(int, input().split())
parent = [0] * (v + 1) # 부모 테이블 초기화하기
# 부모 테이블상에서, 부모를 자기 자신으로 초기화
for i in range(1, v + 1):
parent[i] = i
cycle = False # 사이클 발생 여부
for i in range(e):
a, b = map(int, input().split())
# 사이클이 발생한 경우 종료
if find_parent(parent, a) == find_parent(parent, b):
cycle = True
break
# 사이클이 발생하지 않았다면 합집합(Union) 연산 수행
else:
union_parent(parent, a, b)
if cycle:
print("사이클이 발생했습니다.")
else:
print("사이클이 발생하지 않았습니다.")
신장 트리 : 그래프에서 모든 노드를 포함하면서 사이클이 존재하지 않는 부분그래프 의미
→ 모든 노드가 포함되어 서로 연결되어 사이클이 존재하지 않는 것은 트리의 조건이기도 함!!
최소 신장 트리 : 최소한의 비용으로 구성되는 신장트리(전체 노드가 서로 연결되게 하게 그 간선의 크기가 최소가 될 수 있도록)
크루스칼 알고리즘
# 특정 원소가 속한 집합을 찾기
def find_parent(parent, x):
# 루트 노드가 아니라면, 루트 노드를 찾을 때까지 재귀적으로 호출
if parent[x] != x:
parent[x] = find_parent(parent, parent[x])
return parent[x]
# 두 원소가 속한 집합을 합치기
def union_parent(parent, a, b):
a = find_parent(parent, a)
b = find_parent(parent, b)
if a < b:
parent[b] = a
else:
parent[a] = b
# 노드의 개수와 간선(Union 연산)의 개수 입력 받기
v, e = map(int, input().split())
parent = [0] * (v + 1) # 부모 테이블 초기화하기
# 모든 간선을 담을 리스트와, 최종 비용을 담을 변수
edges = []
result = 0
# 부모 테이블상에서, 부모를 자기 자신으로 초기화
for i in range(1, v + 1):
parent[i] = i
# 모든 간선에 대한 정보를 입력 받기
for _ in range(e):
a, b, cost = map(int, input().split())
# 비용순으로 정렬하기 위해서 튜플의 첫 번째 원소를 비용으로 설정
edges.append((cost, a, b))
# 간선을 비용순으로 정렬
edges.sort()
# 간선을 하나씩 확인하며
for edge in edges:
cost, a, b = edge
# 사이클이 발생하지 않는 경우에만 집합에 포함
if find_parent(parent, a) != find_parent(parent, b):
union_parent(parent, a, b)
result += cost
print(result)
위상 정렬 : 사이클이 없는 방향 그래프의 모든 노드를 방향성에 거스르지 않도록 순서대로 나열하는 것 의미
진입 차수 : 특정한 노드로 들어오는 간서의 개수
진출 차수 : 특정한 노드에서 나가는 간선의 개수
위상정렬
from collections import deque
# 노드의 개수와 간선의 개수를 입력 받기
v, e = map(int, input().split())
# 모든 노드에 대한 진입차수는 0으로 초기화
indegree = [0] * (v + 1)
# 각 노드에 연결된 간선 정보를 담기 위한 연결 리스트 초기화
graph = [[] for i in range(v + 1)]
# 방향 그래프의 모든 간선 정보를 입력 받기
for _ in range(e):
a, b = map(int, input().split())
graph[a].append(b) # 정점 A에서 B로 이동 가능
# 진입 차수를 1 증가
indegree[b] += 1
# 위상 정렬 함수
def topology_sort():
result = [] # 알고리즘 수행 결과를 담을 리스트
q = deque() # 큐 기능을 위한 deque 라이브러리 사용
# 처음 시작할 때는 진입차수가 0인 노드를 큐에 삽입
for i in range(1, v + 1):
if indegree[i] == 0:
q.append(i)
# 큐가 빌 때까지 반복
while q:
# 큐에서 원소 꺼내기
now = q.popleft()
result.append(now)
# 해당 원소와 연결된 노드들의 진입차수에서 1 빼기
for i in graph[now]:
indegree[i] -= 1
# 새롭게 진입차수가 0이 되는 노드를 큐에 삽입
if indegree[i] == 0:
q.append(i)
# 위상 정렬을 수행한 결과 출력
for i in result:
print(i, end=' ')
topology_sort()
소수 : 1보다 큰 자연수 중에서 1과 자기 자신을 제외한 자연수로는 나누어떨어지지 않는 자연수
→ 자연수가 소수인지 아닌지 판별하는 문제가 자주 출제됨
기본적인 알고리즘은 모든 자연수에 대해 연산을 수행해야 하므로 시간 복잡도는 O(X)임
약수의 성질을 이용하면 시간 단축 가능!
→ 모든 약수가 가운데 약수를 기준으로 곱셈 연산에 대해 대칭을 이루는 성질 이용
import math
def is_prime_number(x):
for i in range(2, int(math,sqrt(x)) + 1):
if x % i == 0:
return False
return True
이렇게 하면 시간 복잡도 O(N^1/2)
다수의 소수 판별하는 방법은 → 에라토스테네스의 체 알고리즘 이용
import math
n = 1000
array [True for i in range(n + 1)]
for i in range(2, int(math.sqrt(n) + 1):
if array[i] == True
j = 2
while i * j <= n:
array[i * j] = False
j += 1
for i in range(2, n + 1)
if array[i]:
print(i, end=' ')
투 포인터 알고리즘 : 리스트에 순차적으로 접근해야할 때 두개의 점의 위치를 기록하면서 처리하는 알고리즘
n = 5
m = 5
data = [1, 2, 3, 2, 5]
count = 0
interval_sum = 0
end = 0
for start in range(n):
while interval_sum < m && end < n:
interval_sum += data[end]
end += 1
if interval_sum == m:
count += 1
interval_sum -= data[start]
print(count)
구간 합 : 연속적으로 나열된 N개의 수가 있을 때 특정 구간의 모든 수를 합한 값을 구하는 문제
접두사 합 이용하기 → 배열의 맨 앞부터 특정 위치까지의 합을 미리 구해 놓는 것
n = 5
data = [10, 20, 30, 40, 50]
sum_value = 0
prefix_sum = [0]
for i in data:
sum_value += 1
prefix_sum.append(sum_value)
left = 3
right = 4
print(prefix_sum[right] - prefix_sum[left - 1])
정해진 목적에 따라서 동작하는 완성된 프로그램을 개발하는 것을 요구하는 코딩 테스트 유형
서버 클라이언트 : 클라이언트가 요청(request)를 보내면 서버가 응답
클라이언트 = 고객
서버 = 서비스 제공자
HTTP : 웹상에서 데이터를 주고받기 위한 프로토콜을 의미
HTTP는 GET, POST, PUT, DELETE 등의 다양한 HTTP 메서드를 지원한다
근데 저마다 다른 방식으로 개발하면 문제가 되서 기준이 되는 아키텍처 필요
→ REST의 등장 배경!!
REST는 각 자원에 대하여 자원의 상태에 대한 정보를 주고받는 개발 방식 의미
API(Application Programming Interface) : 프로그램이 상호 작용하기 위한 인터페이스 의미
REST API : REST 아키텍처를 따르는 API 의미
REST API 호출 : REST 방식을 따르고 있는 서버에 특정한 요청을 전송하는 것을 의미
JSON : 데이터를 주고받는데 사용하는 경량의 데이터 형식(키와 값의 쌍으로 이루어진 데이터 객체 저장)
우선순위 큐 : 우선순위가 가장 높은 데이터를 가장 먼저 삭제하는 자료구조
→ 우선순위에 따라 처리하고 싶을 때 사용!!
구현 방법
1. 단순히 리스트를 이용하여 구현
2. 힙을 이용하여 구현
우선순위 큐 구현 방식 | 삽입시간 | 삭제 시간 |
---|---|---|
리스트 | O(1) | O(N) |
힙(Heap) | O(logN) | O(logN) |
단순히 N개의 데이터를 힙에 넣었다가 모두 꺼내는 작업은 정렬과 동일하여 → 시간 복잡도 O(NlogN)
힙의 특징
완전 이진 트리 : 루트 노드부터 시작하여 왼쪽 자식 노드, 오른쪽 자식 노드 순서대로 데이터가 차례대로 삽입되는 트리
최소 힙 구성 함수 : Min-Heapify()
원소가 제거될 때 O(logN)의 시간 복잡도로 힙 성질을 유지하고 루트 노드에서부터 하향식으로 Heapify()를 진행
트리 : 가계도와 같은 계층적인 구조를 표현할 때 사용할 수 있는 자료구조
기본적으로 트리의 크기가 N이면 전체 간선의 개순는 N - 1개
이진 탐색 트리 : 이진 탐색이 동작할 수 있도록 고안된 효율적인 탐색이 가능한 자료구조의 일종
트리의 순회 : 트리 자료구조에 포함된 노드를 특정한 방법으로 한 번씩 방문하는 방법 의미
→ 트리의 정보를 시각적으로 확인
음수 간선이 포함된 상황에서의 최단 거리 문제에 적용
→ 음수 간선의 순환이 포함되어 있으면 음의 무한인 노드가 발생할 수 있음!!
벨만 포드 최단경로 알고리즘은 음의 간선이 포함된 상황에서도 사용할 수 있음
음수 간선 순환이 발생하는지 체크하고 싶으면 3번 과정 한번 더 수행하기.
→ 이 때 최단 거리 테이블이 갱신되면 음수 간선 순환이 존재하는 것.
다익스트라는 매번 방문하지 않은 노드 중에서 최단 거리가 가장 짧은 노드를 선택
→ 음수 간선이 없다면 최적의 해 찾음
벨만 포드는 매번 모든 간선 전부 확인
→ 다익스트라 알고리즘에서 최적의 해를 항상 포함
→ 좀 시간이 걸려도 음수 간선 순환을 탐지할 수 있음
import sys
input = sys.stdin.readline
INF = int(1e9)
def bf(strat):
dist[start] = 0
for i in range(n):
for j in range(m):
cur = edges[j][1]
next_node = edges[j][1]
cost = edges[j][2]
if dist[cur] != INF and dist[next_node] > dist[cur] + cost:
dist[next_node] = dist[cur] + cost
if i == n - 1:
return True
return False
n, m = map(int, input().split())
edges = []
dist = [INF] * (n + 1)
for _ in range(m):
a, b, c = map(int, input().split())
edges.append((a,b,c))
negative_cycle = bf(1)
if negative_cycle:
print("-1")
else :
for i in range(2, n + 1):
if dist[i] == INF:
print("-1")
else:
print(dist[i])
바이너리 인덱스 트리(펜윅 트리) : 2진법 인덱스 구조를 활용해 구간 합 문제를 효과적으로 해결
import sys
input = sys.stdin.readline
INF = int(1e9) # 무한을 의미하는 값으로 10억을 설정
# 노드의 개수, 간선의 개수를 입력받기
n, m = map(int, input().split())
# 모든 간선에 대한 정보를 담는 리스트 만들기
edges = []
# 최단 거리 테이블을 모두 무한으로 초기화
distance = [INF] * (n + 1)
# 모든 간선 정보를 입력받기
for _ in range(m):
a, b, c = map(int, input().split())
# a번 노드에서 b번 노드로 가는 비용이 c라는 의미
edges.append((a, b, c))
def bf(start):
# 시작 노드에 대해서 초기화
distance[start] = 0
# 전체 n - 1번의 라운드(round)를 반복
for i in range(n):
# 매 반복마다 "모든 간선"을 확인하며
for j in range(m):
cur_node = edges[j][0]
next_node = edges[j][1]
edge_cost = edges[j][2]
# 현재 간선을 거쳐서 다른 노드로 이동하는 거리가 더 짧은 경우
if distance[cur_node] != INF and distance[next_node] > distance[cur_node] + edge_cost:
distance[next_node] = distance[cur_node] + edge_cost
# n번째 라운드에서도 값이 갱신된다면 음수 순환이 존재
if i == n - 1:
return True
return False
# 벨만 포드 알고리즘을 수행
negative_cycle = bf(1) # 1번 노드가 시작 노드
if negative_cycle:
print("-1")
else:
# 1번 노드를 제외한 다른 모든 노드로 가기 위한 최단 거리를 출력
for i in range(2, n + 1):
# 도달할 수 없는 경우, -1을 출력
if distance[i] == INF:
print("-1")
# 도달할 수 있는 경우 거리를 출력
else:
print(distance[i])
깊이 계산 → DFS이용
매 쿼리마다 부모 방향으로 거슬러 올라가기 위해 최악의 경우 O(N)의 시간 복잡도 요구됨
→ 따라서 모든 쿼리를 처리할 때의 시간 복잡도는 O(NM)임
2의 제곱 형태로 거슬러 올라가도록 하면 O(logN)의 시간 복잡도를 보장할 수 있음
→ 메모리를 조금 더 사용하여 각 노드에 대하여 2^i 번 째 부모에 대한 정보를 기록
import sys
input = sys.stdin.readline # 시간 초과를 피하기 위한 빠른 입력 함수
sys.setrecursionlimit(int(1e5)) # 런타임 오류를 피하기 위한 재귀 깊이 제한 설정
LOG = 21 # 2^20 = 1,000,000
n = int(input())
parent = [[0] * LOG for _ in range(n + 1)] # 부모 노드 정보
d = [0] * (n + 1) # 각 노드까지의 깊이
c = [0] * (n + 1) # 각 노드의 깊이가 계산되었는지 여부
graph = [[] for _ in range(n + 1)] # 그래프(graph) 정보
for _ in range(n - 1):
a, b = map(int, input().split())
graph[a].append(b)
graph[b].append(a)
# 루트 노드부터 시작하여 깊이(depth)를 구하는 함수
def dfs(x, depth):
c[x] = True
d[x] = depth
for y in graph[x]:
if c[y]: # 이미 깊이를 구했다면 넘기기
continue
parent[y][0] = x
dfs(y, depth + 1)
# 전체 부모 관계를 설정하는 함수
def set_parent():
dfs(1, 0) # 루트 노드는 1번 노드
for i in range(1, LOG):
for j in range(1, n + 1):
parent[j][i] = parent[parent[j][i - 1]][i - 1]
# A와 B의 최소 공통 조상을 찾는 함수
def lca(a, b):
# b가 더 깊도록 설정
if d[a] > d[b]:
a, b = b, a
# 먼저 깊이(depth)가 동일하도록
for i in range(LOG - 1, -1, -1):
if d[b] - d[a] >= (1 << i):
b = parent[b][i]
# 부모가 같아지도록
if a == b:
return a;
for i in range(LOG - 1, -1, -1):
# 조상을 향해 거슬러 올라가기
if parent[a][i] != parent[b][i]:
a = parent[a][i]
b = parent[b][i]
# 이후에 부모가 찾고자 하는 조상
return parent[a][0]
set_parent()
m = int(input())
for i in range(m):
a, b = map(int, input().split())
print(lca(a, b))
이렇게 이코테 내용들을 다 정리해보았다. 문제를 같이 병행하면서 제대로 풀지 못해서 정리를 마치고 각 내용들을 돌려가면서 풀어볼 예정이다. 여행을 다녀오면서 좀 생각이 많았는데, 조급해하지 않아도 내가 앞으로 뭘 할지를 감을 잡기가 굉장히 힘들었던 것 같다. 분명히 여러가지를 병행하면서 하고 있는데 내가 앞으로 뭘 하고 싶은지 딱 정하기가 힘든 느낌,,,?
암튼 현재 내가 하고 있는게 코테준비, 피그마 디자인, 기획, 여러 프로젝트 운영을 하고 있는데 방학내에 마무리할 수 있는게 있다면 최대한 마무리해보도록 하고 요새 좀 소홀했던 개발 공부도 다시 시작해볼 예정이다.
앞으로 남은 방학 일정들을 소화해볼려면 개발도 손놓지 않으면서도 이것저것 자소서랑 포트폴리오도 채워보도록 하자!!
아참 영어 회화는 제때제때 하자... 방학안에는 오픽 해치웠으니 토스 꼭 해두자!!
화팅~!