(2021.06.21 - 23)
찾으려는 숫자 리스트를 오른쪽 왼쪽으로 조건에 맞게 리스트 배열을 움직여서 인덱스 0번째에서 뽑아내는 최소 횟수를 구하는 문제이다.
deque를 활용하는 것이 좋은데(시간복잡도를 낮춰줌) 일단 이 문제에서는 일반 리스트 내장함수를 활용해서 풀었다.
처음 큐의 크기에 관한 n을 입력받고 뽑으려는 수의 갯수를 입력받는다.
왼쪽, 오른쪽으로 이동할 때마다 횟수를 세야하므로 count 변수를 만들어준다.
찾고 싶은 숫자를 찾을 때까지 while 문을 돌고, 그것을 총 찾으려는 숫자만큼 for문으로 반복한다.
큐라는 리스트는 오른쪽, 왼쪽으로 회전하고 또 값을 빼내기도 하므로 리스트의 길이와 인덱스에 해당하는 값들이 가변적이다.
때문에 찾으려는 숫자가 해당 큐의 왼쪽에 있으면 왼쪽으로 돌리고 오른쪽에 가까우면 오른쪽으로 돌린 뒤 숫자를 찾으면 큐와 찾는 숫자 리스트에서 해당 값을 pop 해준다.
from sys import stdin
n, m = map(int, stdin.readline().rstrip().split())
# result_list = []
count = 0
goal = list(map(int, stdin.readline().rstrip().split()))
q = list(range(1, n + 1))
for _ in range(len(goal)): # 목표 숫자갯수만큼 반복
while q[0] != goal[0]: # 목표 숫자를 찾을 때 까지 반복
if goal[0] in q[int(len(q)//2) + 1:]: # 찾는 숫자가 큐의 오른쪽에 있으면 오른쪽으로 회전시키라는 조건
q.insert(0, q[-1]) # 오른쪽 끝 값을 왼쪽에 추가
q.pop() # 오른쪽 값 제거
count += 1
else:
q.append(q[0]) # 왼쪽에 있으면 오른쪽에 값 추가
q.pop(0) # 왼쪽 값 제거
count += 1
if q[0] == goal[0]: # 일치하는 값 찾으면
# result_list.append(q[0]) # 문제 풀때 썼던 참고용 리스트
q.pop(0) # 큐에서 해당 값 삭제
goal.pop(0) # 찾는 값에서 삭제
print(count)
오른쪽으로 돌때 리스트의 왼쪽에 값을 추가해 줘야하기 때문에 insert함수를 사용했다.
기본 리스트로 이러한 동작을 구현하기 위해서는 리스트의 모든 원소를 한칸씩 이동시켜야하므로 시간복잡도가 O(N)이된다.
이때문에 deque를 사용해서 appendleft(), popleft()를 사용해서 시간복잡도를 낮추는 것이 훨씬 효율적이다.
조건에 맞게 큐를 구현하는 문제다.
# PyPy3 으로는 맞다고 나옴
from sys import stdin
class Node():
def __init__(self, data):
self.data = data
self.next = None
class Queue():
def __init__(self):
self.head = None
self.tail = None
self.len = 0
def push(self, data):
new_data = Node(data)
self.len += 1
if self.head == None:
self.head = new_data
self.tail = new_data
else:
self.tail.next = new_data
self.tail = new_data
return
def empty(self):
if self.head is None:
return 1
else:
return 0
def pop(self):
if self.empty() == 1:
return -1
else:
pop_data = self.head.data
self.head = self.head.next
self.len -= 1
return pop_data
def size(self):
return self.len
def front(self):
if self.empty() == 1:
return -1
else:
return self.head.data
def back(self):
if self.empty() == 1:
return -1
else:
return self.tail.data
queue = Queue()
n = int(stdin.readline())
for _ in range(n):
cmd = stdin.readline().rstrip().split()
if cmd[0] == 'push':
queue.push(int(cmd[1]))
elif cmd[0] == 'pop':
print(queue.pop())
elif cmd[0] == 'size':
print(queue.size())
elif cmd[0] == 'empty':
print(queue.empty())
elif cmd[0] == 'front':
print(queue.front())
elif cmd[0] == 'back':
print(queue.back())
먼저 클래스로 노드를 활용하여 큐를 구현하면 pypy3에서 정상적으로 처리되지만 데이터 처리 속도에 불리한 파이썬을 사용하게 되면 시간초과로 처리가 된다.
따라서 시간복잡도를 낮출 수 있는 deque를 사용해야하는 문제다.
deque 함수 종류
# 덱 사용 = deque
from sys import stdin
from collections import deque
deq = deque()
n = int(stdin.readline())
count = 0
for _ in range(n):
cmd = stdin.readline().rstrip().split()
if cmd[0] == 'push':
count += 1
deq.append(cmd[1])
elif cmd[0] == 'pop':
if count == 0:
print(-1)
else:
count -= 1
print(deq.popleft())
elif cmd[0] == 'size':
print(count)
elif cmd[0] == 'empty':
if count == 0:
print(1)
else:
print(0)
elif cmd[0] == 'front':
if count == 0:
print(-1)
else:
print(deq[0]) # deq = dequeue() 여기서 () 안에 리스트가 들어갈 수도 있고 그냥 쓰면 문자열 형태로 들어감 그래서 deq[0] 이렇게 쓸수있는 것
elif cmd[0] == 'back':
if count == 0:
print(-1)
else:
print(deq[-1])
탐색의 개념이 잡히지 않은 상태로 문제를 접해서 굉장히 어려웠지만 이 문제로 깊이우선탐색과 너비우선탐색의 개념을 잡을 수 있었다.
from collections import deque # 큐에서 pop 할때 시간복잡도를 낮춰줌
def bfs(graph, v):
visit = []
queue = deque([v])
while queue:
node = queue.popleft() # 맨처음 들어온 값을 뽑아냄, 선입선출로 여기서 넓이 탐색의 특색이 나타남
if node not in visit: # 중복으로 큐에 들어온 노드값이 여기서 걸러짐 ex) queue = [4, 4, 4]일때 결국 다 방문한 거라서 다 popleft로 빠져나가고 종료됨
visit.append(node)
if node in graph: # 이 부분 없으면 key error 발생함.
temp = list(set(graph[node]) - set(visit)) # graph의 value인 리스트가 정렬되지 않은 상태이므로 정렬 후 이미 검토한 내용을 빼줌
temp.sort() # set은 순서가 없어서 위에 리스트로 만든걸 다시 정렬 해줘야 함. 참고로 셋은 정렬도 못함 리스트만 가능. 순서대로 큐에 더해져야 넓이 탐색 순서를 제대로 지킬 수 있음
queue += temp
return " ".join(str(i) for i in visit)
deque를 사용할 수 있는 큐 리스트(앞으로 방문해야할 인접노드를 저장해두는 곳)와 이미 방문한 노드를 저장할 visit 리스트가 필요하다.
큐가 빈리스트가 되기 전까지는 반복문을 계속 돌면서 가장 처음 저장된 인접노드를 popleft()로 데이터를 들고온다.
만약 그 노드 값이 visit에 없다면 이 순간부터 방문했다는 의미로 visit에 추가해주고, 그 값이 그래프에 있다면 인접노드 리스트에서 이미 방문한 노드를 제외하고 큐에 추가해준다.
여기서 중요한 것은 set은 우리 눈에는 정렬된 값처럼 보이지만 실제로는 인덱스를 갖지않는 순서가 없는 자료형이므로 다시 list화 시켜서 sort해주어야 제대로된 값이 출력된다. (bfs, dfs 동일)
정렬할 때, 조건에 따라 reverse=True 를 해야할 때도 있다.
큐의 인접노드를 모두 방문했다면 queue가 0이되면서 반복문을 탈출하고 방문한 노드를 반환하도록 한다.
from collections import deque # 큐에서 pop 할때 시간복잡도를 낮춰줌
def dfs(graph, v):
visit = []
stack = [v]
while stack:
node = stack.pop()
if node not in visit:
visit.append(node)
if node in graph:
temp = list(set(graph[node]) - set(visit))
temp.sort(reverse=True) # 스택은 뒤에서 마지막에 넣은게 빠지기 때문에 리버스해줘야 순서대로 잘 찾을 수 있음
stack += temp # 가장 최근에 탐색한 노드의 인접한 노드가 스택 뒤에 다시 들어가게 됨
return " ".join(str(i) for i in visit)
n, m, v = map(int, stdin.readline().rstrip().split())
connect_list = [stdin.readline().rstrip().split() for _ in range(m)]
graph = {}
for x, y in connect_list:
x = int(x)
y = int(y)
if x not in graph: # 정점이 그래프에 없으면
graph[x] = [y] # 인접노드를 value로 가지고 key값이 정점인 딕셔너리를 만들어준다.
elif y not in graph[x]: # 해당 정점이 이미 있지만 인접노드 정보가 해당 key의 value값에 없는 경우
graph[x].append(y) # value에 추가해준다.
if y not in graph: # 반대도 똑같은 생각으로 구현해준다.
graph[y] = [x]
elif x not in graph[y]:
graph[y].append(x)
x, y 양방향으로 딕셔너리에 추가해주는 것이 포인트
dfs, bfs 함수를 작성할 때 이 코드가 빠지면 value error가 뜬다. 아무래도 해당 조건을 걸어주지 않으면 생기는 예외가 있는 것 같다.
if node not in visit:
def bfs(graph, v):
visit = []
queue = deque([v])
while queue:
# print(queue)
node = queue.popleft() # 맨처음 들어온 값을 뽑아냄, 선입선출로 여기서 넓이 탐색의 특색이 나타남
if node not in visit: # 중복으로 큐에 들어온 노드값이 여기서 걸러짐 ex) queue = [4, 4, 4]일때 결국 다 방문한 거라서 다 popleft로 빠져나가고 종료됨
visit.append(node)
if node in graph:
for v_node in sorted(list(graph[node])):
if v_node not in visit:
queue.append(v_node)
산술평균, 중앙값, 최빈값, 범위를 구하는 문제다. 특별한 부분은 없고 statistics의 함수들을 활용해서 풀어야한다.
from sys import stdin
import statistics
n = int(stdin.readline())
num = [int(stdin.readline().rstrip()) for _ in range(n)]
num.sort()
mean = statistics.mean(num)
median = statistics.median(num)
mode = statistics.multimode(num)
if len(mode) > 1:
mode = mode[1]
else:
mode = mode[0]
range = num[-1] - num[0]
print(round(mean), median, mode, range, sep='\n')
큰 문제를 작은 문제로 쪼개어서 풀이하는 개념인데, 비슷하게 헷갈리는 개념으로는 백트랙킹과 동적 계획법 등이 있다. 분할 정복은 작게 나누어 생각할 수 있는 문제이지만 각각의 분할된 개념들이 서로에게 영향을 주지 않는 독립적인 분할이며 재귀함수를 활용하여 풀이하게 된다. 각각의 분할된 작은 부분들이 각각 다른조건으로 같은 함수를 공유하게 된다고나 할까
import sys
n = int(sys.stdin.readline())
matrix = [list(map(int,sys.stdin.readline().split())) for _ in range(n)]
one = 0
zero = 0
def cut(x, y, n): # x, y는 좌표 시작점, n은 변의 길이 / x가 가로같지만 리스트 인덱스 특성상 가로임
global one, zero
check = matrix[x][y]
for i in range(x, x + n):
for j in range(y, y + n):
if check != matrix[i][j]: # i가 세로, j가 가로
cut(x, y, n//2) # 1사분면
cut(x, y + n//2, n//2) # 2사분면
cut(x + n//2, y, n//2) # 3사분면
cut(x + n//2, y + n//2, n//2) # 4사분면
return
# 위 이중 반복문을 모두 돌았는데 사분면의 첫번째 좌표 check와 같다면 아래 사항 체크하게 됨
if check == 0:
zero += 1
return
else:
one += 1
return
cut(0, 0, n)
print(zero, one, sep='\n')
matrix = [[1, 1, 0, 0, 0, 0, 1, 1],
[1, 1, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 1, 1, 0, 0],
[0, 0, 0, 0, 1, 1, 0, 0],
[1, 0, 0, 0, 1, 1, 1, 1],
[0, 1, 0, 0, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1, 1]]
이중 for문의 i는 세로, j는 가로 인덱스를 가리키며 각 분할면의 첫 값(check)과 같은지를 비교하게 된다.
백트랙킹의 개념은 오목, 체스의 개념과 비슷하다. 한수 두수를 생각해보면서 그 결과가 맞지않다면 다시 원점으로 돌아와서 다음 수를 다시 생각해나가며 경우들을 계산하며 오답을 제거해 나가는 방식이다.
동적계획법 (Dynamic programming)을 이용한문제다. 동적계획법은 분할정복처럼 큰문제를 작은문제로 쪼개어 생각하는 것은 같지만 분할정복과 달리 작은 문제가 큰문제를 푸는 일에 영향을 준다는 점이다. 따라서 memorization 이라는 계산이 필요한 순간 계산해서 값을 저장해 두는 방식이 필요하다. 이것을 통해 저장했던 값을 재활용해서 사용하는 시간복잡도 O(N)의 매우!!! 효율적인 프로그래밍을 가능하게 만든다.
from sys import stdin
n = int(stdin.readline())
arr = [0] * 300 # 계단 수가 3보다 작은 경우가 있어서 0으로 초기화 시켜주는 것
for i in range(n):
arr[i] = int(stdin.readline())
dp = [0] * 300 # 계단 수가 3보다 작은 경우가 있어서 0으로 초기화 시켜주는 것
dp[0] = (arr[0])
dp[1] = (max(arr[1], arr[0] + arr[1]))
dp[2] = (max(arr[1] + arr[2], arr[0] + arr[2]))
for i in range(3, n): # 맨 마지막으로 append 되는 값이 마지막 계단을 올랐을 때의 최대값
dp[i] = (max(dp[i-2] + arr[i], dp[i-3] + arr[i-1] + arr[i]))
print(dp[n - 1]) # n번째 계단에 해당하는 값 꺼내기
3보다 작은 경우도 고려해줘야한다. 그렇지않으면 인덱스값이 3개가 안되는데 [2]를 호출하는 상황이 발생해서 에러가 발생한다.
그래서 초기값으로 0을 넣어서 모든 인덱스값을 채워넣어준다.
원 좌표와 반지름이 주어질 때, 접점의 갯수를 계산하는 문제이다.
두 원의 중심사이의 거리와 각 원의 반지름 사이의 관계를 이용하여 접점이 2, 1, 0 개인 경우를 따져서 풀이하면 된다.
from sys import stdin
n = int(stdin.readline())
for _ in range(n):
x1, y1, r1, x2, y2, r2 = map(int, stdin.readline().strip().split())
r = ((x1 - x2)**2 + (y1 - y2)**2)**0.5
R = [r, r1, r2]
max_r = max(R)
R.remove(max_r)
if r == 0 and r1 == r2:
print(-1)
elif max_r > sum(R):
print(0)
elif max_r == sum(R):
print(1)
else:
print(2)
from sys import stdin
n = int(stdin.readline())
for _ in range(n):
x1, y1, r1, x2, y2, r2 = map(int, stdin.readline().strip().split())
r = ((x1 - x2)**2 + (y1 - y2)**2)**0.5
if r == 0 and r1 == r2:
print(-1)
elif abs(r1 - r2) == r or r1 + r2 == r:
print(1)
elif abs(r1 - r2) < r < r1 + r2:
print(2)
else:
print(0)
카드를 중복없이 무작위로 3장 뽑았을 때 목표값에 가장 가까운 큰 수를 구하는 문제이다.
처음에는 random함수와 조합을 활용해서 풀려고 했지만 런타임에러 (overflowError)가 발생했다.
브루트포스 방법으로 모든 경우를 탐색하며 결과를 출력한다.
from sys import stdin
n, m = map(int, stdin.readline().strip().split())
cards_num = list(map(int, stdin.readline().strip().split()))
result_guess = []
for i in range(n):
for j in range(i+1, n):
for k in range(j+1, n):
sum_g = cards_num[i] + cards_num[j] + cards_num[k]
if sum_g <= m:
result_guess.append(sum_g)
print(max(result_guess))
주어진 숫자의 생성자를 찾아서 가장 작은 생성자를 출력하고 생성자가 없다면 0을 출력하는 문제다.
from sys import stdin
n = int(stdin.readline().strip())
sum_list = []
for num in range(1, n):
sum_n = num
for i in range(len(str(num))):
sum_n += int(str(num)[i])
if sum_n == n:
sum_list.append(num)
if sum_list:
print(min(sum_list))
else:
print(0)
목표 숫자까지 1부터 모든 수의 분해합을 구해서 해당 숫자가 목표숫자의 생성자가 될 수 있는지를 확인하는 프로그램이다.
처음에는 몫, 나머지를 활용하려고 했으나 각 자릿수가 달라질때마다 고려할 사항이 많았다. 오히려 str, int로 형변환을 하면서 각 자리수를 더해주는 방식을 사용하니 활용도가 더 높은 코드가 만들어졌다.
백트랙킹을 제외하고는 목표했던 40문제를 모두 풀어보았다는 것이 매우 뿌듯했다. 개념을 잘 모르던 문제들은 문제를 풀면서 개념을 익힐 수 있어서 의미있었다. 코딩은 수학이 아니지만 알고리즘을 수학을 잘하게 되는 방법과 같은 노선에 있다는 것을 알 수 있었다. 지금은 개념익힘책을 풀면서 개념을 몸에 익히는 시간이었다고 생각된다. 이제 이것들을 활용할 수 있는 수준이 될때까지 계속해서 꾸준히 풀어봐야한다. 문제를 푸는 힘을 기르고 점점 문제 풀이의 길이 보일 때까지 반복하며 응용력을 길러야 한다.
유튜브에도 정말 좋은 알고리즘 강의들이 있었다. 알고리즘도 막연히 혼자 끙끙거린다고 잘푸는 것이 아니다. 배우고 익히고 응용하는 연습이 많이 필요하다.