[그리디]

 

- 큰 수의 법칙

큰 수 두개만 골라서 쓰면 된다

 

- 숫자 카드 게임

각 행마다 가장 작은 수를 찾고 그 수 중에서 가장 큰 수를 찾는 것

 

- 1이 될 때까지

최대한 많이 나누기

1을 빼는 작업은 몫에 k를 곱함으로써 한번에 수행할 수 있다

더보기
n, k = map(int, input().split())
count = 0

while n!=1:
  if n%k==0:
    n = n//k
    count += 1
  else:
    n -= 1
    count += 1

print(count)

이코테 책에서는 이보다 더 빠른 방법을 사용했다

 

- 곱하기 혹은 더하기

대부분 곱하는게 이득이지만 두 수중 0 또는 1인 경우는 더하는게 이득이다.

더보기
data = input()

result = int(data[0])

for i in range(1, len(data)):
  num = int(data[i])
  if result <= 1 or num <= 1:
    result += num
  else:
    result *= num

print(result)

 

- 모험가 길드

정렬 후 낮은 공포도부터 그룹 결성을 시키면 된다

더보기
n = int(input())
data = list(map(int, input().split()))
data.sort()

result = 0
f = 0.0

# 인원수가 가장 낮은 공포도보다 적으면 안됨
while len(data) > data[0]:
  f += 1.0/float(data[0]) # 1/공포도 만큼을 인원수만큼 더해서 1이 넘으면 그룹 결성
  data.pop(0) # 인원을 빼줌
  if f >= 1.0:
    result += 1
    f = 0.0

print(result)

개념 접근 자체는 이코테의 해답과 비슷한데 좀 더 복잡하게 생각했기때문에 단순화가 필요함

연산도 필요없고 그저 카운팅만 시키면 되는 단순한 해답이 있음

 

[구현]

 

- 상하좌우

기준점을 두고 Vector이동이라고 가정하면 구현하기 쉽다

더보기
dx = [0, 0, -1, 1]
dy = [-1, 1, 0, 0]
move_types = ['L', 'R', 'U', 'D']
x, y = 1, 1

n = int(input())
plans = list(map(str, input().split()))

for plan in plans:
  for i in range(len(move_types)):
    if plan == move_types[i]:
      nx = x + dx[i] # 반복문 안에서 선언된 변수도 외부에서 사용할 수 있는듯 하다
      ny = y + dy[i] # c/c++이랑 다른 문법인것 같음

  if nx < 1 or ny < 1 or nx > n or ny > n:
    continue
  x, y = nx, ny

print(x, y)

 

- 시각

범위가 하루로 잡혀있고 하루는 86400초이기 때문에 복잡하게 생각할 것 없이 단순하게 모든 초를 조사하면 된다

더보기
n = int(input())
three = 15 # 0~60 사이 3이 포함된 숫자의 개수
result = 0

for i in range(n+1):
  if i==3 or i==13 or i==23: # 시간에 3이 포함되면 1시간 전체가 포함됨 
    result += 3600
  else: # 분에 3이 포함되면 60초가 모두 포함되어야 하고 그 외엔 초에 포함된 3의 경우만 고려하면 됨
    result += three * 60 + (60-three) * three

print(result)

평소처럼 단순하게 생각했으면 더 쉽게 풀수 있었을텐데 경우의 수를 계산하느라 시간제한 15분이 넘어버렸다

 

- 왕실의 나이트

8X8로 제한이 되어있으니 완전 탐색으로 구현하면 됨

더보기
# 행row 위치는 1-8, 열column 위치는 a-h
# 따로 주석 달게 없음

# 리스트 이용
steps = { (2, -1), (2, 1), (1, 2), (-1, 2), (-2, -1), (-2, 1), (-1, -2), (1, -2) }

data = input()
row = int(data[1])
column = int(ord(data[0])) - int(ord('a')) + 1
result = 0

for step in steps:
  nrow = row + step[0]
  ncolumn = column + step[1]

  if nrow < 1 or ncolumn < 1 or nrow > 8 or ncolumn > 8:
    continue
  result += 1

print(result)

 

