Brute Force
Divide and Conquer
Dynamic Programming
Greedy Algorithm
Algorithmic paradigm
알고리즘 클래스 설계의 기초가 되는 일반화된 모델
General
Parameterized complexity
파라미터들과 관련된 어려운 계산 문제에 초점을 둔 알고리즘 패러다임Computational geometry
기하학 알고리즘 패러다임Brute Force
가능한 모든 경우의 수를 시도하는 알고리즘 패러다임
Trapping Rain Water 문제
def trapping_rain(buildings):
max_height = max(buildings)
min_height = min(buildings)
total_cell = 0
for height in range(max_height, min_height, -1):
count = 0
start = 0
end = 0
for i in range(len(buildings)):
if buildings[i] >= height:
if count == 0:
start = i
end = i
count += 1
# height 값에 해당하는 층에 빗물이 있는 셀 갯수
adding_cell = end - start - count + 1
total_cell += adding_cell
return total_cell
# test
print(trapping_rain(\[3, 0, 0, 2, 0, 4]))
print(trapping_rain(\[0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]))
10
6
m = max_height - min_height
for i in range(len(buildings))
부분에서 이미 count된 빌딩을 반복문에서 다시 조회하지 않도록 코드를 수정한다면, Best-case time complexity는 확실히 줄어들 것입니다.Divide and Conquer
문제를 작은 문제로 분할하여 문제를 해결하는 알고리즘 패러다임
divide
문제를 두 개 이상의 부분 문제로 재귀적으로 분류하는 과정conquer
문제가 직접 해결될 수 있을 정도로 간단해진 부분 문제의 솔루션을 구하는 과정combine
부분 문제에 대한 솔루션을 결합하여 원래 문제의 답을 구하는 과정Merge Sort
divide
배열을 반으로 나눕니다.conquer
왼쪽 배열과 오른쪽 배열을 각각 정렬한다.combine
정렬된 두 배열을 하나의 정렬된 배열로 합병한다.def merge(list1, list2):
merged_list = []
counter = 0
for i in range(len(list1) + len(list2)):
if counter == len(list1):
merged_list.append(list2[i - counter])
elif (i - counter) == len(list2):
merged_list.append(list1[counter])
counter += 1
elif list1[counter] < list2[i - counter]:
merged_list.append(list1[counter])
counter += 1
else:
merged_list.append(list2[i - counter])
return merged_list
def merge_sort(my_list):
if len(my_list) < 2 :
return my_list
mid = len(my_list) // 2
return merge(merge_sort(my_list[:mid]), merge_sort(my_list[mid:]))
# test
print(merge_sort([1, 3, 5, 7, 9, 11, 13, 11]))
print(merge_sort([28, 13, 9, 30, 1, 48, 5, 7, 15]))
print(merge_sort([2, 5, 6, 7, 1, 2, 4, 7, 10, 11, 4, 15, 13, 1, 6, 4]))
[1, 3, 5, 7, 9, 11, 11, 13]
[1, 5, 7, 9, 13, 15, 28, 30, 48]
[1, 1, 2, 2, 4, 4, 4, 5, 6, 6, 7, 7, 10, 11, 13, 15]
Quick Sort
divide
배열 중 요소 하나를 pivot으로 정하고, pivot 앞에는 pivot보다 값이 작은 모든 요소들이 오고, pivot 뒤에는 pivot보다 값이 큰 모든 원소들이 오도록 분할(partition)합니다.conquer
pivot의 왼쪽 요소들을 모은 배열과 오른쪽 요소들을 모은 배열을 정렬합니다.combine
없습니다.def swap_elements(my_list, index1, index2):
temp = my_list[index1]
my_list[index1] = my_list[index2]
my_list[index2] = temp
def partition(my_list, start, end):
p = end
b = start
for i in range(start, end):
if my_list[i] < my_list[p]:
swap_elements(my_list, i, b)
b += 1
swap_elements(my_list, b, p)
p = b
return p
def quicksort(my_list, start = 0, end = None):
if end == None:
end = len(my_list) - 1
if start < end:
p = partition(my_list, start, end)
quicksort(my_list, start, p - 1)
quicksort(my_list, p + 1, end)
# test
list1 = [1, 3, 5, 7, 9, 11, 13, 11]
quicksort(list1)
print(list1)
list2 = [28, 13, 9, 30, 1, 48, 5, 7, 15]
quicksort(list2)
print(list2)
list3 = [2, 5, 6, 7, 1, 2, 4, 7, 10, 11, 4, 15, 13, 1, 6, 4]
quicksort(list3)
print(list3)
[1, 3, 5, 7, 9, 11, 11, 13]
[1, 5, 7, 9, 13, 15, 28, 30, 48]
[1, 1, 2, 2, 4, 4, 4, 5, 6, 6, 7, 7, 10, 11, 13, 15]
Dynamic Programming
Optimal Substructure
, Overlapping Subproblems
를 가지고 있는 문제를 작은 문제로 분할하여 문제를 해결하는 알고리즘 패러다임Memorization
또는 Tabulation
방법으로 구현할 수 있다.Optimal Substructure
Overlapping Subproblems
Memorization
def fib_memo(n, cache):
# base case
if n < 3:
return 1
# recursive case
if n in cache:
return cache[n]
cache[n] = fib_memo(n - 1, cache) + fib_memo(n - 2, cache)
return cache[n]
def fib(n):
# n번째 피보나치 수를 담는 사전
fib_cache = {}
return fib_memo(n, fib_cache)
# test
print(fib(10))
print(fib(50))
print(fib(100))
55
12586269025
354224848179261915075
Tabulation
def fib_tab(n):
table = []
for i in range(n):
if i < 2:
table.append(1)
else:
table.append(table[i - 1] + table[i - 2])
return table[n - 1]
# test
print(fib_tab(10))
print(fib_tab(56))
print(fib_tab(132))
55
225851433717
1725375039079340637797070384
def fib_tab(n):
current = 1
previous = 0
for i in range(1, n):
current, previous = current + previous, current
return current
# 테스트 코드
print(fib_tab(16))
print(fib_tab(53))
print(fib_tab(213))
987
53316291173
146178119651438213260386312206974243796773058
Memorization
vs Tabulation
Memorization
Tabulation
Maximizing Profit Problem
count
판매할 사탕 개수price_list
개수별 가격이 정리되어 있는 배열count
가 5, price_list
가 [0, 100, 400, 800, 900, 1000]이라면 한 사람에게 3개를 팔고 다른 사람에게 2개를 팔아서 최대 1,200의 수익을 낼 수 있다.def max_profit_memo(price_list, count, cache):
# base case
if count < 2:
cache[count] = price_list[count]
return cache[count]
# recursive case
if count in cache:
return cache[count]
# profit: count개를 팔아서 가능한 최대 수익
if count < len(price_list):
profit = price_list[count]
else:
profit = 0
for i in range(1, count // 2 + 1):
profit = max(profit, max_profit_memo(price_list, i, cache)
+ max_profit_memo(price_list, count - i, cache))
cache[count] = profit
return profit
def max_profit(price_list, count):
# cache: 개수별 최대 수익이 저장되어 있는 사전
max_profit_cache = {}
return max_profit_memo(price_list, count, max_profit_cache)
# test
print(max_profit([0, 100, 400, 800, 900, 1000], 5))
print(max_profit([0, 100, 400, 800, 900, 1000], 10))
print(max_profit([0, 100, 400, 800, 900, 1000, 1400, 1600, 2100, 2200], 9))
1200
2500
2400
def max_profit(price_list, count):
profit_table = [0]
for i in range(1, count + 1):
# profit: count개를 팔아서 가능한 최대 수익
if i < len(price_list):
profit = price_list[i]
else:
profit = 0
for j in range(1, i // 2 + 1):
profit = max(profit, profit_table[j] + profit_table[i - j])
profit_table.append(profit)
return profit_table[count]
2000
2400
1800
Greedy Algorithm
각 단계에서 국소적으로 최적의 선택을 하는, problem-solving heuristics을 따르는 알고리즘 패러다임
heuristic
체계적이면서 합리적인 판단을 할 수 없거나 필요하지 않을 때 빠르게 사용할 수 있는 간편추론의 방법Optimal Substructure
, Greedy Choice Property
를 가지고 있는 문제는 Greedy Algorithm으로 최적의 답을 보장할 수 있다.Greedy Choice Property
globally optimal solution이 locally optimal choice로부터 얻어질 수 있는 특성
최소 동전으로 거슬러 주기 문제
value
거슬러 줘야 하는 총액coin_list
사용 가능한 동전 금액 리스트def min_coin_count(value, coin_list):
# count: 누적 동전 개수
count = 0
for coin in sorted(coin_list, reverse=True):
count += (value // coin)
value %= coin
return count
# 테스트 코드
default_coin_list = [100, 500, 10, 50]
print(min_coin_count(1440, default_coin_list))
print(min_coin_count(1700, default_coin_list))
print(min_coin_count(23520, default_coin_list))
print(min_coin_count(32590, default_coin_list))
10
5
49
70
최대 곱 구하기 문제
def max_product(card_lists):
product = 1
for card_list in card_lists:
product *= max(card_list)
return product
# test
test_cards1 = [[1, 6, 5], [4, 2, 3]]
print(max_product(test_cards1))
test_cards2 = [[9, 7, 8], [9, 2, 3], [9, 8, 1], [2, 8, 3], [1, 3, 6], [7, 7, 4]]
print(max_product(test_cards2))
test_cards3 = [[1, 2, 3], [4, 6, 1], [8, 2, 4], [3, 2, 5], [5, 2, 3], [3, 2, 1]]
print(max_product(test_cards3))
test_cards4 = [[5, 5, 5], [4, 3, 5], [1, 1, 1], [9, 8, 3], [2, 8, 4], [5, 7, 4]]
print(max_product(test_cards4))
수강 신청 문제
[(4, 7), (2, 5), (1, 3), (8, 10), (5, 9), (2, 6), (13, 16), (9, 11), (1, 8)]
course_selection
파라미터로 전체 수업 리스트를 받고 가능한 많은 수업을 담은 리스트를 리턴하는 함수def course_selection(course_list):
# 수업을 끝나는 순서로 정렬한다
sorted_list = sorted(course_list, key=lambda x: x[1])
# 가장 먼저 끝나는 수업은 무조건 듣는다
my_selection = [sorted_list[0]]
# 이미 선택한 수업과 안 겹치는 수업 중 가장 빨리 끝나는 수업을 고른다
for course in sorted_list:
# 마지막 수업이 끝나기 전에 새 수업이 시작하면 겹친다
if course[0] > my_selection[-1][1]:
my_selection.append(course)
return my_selection
# 테스트 코드
print(course_selection([(6, 10), (2, 3), (4, 5), (1, 7), (6, 8), (9, 10)]))
print(course_selection([(1, 2), (3, 4), (0, 6), (5, 7), (8, 9), (5, 9)]))
print(course_selection([(4, 7), (2, 5), (1, 3), (8, 10), (5, 9), (2, 5), (13, 16), (9, 11), (1, 8)]))
[(2, 3), (4, 5), (6, 8), (9, 10)]
[(1, 2), (3, 4), (5, 7), (8, 9)]
[(1, 3), (4, 7), (8, 10), (13, 16)]
Reference