- 문자열 재정렬

완전 탐색. 알파벳과 숫자를 분리한 후 차후 합하면 됨

더보기
s = list(input())
s.sort() # 리스트로 입력 받고 정렬
result = 0

while True:
  # 오름차순 정렬 시 맨 앞에는 숫자가 오므로 숫자인지 판별
  if s[0].isdigit(): 
    result += int(s[0])
    # 다음 문자 확인을 위해 리스트에서 제거
    s.remove(s[0])
  else:
    break

print(''.join(s)+str(result))

 

이코테에서는 입력받은 문자열을 모두 순회하면서 문자는 리스트에, 숫자는 합산해서 따로 관리 후 마지막에 숫자 합을 리스트에 추가시켜서 출력함

다만 이런경우는 문자열 관련 기능이 강력한 파이썬에서나 가능한거고 c/c++ 환경에서는 완전 탐색으로 구현해야 할 것 같다

 

- 게임 개발

문제 이해하는게 가장 어려움. dx, dy라는 별도의 벡터를 이용하여 구현하면 어렵지 않음

더보기
# # 0:북 1:동 2:남 3:서
# # 0:육지 1:바다

n, m = map(int, input().split())
x, y, direction = map(int, input().split())

# 이동 벡터
dx = [0, 1, 0, -1]
dy = [-1, 0, 1, 0]

d = [[0] * m for _ in range(n)]
d[x][y] = 1 # 첫 위치는 방문한걸로 기록

map_list = []
for i in range(n):
  map_list.append(list(map(int, input().split())))

def turn_left():
  global direction
  direction -= 1
  
  if direction < 0:
    direction = 3

turn_time = 0
count = 1

while True:
  turn_left()
  
  nx = x + dx[direction]
  ny = y + dy[direction]

  # 방문한적이 있거나 바다인 경우
  if d[nx][ny] == 1 or map_list[nx][ny] == 1:
    turn_time += 1
  else:
    d[nx][ny] = 1
    x = nx
    y = ny
    count += 1
    turn_time = 0
    continue

  # 4번의 회전을 모두 한 경우
  if turn_time == 4:
    nx = x - dx[direction]
    ny = y - dy[direction]

    # 뒤가 바다라면 시뮬레이션 종료
    if map_list[nx][ny] == 1:
      break
    else:
      x = nx
      y = ny
    turn_time = 0

print(count)

 

[DFS/BFS]

 

- 음료수 얼려 먹기

주어진 노드들을 따로 관리할 필요가 없으므로 visited를 별도로 관리하지 않아도 되고 DFS로 구현하면 된다

더보기
# N x M 얼음틀
# 구멍이 뚫린 부분은 0, 칸막이가 존재하는 부분은 1
# 구멍이 뚫린 부분끼리 상, 하, 좌, 우가 붙어있는 경우 서로 붙은것으로 간주

n, m = map(int, input().split())

graph = []
for i in range(n):
  graph.append(list(map(int, input())))

def dfs(x, y):
  # 범위를 벗어나는 경우 즉시 종료
  if x <= -1 or x >= n or y <= -1 or y >= m:
    return False

  # 아직 방문하지 않은 곳일 때
  if graph[x][y] == 0:
    # 방문처리 후 상하좌우 모두 탐색
    graph[x][y] = 1
    dfs(x+1, y)
    dfs(x-1, y)
    dfs(x, y+1)
    dfs(x, y-1)
    return True
  return False

count = 0
# 모든 노드에 대하여 조사
for i in range(n):
  for j in range(m):
    if dfs(i, j) == True:
      count += 1

print(count)

 

- 미로 탈출

노드간의 간선 값이 모두 1이므로 목표지점까지의 최단거리를 보장할 수 있다. BFS로 구현

더보기
# 미로의 크기는 NxM이고 출구는 좌표는 (N, M)

from collections import deque

n, m = map(int, input().split())
# 상, 하, 좌, 우
dx = (-1, 1, 0, 0)
dy = (0, 0, -1, 1)

maze = []
for i in range(n):
  maze.append(list(map(int, input())))

def bfs(x, y):
  queue = deque()
  # 최초 시작노드 큐에 삽입
  queue.append((x, y))

  while queue:
    x, y = queue.popleft()

    # 각 노드에 연결된 최대 4방향에 대해 확인
    for i in range(4):
      nx = x + dx[i]
      ny = y + dy[i]
      
      if nx <= -1 or nx >= n or ny <= -1 or ny >= m:
        continue
      
      if maze[nx][ny] == 0:
        continue

      if maze[nx][ny] == 1:
        maze[nx][ny] = maze[x][y] + 1
        queue.append((nx, ny))

  return maze[n-1][m-1]

print(bfs(0, 0))

 

[정렬]

 

- 위에서 아래로

내림차순 정렬만 하면 된다

더보기
n = int(input())

arr = []
for i in range(n):
  arr.append(int(input()))

# 내림차순 정렬
arr.sort(reverse=True)

for i in range(n):
  print(arr[i], end=' ')

 

- 두 배열의 원소 교체

오름차순, 내림차순 정렬 후에 값 교체해주고 더하면 된다

더보기
# 두 배열을 각각 오름차순, 내림차순 정렬 후 k만큼 앞에서 부터 바꿔주면 됨

n, k = map(int, input().split())

aArr = list(map(int, input().split()))
bArr = list(map(int, input().split()))

aArr.sort() # 오름차순
bArr.sort(reverse=True) # 내림차순

for i in range(k):
  if aArr[i] < bArr[i]: # a보다 b가 큰 경우 서로 값을 바꿔줌
    aArr[i], bArr[i] = bArr[i], aArr[i]
  # 더이상 a보다 큰 수는 없으면 루프 탈출
  else:
    break

print(sum(aArr))

 

[이진 탐색]

 

- 떡볶이 떡 만들기

적절한 높이를 찾을 때까지 이진 탐색을 수행하여 높이를 반복 조정하면 됨

더보기
n, m = map(int, input().split())
hlist = list(map(int, input().split()))

start = 0
end = max(hlist)

result = 0
while start <= end:
  total = 0
  mid = (start + end) // 2

  for x in hlist:
    # 잘랐을 때 떡의 양 계산
    if x > mid:
      total += x - mid

  # 떡의 양이 작으면 자르는 높이를 줄인다
  if total < m:
    end = mid - 1
  # 떡의 양이 많으면 자르는 높이를 늘린다
  # 최대한 덜 자르는게 목표이므로 이때 결과값을 보관해둔다
  else:
    start = mid + 1
    result = mid

print(result)

 

이 문제의 경우 이진 탐색을 이용한다고 해서 데이터의 정렬이 반드시 필요한 것은 아니다

찾아야 하는 해가 계속 바뀌기 때문에 탐색 과정에 이진 탐색의 개념만 도입하여 파라메트릭 서치로 바꾼것이다

 

- 정렬된 배열에서 특정 수의 개수 구하기

bisect(C++에서는 lower_bound, upper_bound) 사용시 매우 쉽게 구할 수 있다

더보기
def bisect_left(array, x):
  start = 0
  end = len(array) - 1
  result = 0
  
  while start <= end:
    mid = (start + end) // 2

    # 값을 찾았다면 루프 종료가 아니라 mid 값을 보관하고 중복된 값을 찾으러 범위를 좁혀준다
    # 왼쪽을 찾아야 하므로 끝점을 땡겨준다
    if array[mid] == x:
      result = mid
      end = mid - 1
    elif array[mid] > x:
      end = mid - 1
    else:
      start = mid + 1

  # 값 삽입시 현재 인덱스의 위치에 삽입하고 밀어내면 된다
  return result

def bisect_right(array, x):
  start = 0
  end = len(array) - 1
  # 마지막에 1을 더해주기 때문에 0을 만들어주기 위해 -1 대입
  result = -1
  
  while start <= end:
    mid = (start + end) // 2
    
    # 값을 찾았다면 루프 종료가 아니라 mid 값을 보관하고 중복된 값을 찾으러 범위를 좁혀준다
    # 오른쪽을 찾아야 하므로 시작점을 끌어올려준다
    if array[mid] == x:
      result = mid
      start = mid + 1
    elif array[mid] > x:
      end = mid - 1
    else:
      start = mid + 1

  # 값 삽입시 현재 인덱스보다 오른쪽에 삽입해야 하기때문에 1을 더해준다
  return result + 1

n, x = map(int, input().split())
array = list(map(int, input().split()))

result = bisect_right(array, x) - bisect_left(array, x)

if result == 0:
  print(-1)
else:
  print(result)

 

아래처럼 bisect 라이브러리를 이용해서 쉽고 간단하게 구현할 수 있지만 연습을 위해 직접 구현

실제 bisect는 값을 찾지 못하면 정렬이 깨지지 않는 적절한 인덱스를 반환한다

그것에 맞춰서 수정한건 아래와 같다

 

def bisect_left(array, x):
  start = 0
  end = len(array) - 1
  # 찾는 값이 리스트 안의 모든 값보다 클때 result값이 변하는 일이 없으므로 미리 삽입될 끝 인덱스 지정
  result = len(array)
  
  while start <= end:
    mid = (start + end) // 2

    # 값을 찾았다면 루프 종료가 아니라 mid 값을 보관하고 중복된 값을 찾으러 범위를 좁혀준다
    # 왼쪽을 찾아야 하므로 끝점을 땡겨준다
    # 값을 찾지 못한경우 적절한 위치를 찾기 위해 부등호 >= 를 사용해준다
    if array[mid] >= x:
      result = mid
      end = mid - 1
    elif array[mid] > x:
      end = mid - 1
    else:
      start = mid + 1

  # 값 삽입시 현재 인덱스의 위치에 삽입하고 밀어내면 된다
  return result

def bisect_right(array, x):
  start = 0
  end = len(array) - 1
  # 마지막에 1을 더해주기 때문에 0을 만들어주기 위해 -1 대입
  result = -1
  
  while start <= end:
    mid = (start + end) // 2
    
    # 값을 찾았다면 루프 종료가 아니라 mid 값을 보관하고 중복된 값을 찾으러 범위를 좁혀준다
    # 오른쪽을 찾아야 하므로 시작점을 끌어올려준다
    # 값을 찾지 못한경우 적절한 위치를 찾기 위해 부등호 <= 를 사용해준다
    if array[mid] <= x:
      result = mid
      start = mid + 1
    elif array[mid] > x:
      end = mid - 1
    else:
      start = mid + 1

  # 값 삽입시 현재 인덱스보다 오른쪽에 삽입해야 하기때문에 1을 더해준다
  return result + 1

 

값을 찾지 못한경우 두 함수의 반환값은 모두 같다. 실제 bisect_left, bisect_right와 동일한 반환값을 가진다

 

따로 구현하지 않고 사용시에는 최상단에 from bisect import bisect_left, bisect_right 를 선언해주고 동일하게 쓰면 된다

 

 

- 부품 찾기

보유중인 부품들을 정렬한 후 이진 탐색을 실시하면 된다

더보기
# 퀵 정렬
def quick_sort(array, start, end):
  if start >= end:
    return

  pivot = start
  left = start + 1
  right = end

  while left <= right:
    while left <= end and array[left] <= array[pivot]:
      left += 1
    while right > start and array[right] >= array[pivot]:
      right -= 1
    if (left > right):
      array[right], array[pivot] = array[pivot], array[right]
    else:
      array[right], array[left] = array[left], array[right]

  quick_sort(array, start, right - 1)
  quick_sort(array, right + 1, end)

  return

# 이진 탐색
def binary_search(array, target, start, end):
  if start > end:
    return None
    
  mid = (start + end) // 2

  if array[mid] == target:
    return mid
  elif array[mid] > target:
    return binary_search(array, target, start, mid-1)
  else:
    return binary_search(array, target, mid+1, end)
    
  
  
n = int(input())
nlist = list(map(int, input().split()))

m = int(input())
mlist = list(map(int, input().split()))

quick_sort(nlist, 0, n - 1)

for x in mlist:
  result = binary_search(nlist, x, 0, n - 1)
  if result == None:
    print("no", end=' ')
  else:
    print("yes", end=' ')

 

퀵정렬은 구현하지 않고 리스트의 sort를 이용해도 되는데 연습할 겸 직접 작성해서 넣어봤음

 

[다이나믹 프로그래밍]

 

- 개미 전사

점화식을 만들어내는게 중요하다

더보기
n = int(input())
warehouse = list(map(int, input().split()))
dp = [0] * 100

dp[0] = warehouse[0]
dp[1] = max(warehouse[0], warehouse[1])

for i in range(2, n):
  dp[i] = max(dp[i-1], dp[i-2] + warehouse[i])

print(dp[n - 1])

 

점화식은 다음과 같이 정의할 수 있다.

한 칸 이상 떨어진 식량창고는 항상 털 수 있으므로 (i-3)번째 이하는 고려할 필요가 없다.

점화식을 유도해내는게 어렵다.

 

- 1로 만들기

그리디와 구분을 지을줄 알아야 하고 마찬가지로 점화식을 잘 세워야한다

더보기
x = int(input())
dp = [0] * 30001

# Bottom-up
# dp[1]은 0이라서 2부터 수행이 가능하다
for i in range(2, x + 1):
  # 현재 수에서 1을 빼는경우. 연산횟수 1회가 증가된다
  dp[i] = dp[i - 1] + 1

  if i % 2 == 0:
    # 현재 수가 2로 나누어 떨어지는 경우, 1을 뺀 현재 연산횟수보다
    # 2로 나누었을때의 연산횟수가 더 적은지 확인한다.
    # 2로 나누는 과정 자체도 한번의 연산횟수이기 때문에 1을 더해준 값을 비교한다.
    # 좌항은 연산횟수 1을 안더하는 이유는 이미 위에서 더해줬기 때문이다.
    dp[i] = min(dp[i], dp[i // 2] + 1)
    # 3으로 나누어 떨어지는 경우
  if i % 3 == 0:
    dp[i] = min(dp[i], dp[i // 3] + 1)
    # 5로 나누어 떨어지는 경우
  if i % 5 == 0:
    dp[i] = min(dp[i], dp[i // 5] + 1)

# dp에 저장된 값만 꺼내오면 된다
print(dp[x])

 

그리디처럼 큰 수인 5로 먼저 나누는것이 항상 능사가 아닌것을 알아야한다.

예를들어 27인경우 5로 먼저 나누려고 하면 26-25-5-1 총 4번의 연산이 이루어지지만 3으로 나눈다면 9-3-1 총 3번의 연산만으로 1을 만들수 있다. 그렇기에 그리디로는 풀 수 없다.

 

- 효율적인 화폐 구성

더보기
n, m = map(int, input().split())
dp = [10001] * (m + 1)

money = []
for i in range(n):
  money.append(int(input()))

dp[0] = 0
for i in money:
  for j in range(i, m + 1):
    # (i-k)원을 만드는 방법이 존재하는 경우
    if dp[j - i] != 10001:
      # 1을 더해주는 이유는 화폐 장수 한장이 추가되기 때문
      dp[j] = min(dp[j], dp[j - i] + 1)
    
if dp[m] == 10001:
  print(-1)
else:
  print(dp[m])

문제를 잘못 이해해서 30분정도 삽질을 했었다. 심플하게 잘 풀었다고 생각했는데 조건 자체가 잘못됐었음

 

 

[최단 경로]

 

- 미래 도시

어떤 최단 경로 알고리즘을 쓰는지 우선 파악해야한다.

더보기
from sys import stdin
input = stdin.readline
# 무한 값
INF = int(1e9)

n, m = map(int, input().split())

# 최단 거리 테이블 값을 무한으로 초기화
table = [[INF] * (n + 1) for _ in range(n + 1)]

# 자기 자신에서 자기 자신으로 가는 거리는 0
for i in range(1, n+1):
  table[i][i] = 0

for i in range(m):
  x1, x2 = map(int, input().split())
  # 양방향이면서 거리가 모두 1
  table[x1][x2] = 1
  table[x2][x1] = 1

# 목적지 x와 경유지k
x, k = map(int, input().split())

# 정점 개수만큼 순회 O(N)
for i in range(1, n + 1):
  # 2차원 최단 거리 테이블 갱신 O(N²)
  for x1 in range(1, n + 1):
    for x2 in range(1, n + 1):
      table[x1][x2] = min(table[x1][x2], table[x1][i] + table[i][x2])

# 1부터 경유지까지의 거리 + 경유지부터 목적지까지의 거리
distance = table[1][k] + table[k][x]

# INF가 넘는다면 경로가 없음
if distance >= INF:
  print(-1)
else:
  print (distance)

 

처음 다익스트라로 구현하다가 아닌 것 같아서 보니까 플루이드-마샬이었다.

그리고 공부 직후 구현하려니까 자잘한 부분이 생각이 안나서 조금씩 참고하면서 작성했다.

 

- 전보

도시-도시간의 최단 거리 문제로 치환할 수 있다

더보기
from sys import stdin
import heapq
input = stdin.readline
# 무한 값
INF = int(1e9)

n, m, c = map(int, input().split())

graph = [[] for _ in range(n + 1)]
distance = [INF] * (n + 1)

for _ in range(m):
  x, y, z = map(int, input().split())
  graph[x].append((y, z))

def dijkstra(start):
  q = []
  heapq.heappush(q, (0, start))
  distance[start] = 0

  while q:
    dist, now = heapq.heappop(q)

    if dist > distance[now]:
      continue

    for x in graph[now]:
      cost = dist + x[1]
      if cost < distance[x[0]]:
        distance[x[0]] = cost
        heapq.heappush(q, (cost, x[0]))

dijkstra(c)

count = 0
max_distance = 0

# 도달할 수 있는 노드중 가장 멀리있는 노드와의 최단거리
for x in distance:
  if x != INF:
    count += 1
    max_distance = max(max_distance, x)

# 시작 노드는 제외해야 하므로 -1
print(count - 1, max_distance)

 

다익스트라 알고리즘은 이제 생각하면서 짜면 95%정도는 나오는 수준은 됐음

마지막 도시 개수와 거리 뽑아내는 부분에서 살짝 뇌정지와서 책의 도움을 받았음

 

 

[그래프 이론]

 

- 팀 결성

서로소 집합 알고리즘을 사용하면 된다

더보기
from sys import stdin
input = stdin.readline

# 부모 리스트, 찾을값
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

n, m = map(int, input().split())
parent = [x for x in range(n + 1)] # 부모 리스트 초기화

for i in range(m):
  x, a, b = map(int, input().split())

  # union
  if x == 0:
    union_parent(parent, a, b)
  # find
  else:
    if find_parent(parent, a) == find_parent(parent, b): # 서로의 루트가 같다면
      print("YES")
    else:
      print("NO")

서로소 집합 알고리즘 생각나는대로 풀어서 쭉 쓰면 그대로 답임

 

- 도시 분할 계획

최소 신장 트리를 두개로 나누는 방법을 이용하면 된다

더보기
from sys import stdin
input = stdin.readline

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

# 노드n 간선m. a,b,c : a-b의 연결비용 c
n, m = map(int, input().split())
parent = [x for x in range(n + 1)]

edges = [] # 간선 저장 리스트

for i in range(m):
  a, b, c = map(int, input().split())
  edges.append((c, a, b))

edges.sort()

answer = 0
max_cost = 0
for edge in edges:
  cost, a, b = edge

  # 사이클이 아닌 경우만 수행
  if find_parent(parent, a) != find_parent(parent, b):
    union_parent(parent, a, b)
    answer += cost
    max_cost = cost # 비용순으로 정렬되어서 가장 마지막에 들어온 값이 클수밖에 없음

# 가장 높은 비용을 빼서 트리를 2개로 분리
print(answer - max_cost)

 

- 커리큘럼

추후 다시 풀어볼 예정

 

'알고리즘 > 문제풀이' 카테고리의 다른 글

백준(BOJ) 문제 풀이  (0) 2022.07.20
코드업 문제 풀이  (0) 2022.07.15

+ Recent posts