7-0. 해밀턴 경로 문제(Hamiltonian Path Problem)

  • 연결된 무방향 그래프의 한 정점에서 출발하여, 그래프의 모든 정점을 한 번씩만 방문하고 다시 원래 출발한 정점으로 되돌아오는 경로 구하기
  • 오일러 경로(Eulerian Path): 모든 간선을 한 번씩만 방문 --- 한 붓 그리기
  • 백트래킹으로 해결

def promising(i, W, vindex):
    flag = True
    if i == (N - 1) and not W[vindex[N - 1]][vindex[0]]:
        flag = False
    elif i > 0 and not W[vindex[i - 1]][vindex[i]]:
        flag = False
    else:
        j = 1
        while j < i and flag:
            if vindex[i] == vindex[j]:
                flag = False
            j += 1
    return flag

def hamiltoninan(i, W, vindex):
    if promising(i, W, vindex):
        if i == N - 1:
            print(vindex[0:N])
        else:
            for j in range(2, N + 1):
                vindex[i + 1] = j
                hamiltoninan(i + 1, W, vindex)

N = 4
edges = [
    [1, 2],
    [1, 3],
    [1, 4],
    [2, 3],
    [2, 4],
    [3, 4]
]
W = [[False] * (N + 1) for _ in range(N + 1)]
for edge in edges:
    W[edge[0]][edge[1]] = W[edge[1]][edge[0]] = True
vindex = [-1] * (N + 1)
vindex[0] = 1
hamiltoninan(0, W, vindex)

7-1. 외판원 문제(Traveling Salesperson Problem)

  • 해밀턴 경로 문제에서 그래프에 가중치가 추가되어 최단 비용이 되는 경로 구하기
  • W: 주어진 그래프 G = (V, E)의 인접 행렬
  • V: 모든 도시의 집합
  • A: V의 부분 집합
  • D[vi][A]: A에 속한 도시를 각각 한 번씩만 거쳐서 vi에서 v1로 가는 최단 경로의 길이

1) 동적 계획법으로 해결

  • 1. 재귀 관계식
    i != 1 이고 vi가 A에 속하지 않으면
         D[vi][A] = minimum(W[i][j] + D[vj][A-{vj}]), A!=ø
        D[vi][ø] = W[i][j], A!=ø
    2. 상향식 방법
         P[n][W]를 계산하려면 P[n-1][W], P[n-1][W-wn]만 필요
         P[i][w]를 계산하려면 P[i-1][w], P[i-1][w-wi]만 필요
def isIn(i, A): # 부분집합 A에 vi가 있는지
    if A & (1 << (i - 2)) != 0:
        return True
    else:
        return False

def diff(A, j): # 부분집합 A에서 vj를 제외
    t = 1 << j - 2
    return A & ~t

def count(A, n): # A의 원소 갯수
    cnt = 0
    for i in range(n):
        if A & (1 << i) != 0:
            cnt += 1
    return cnt

def minimum(W, D, i, A):
    minV = INF
    minJ = 1
    n = len(W) - 1
    for j in range(2, n + 1):
        if isIn(j, A):
            m = W[i][j] + D[j][diff(A, j)]
            if minV > m:
                minV = m
                minJ = j
    return minV, minJ

def travel_dp(W):
    n = len(W) - 1
    size = 2 ** (n - 1)
    D = [[0] * size for _ in range(n + 1)]
    P = [[0] * size for _ in range(n + 1)]
    for i in range(2, n + 1):
        D[i][0] = W[i][1]
    for k in range(1, n - 1):
        for A in range(1, size):
            if count(A, n) == k:
                for i in range(2, n + 1):
                    if not isIn(i, A):
                        D[i][A], P[i][A] = minimum(W, D, i, A)
    A = size - 1
    D[1][A], P[1][A] = minimum(W, D, 1, A)
    return D, P

INF = 999
W = [
    [-1, -1, -1, -1, -1],
    [-1, 0, 2, 9, INF],
    [-1, 1, 0, 6, 4],
    [-1, INF, 7, 0, 8],
    [-1, 6, 3, INF, 0]
]
D, P = travel_dp(W)
print('Minlength =', D[1][2 ** (len(W) - 2) - 1])

 

2) 분기 한정법으로 해결

  • 하한값(lower bound)구하기: 경로 길이의 하한은 정점을 떠나는 간선 가중치 최솟값들의 합
  • 한계 값은 내부 노드에서 계산 / 여행 경로의 길이는 리프 노드에서 계산
from queue import PriorityQueue

class SSTNode:
    def __init__(self, level):
        self.level = level
        self.path = []
        self.bound = 0

    def contains(self, x):
        for i in range(len(self.path)):
            if self.path[i] == x:
                return True
        return False

def length(path, W):
    total = 0
    prev = 1
    for i in range(len(path)):
        if i != 0:
            prev = path[i - 1]
        total += W[prev][path[i]]
        prve = path[i]
    return total

def hasOutgoing(v, path):
    for i in range(len(path) - 1):
        if path[i] == v:
            return True
    return False

def hasIncoming(v, path):
    for i in range(1, len(path)):
        if path[i] == v:
            return True
    return False

def bound(v, W):
    n = len(W) - 1
    total = length(v.path, W)
    for i in range(1, n + 1):
        if hasOutgoing(i, v.path):
            continue
        min = INF
        for j in range(1, n + 1):
            if i == j: continue
            if hasIncoming(j, v.path): continue
            if j == 1 and i == v.path[len(v.path) - 1]: continue
            if min > W[i][j]: min = W[i][j]
        total += min
    return total


def travel_bb(W):
    global minlength, opttour
    n = len(W) - 1
    PQ = PriorityQueue()
    v = SSTNode(0)
    v.path = [1]
    v.bound = bound(v, W)
    minlength = INF
    PQ.put((v.bound, v))
    while not PQ.empty():
        v = PQ.get()[1]
        if v.bound < minlength:
            for i in range(2, n + 1):
                if v.contains(i):
                    continue
                u = SSTNode(v.level + 1)
                u.path = v.path[:]
                u.path.append(i)
                if u.level == n - 2:
                    for k in range(2, n + 1):
                        if not u.contains(k):
                            u.path.append(k)
                    u.path.append(1)
                    if length(u.path, W) < minlength:
                        minlength = length(u.path, W)
                        opttour = u.path[:]
                else:
                    u.bound = bound(u, W)
                    if u.bound < minlength:
                        PQ.put((u.bound, u))

INF = 999
W = [
    [-1, -1, -1, -1, -1, -1],
    [-1, 0, 14, 4, 10, 20],
    [-1, 14, 0, 7, 8, 7],
    [-1, 4, 5, 0, 7, 16],
    [-1, 11, 7, 9, 0, 2],
    [-1, 18, 7, 17, 4, 0]
]
minlength = INF
opttour = None
travel_bb(W)

print('Minlength =', minlength)
print('Optimal tour =', opttour)

 

 

 

※ 강의 링크: https://inf.run/v8Rn

6-0. 배낭 문제(Knapsack Problem)

  • Nkg까지 담을 수 있는 배낭으로 메고 곡식 창고에 침투한 도둑
    창고 입구에는 보관중인 곡식의 전체 수량과 1kg당 가격이 적혀 있을 때, 이익이 최대가 되도록 배낭 채우기
    - n개의 아이템 집합 S, 아이템의 무게 w, 아이템의 가치 p, 배낭의 용량 W
  • 0-1 배낭 문제
    - 아이템의 분할이 불가능 --- 탐욕 알고리즘으로 해결 불가능
    - 배낭의 용량을 넘지 않으면서 이익이 최대가 되도록 배낭 채우기

6-1. 탐욕 알고리즘으로 해결

  • 가장 값어치가 높은 아이템을 우선으로 채움
  • 1. 1kg당 가격을 기준으로 내림차순 정렬
    2. 배낭의 무게를 초과하지 않을 때까지 비싼 순으로 채우기
def knapsack_greedy(W, w):
    N = len(w) - 1
    K = [0] * (N + 1)
    weight = 0
    for i in range(1, N + 1):
        weight += w[i]
        K[i] = w[i]
        if weight > W:
            K[i] -= (weight - W)
            break
    return K

w = [0, 2, 5, 8, 7, 40, 13, 24] # 각 아이템의 무게
p = [0, 15, 12, 8, 8, 7, 5, 2] # 각 아이템의 가치
W = 30 # 전체 배낭 무게
K = knapsack_greedy(W, w) # 각 아이템을 얼마나 담았는지
print(K)

total = 0
for i in range(1, len(K)):
    total += p[i] * K[i]
print('Total price is', total)

6-2. 동적 계획법으로 해결

  • 1. 재귀 관계식
    P[i][w]: 총 무게가 w를 초과할 수 없다는 제약조건 하에서 처음 i개 아이템에서만 선택할 때 얻을 수 있는 최대 이익
         if wi <= w, P[i][w] = max(P[i-1][w], pi+P[i-1][w-wi])
         if i < j, P[i][w] = P[i-1][w]
    2. 상향식 방법
         P[n][W]를 계산하려면 P[n-1][W], P[n-1][W-wn]만 필요
         P[i][w]를 계산하려면 P[i-1][w], P[i-1][w-wi]만 필요
def knapsack_dp(i, W, w, p):
    if i <= 0 or W <= 0:
        return 0
    if w[i] > W:
        return knapsack_dp(i - 1, W, w, p)
    else:
        left = knapsack_dp(i - 1, W, w, p)
        right = knapsack_dp(i - 1, W - w[i], w, p)
        return max(left, p[i] + right)

W = 30
w = [0, 5, 10, 20]
p = [0, 50, 60, 140]
profit = knapsack_dp(len(w) - 1, W, w, p)
print(profit)

6-3. 백트래킹으로 해결

  • 부분집합의 합 문제와 유사
def promising(i, profit, weight):
    if weight > W:
        return False
    else:
        j = i + 1
        bound = profit
        totalweight = weight
        while j <= N and totalweight + w[j] <= W:
            totalweight += w[j]
            bound += p[j]
            j += 1
        k = j
        if k <= N:
            bound += (W - totalweight) * p[k] / w[k]
        return bound > maxprofit

def knapsack_bt(i, profit, weight):
    global maxprofit, numbest, bestset
    if weight <= W and profit > maxprofit:
        maxprofit = profit
        numbest = i
        bestset = include[:]
    if promising(i, profit, weight):
        include[i + 1] = True
        knapsack_bt(i + 1, profit + p[i + 1], weight + w[i + 1])
        include[i + 1] = False
        knapsack_bt(i + 1, profit, weight)

N = 4
W = 16
w = [0, 2, 5, 10, 5]
p = [0, 40, 30, 50, 10]
maxprofit = 0
numbest = 0
bestset = []
include = [False] * (N + 1)
knapsack_bt(0, 0, 0)
print(bestset[1:len(bestset)])
total = 0
for i in range(1, N + 1):
    total += bestset[i] * p[i]
print(total)

6-4. 분기 한정법으로 해결

  • 분기 한정법: 백트래킹과 동일하게 상태공간트리로 문제를 해결하되, BFS를 기반으로 함
  • 유망 함수(promising) 외에 추가적인 용도로 한계값(bound)을 사용 --- 지금까지 찾은 최적의 해답보다 그 한계값이 더 좋다면 그 노드는 유망함
  • 주어진 어떤 노드의 모든 자식 노드를 방문한 후, 유망하면서 확장하지 않은 노드들을 모두 살펴보고 그 중에서 한계값이 가장 좋은 노드들을 우선적으로 확장
from queue import PriorityQueue

class SSTNode:
    def __init__(self, level, profit, weight):
        self.level = level
        self.profit = profit
        self.weight = weight
        self.bound = 0

def bound(u, p, w):
    N = len(p) - 1
    if u.weight >= W:
        return 0
    else:
        result = u.profit
        j = u.level + 1
        totalweight = u.weight
        while j <= N and totalweight + w[j] <= W:
            totalweight += w[j]
            result += p[j]
            j += 1
        k = j
        if k <= N:
            result += (W - totalweight) * p[k] / w[k]
        return result

def knapsack_bb(p, w, W):
    PQ = PriorityQueue()
    v = SSTNode(0, 0, 0)
    maxprofit = 0
    v.bound = bound(v, p, w)
    PQ.put((-v.bound, v))
    while not PQ.empty():
        v = PQ.get()[1]
        if v.bound > maxprofit:
            # left node
            level = v.level + 1
            profit = v.profit + p[level]
            weight = v.weight + w[level]
            u = SSTNode(level, profit, weight)
            if u.weight <= W and u.profit > maxprofit:
                maxprofit = u.profit
            u.bound = bound(u, p, w)
            if u.bound > maxprofit:
                PQ.put((-u.bound, u))
            # right node
            u = SSTNode(level, v.profit, v.weight)
            u.bound = bound(u, p, w)
            if u.bound > maxprofit:
                PQ.put((-u.bound, u))
    return maxprofit


profit = [0, 40, 30, 50, 10]
weight = [0, 2, 5, 10, 5]
W = 16

maxprofit = knapsack_bb(profit, weight, W)
print('Maxprofit:', maxprofit)

 

 

 

※ 강의 링크: https://inf.run/v8Rn

5-0. 백트래킹(Backtracking)

  • 임의의 집합에서 주어진 기준대로 원소의 순서를 선택하는 문제를 푸는 데 적합
  • 1. 상태공간트리를 깊이우선탐색(Depth First Search)으로 탐색
         - 상태공간트리(State Space Tree): 해답을 탐색하기 위한 탐색 공간을 트리 형태의 구조로 암묵적으로 해석한 것
    2. 방문 중인 노드에서 더 하위 노드로 가서 해답이 없을 경우(유망하지 않을 경우), 하위 트리를 방문하지 않고 부모 노드로 되돌아감 + 하위 트리는 가지치기(pruning)

5-1. N-Queens 문제

  • N * N 체스보드에 N개의 퀸을 배치하는 문제
  • 어떤 퀸도 다른 퀸에 의해서 잡아먹히지 않도록 배치해야 함. 즉, 같은 행 / 열 / 대각선에는 다른 퀸을 놓을 수 없음
  • 임의의 집합: 체스보드에 있는 N^2개의 가능한 위치
    기준: 새로 놓을 퀸이 다른 퀸을 위협하면 안 됨
    원소의 순서: 퀸을 놓을 수 있는 N개의 위치
def promising(i, col):
    k = 1
    flag = True
    while k < i and flag:
        if (col[i] == col[k]) or (abs(col[i] - col[k]) == (i - k)):
            flag = False
        k += 1
    return flag

def n_queens(i, col):
    N = len(col) - 1
    if promising(i, col):
        if i == N:
            print(col[1: N + 1])
        else:
            for j in range(1, N + 1):
                col[i + 1] = j
                n_queens(i + 1, col)

N = 4
col = [0] * (N + 1)
n_queens(0, col)

5-2. 부분집합의 합 구하기

  • 원소가 N개인 양의 정수 집합 w와 양의 정수 W가 주어졌을 때, 합이 W가 되는 w의 부분집합을 모두 구하기
  • 원소를 오름차순으로 정렬하면 각 노드가 유망한지 여부를 알 수 있음
    - i번째 레벨에서 wi+1은 남아 있는 정수 원소 중 가장 작은 값
    - 레벨 i까지의 모든 정수 원소의 합을 weight라고 할 때, weight에 wi+1를 더해서 W보다 클 경우 가지치기
    - weight에다 남은 정수 원소의 합 total을 더해서 W보다 작을 경우에도 가지치기
def promising(i, weight, total):
    if (weight + total >= W) and (weight == W or weight + w[i+1] <= W):
        return True
    else:
        return False

def sum_of_subsets(i, weight, total):
    N = len(w) - 1
    if promising(i, weight, total):
        if weight == W:
            print(include[1:N + 1])
        else:
            include[i + 1] = True
            sum_of_subsets(i + 1, weight + w[i + 1], total - w[i + 1])
            include[i + 1] = False
            sum_of_subsets(i + 1, weight, total - w[i + 1])

W = 21
w = [0, 5, 6, 10, 11, 16]
total = sum(w)
include = [False] * (len(w))
sum_of_subsets(0, 0, total)

 

 

 

※ 강의 링크: https://inf.run/v8Rn

4-3. 마감시간 있는 스케줄 짜기

  • 마감시간과 함께 여러 개의 작업이 주어지고, 각 작업을 마치면 받을 수 있는 보상이 정해져 있을 때, 보상을 최대화하는 스케줄 작성하기
  • 각 작업을 끝내는데 1의 단위 시간이 걸리고, 보상은 마감시간 이전이나 마감시간에 끝내는 경우에만 주어짐
  • 1. 보상에 따라서 내림차순으로 작업을 정렬
    2. 각 작업을 순서대로 하나씩 가능한 스케줄에 포함시킴
def feasible(K, deadline):
    for i in range(1, len(K) + 1):
        if i > deadline[K[i - 1]]:
            return False
    return True

def insert(J, i, deadline):
    K = J[:]
    for j in range(len(J), 0, -1):
        if deadline[i] >= deadline[K[j - 1]]:
            j += 1
            break
    K.insert(j - 1, i)
    return K

def schedule(deadline):
    N = len(deadline) - 1
    J = [1]
    for i in range(2, N + 1):
        K = insert(J, i, deadline)
        if feasible(K, deadline):
            J = K[:]
    return J

deadline = [0, 3, 1, 1, 3, 1, 3, 2]
profit = [0, 40, 35, 30, 25, 20, 15, 10]
J = schedule(deadline)

maxprofit = 0
for job in J:
    maxprofit += profit[job]
print('Max Profit is', maxprofit)

4-4. 허프만 코드

  • 최적 이진코드 문제: 주어진 파일에 있는 문자들을 이진코드로 표현할 때, 필요한 비트의 개수가 최소가 되는 이진코드 구하기
    - 이진코드: 데이터 파일을 이진코드로 인코딩하여 저장(길이가 고정될 수도 있고 변할 수도 있음)
  • 전치코드
    - 길이가 변하는 이진코드의 특수한 형태로, 한 문자의 코드워드가 다른 문자의 코드워의 앞부분이 될 수 없음
    - 모든 전치코드는 리프노드가 코드문자인 이진트리로 표현 가능
    - 파일을 파싱(디코딩)할 때 뒷부분을 미리 볼 필요가 없음
  • Huffman 알고리즘: 허프만 코드에 해당하는 이진트리를 구축하는 탐욕 알고리즘
    - 데이터 파일을 인코딩하는데 필요한 비트 수 계산
    - 주어진 이진트리가 T 일때,
       { v1, v2, ..., vn}: 데이터 파일 내 문자들의 집합
       frequency(vi): 문자 vi가 파일 내에서 나타나는 빈도수
       depth(vi): 이진트리 T에서 문자 vi 노드의 깊이
from queue import PriorityQueue

class HuffNode:
    def __init__(self, symbol, freq):
        self.symbol = symbol
        self.freq = freq
        self.left = None
        self.right = None

    def preorder(self):
        print(self.freq, end=' ')
        if self.left:
            self.left.preorder()
        if self.right:
            self.right.preorder()

    def inorder(self):
        if self.left:
            self.left.inorder()
        print(self.freq, end=' ')
        if self.right:
            self.right.inorder()

def huffman(N, PQ):
    for _ in range(N - 1):
        p = PQ.get()[1]
        q = PQ.get()[1]
        r = HuffNode(' ', p.freq + q.freq)
        r.left = p
        r.right = q
        PQ.put((r.freq, r))
    return PQ.get()[1]

PQ = PriorityQueue()
codes = list('becadf')
freqs = [5, 10, 12, 16, 17, 25]
for i in range(len(codes)):
    node = HuffNode(codes[i], freqs[i])
    PQ.put((node.freq, node))

root = huffman(len(codes), PQ)

print('Preorder:', end=' ')
root.preorder()
print('\nInorder:', end=' ')
root.inorder()

 

 

 

※ 강의 링크: https://inf.run/v8Rn

4-0. 탐욕 알고리즘(Greedy)

  • 최종 해답을 찾기 위해서 각 단계마다 가장 좋아보이는 하나의 답을 고름
  • 최적화 문제에서 선택할 당시에는 최적의 답을 고르지만, 최종 해답이 반드시 최적임을 보장하지는 않음
  • 설계 전략
    1. 공집합에서 시작
    2. 집합에 추가할 다음 최적의 원소를 고름
    3. 새로운 집합이 해답으로 적절한지 검사
  • 장점: 상대적으로 설계하기 매우 쉬움
    단점: 최적화 문제에서 반드시 정확성을 증명해야 함

4-1. 최소비용 신장트리(Minimum Spanning Tree)

  • 신장트리: 그래프의 모든 정점을 연결하는 트리, 간선은 n-1개(n은 노드의 개수)
  • 모든 신장트리 T 중 가중치의 합이 최소가 되는 신장트리 구하기
  • 그래프는 모든 정점이 연결된 가중치가 있는 무방향 그래프
  • 1. 간선 집합 E의 부분 집합 F를 공집합으로 둠
    2. E에서 최적의 간선 하나를 추출해서 F에 포함시킴
    3. F의 원소 개수가 n-1이 될 때까지 반복
  • Prim 알고리즘
    1. F = {}, Y = {v1}   (Y는 정점의 집합 V의 부분 집합)
    2. V - Y 집합에서 Y 집합으로부터 가장 가까운 정점 vnear를 선택, Y 집합에 vnear 추가 및 F 집합에 (nearest(vnear), vnear) 추가
    3. Y = V가 되면 종료
def prim(W):
    N = len(W) - 1
    F = []
    nearest = [-1] * (N + 1)
    distance = [-1] * (N + 1)
    for i in range(2, N + 1):
        nearest[i] = 1
        distance[i] = W[1][i]
    for _ in range(N - 1):
        minV = INF
        for i in range(2, N + 1):
            if 0 <= distance[i] < minV:
                minV = distance[i]
                vnear = i
        edge = (nearest[vnear], vnear, W[nearest[vnear]][vnear])
        F.append(edge)
        distance[vnear] = -1
        for i in range(2, N + 1):
            if distance[i] > W[i][vnear]:
                distance[i] = W[i][vnear]
                nearest[i] = vnear
    return F

def cost(F):
    total = 0
    for edge in F:
        total += edge[2]
    return total

INF = 999
W = [
    [-1, -1, -1, -1, -1],
    [-1, 0, 1, 3, INF, INF],
    [-1, 1, 0, 3, 6, INF],
    [-1, 3, 3, 0, 4, 2],
    [-1, INF, 6, 4, 0, 5],
    [-1, INF, INF, 2, 5, 0]
]
F = prim(W)
print('Minimum Cost is', cost(F))

 

  • Kruskal 알고리즘
    1. F = {}, V의 서로소 집합을 생성, E를 오름차순으로 정렬
    2. 정렬된 E 집합에서 간선 e = (i, j)를 선택, 두 정점 i, j가 속한 집합 p, q를 찾아서 p, q가 같으면 e를 버리고, 다르면 F에 e를 포함한 후 p, q를 합침(Union-Find) --- cycle 방지
    3. F의 원소의 개수가 n-1이 되면 종료
  • Union-Find 알고리즘
    - 서로소 집합 자료구조를 이용해서, 두 개의 원소가 같은 집합에 속하는지를 판단하는 알고리즘
class DisjointSet: # Union-Find
    def __init__(self, N):
        self.U = []
        for i in range(N):
            self.U.append(i)

    def equal(self, p, q):
        if p == q:
            return True
        else:
            return False

    def find(self, i):
        j = i
        while self.U[j] != j:
            j = self.U[j]
        return j

    def union(self, p, q):
        if p < q:
            self.U[q] = p
        else:
            self.U[p] = q

def kruskal(N, E):
    F = []
    dset = DisjointSet(N)
    while len(F) < N - 1:
        edge = E.pop(0)
        i, j = edge[0], edge[1]
        p = dset.find(i)
        q = dset.find(j)
        if not dset.equal(p, q):
            dset.union(p, q)
            F.append(edge)
    return F

def cost(F):
    total = 0
    for edge in F:
        total += edge[2]
    return total

INF = 999
N = 5
E = [
    [0, 1, 1],
    [2, 4, 2],
    [0, 2, 3],
    [1, 2, 3],
    [2, 3, 4],
    [3, 4, 5],
    [1, 3, 6]
]
F = kruskal(N, E)
print('Minimum cost is', cost(F))

4-2. 최단 경로

  • 모든 정점의 쌍에 대한 최단 경로 구하기: Floyd 알고리즘(동적 계획법)
  • 단일 정점에서 모든 다른 정점으로의 최단 경로 구하기: Dijkstra 알고리즘(탐욕법)
  • Dijkstra 알고리즘
    - 최소비용 신장트리 문제의 Prim 알고리즘과 유사
def dijkstra(W):
    N = len(W) - 1
    F = []
    touch = [-1] * (N + 1)
    length = [-1] * (N + 1)
    for i in range(2, N + 1):
        touch[i] = 1
        length[i] = W[1][i]
    for _ in range(N - 1):
        minV = INF
        for i in range(2, N + 1):
            if 0 <= length[i] < minV:
                minV = length[i]
                vnear = i
        edge = (touch[vnear], vnear, W[touch[vnear]][vnear])
        F.append(edge)
        for i in range(2, N + 1):
            if length[i] > length[vnear] + W[vnear][i]:
                length[i] = length[vnear] + W[vnear][i]
                touch[i] = vnear
        length[vnear] = -1
    return F

def length(F):
    total = 0
    for edge in F:
        total += edge[2]
    return total

INF = 999
W = [
    [-1, -1, -1, -1, -1],
    [-1, 0, 7, 4, 6, 1],
    [-1, INF, 0, INF, INF, INF],
    [-1, INF, 2, 0, 5, INF],
    [-1, INF, 3, INF, 0, INF],
    [-1, INF, INF, INF, 1, 0]
]
F = dijkstra(W)
print('Shortest Path Length is', length(F))

 

 

 

※ 강의 링크: https://inf.run/v8Rn

3-3. 연쇄 행렬 곱셈

  • 주어진 n개의 연쇄 행렬을 곱하는 최적의 순서 구하기(최적화 문제)
  • 행렬 곱셈은 결합 법칙이 성립하지만, 순서에 따라서 각 원소의 곱셈 횟수가 달라짐 --- 각 원소의 곱셈 횟수가 최소가 되는 곱셈 순서가 최적의 순서
  • 일반적으로 i * k 행렬과 k * j 행렬을 곱하면 i * j 행렬이 나오고 원소 곱셈의 횟수는 i * k * j
  • n개의 연쇄 행렬 곱셈을 A1 * A2 * ... * An이라고 할 때, Ak-1의 행의 개수와 Ak의 열의 개수가 같아야 함
  • dk를 행렬 Ak의 행의 개수라고 할 때, dk-1은 행렬 Ak의 열의 개수이자 Ak-1의 행의 개수(1 <= k <= n)
  • 1. 재귀 관계식
         M: 연쇄 행렬을 곱하는데 필요한 곱셈의 최소 회수 행렬
         M[i][j]: Ai에서 Aj까지 행렬을 곱하는데 필요한 곱셈의 최소 회수(1 <= i <= j <= n)
       => Ai ... Aj 행렬을 (Ai ... Ak)(Ak ... Aj)로 분할하는 재귀 관계식 찾기
    2. 상향식 방법
       초기화: M[i][i] = 0
       최종 목표: M[i][n]
  • For 1 <= i <= j <= n,
    if i = j, M[i][j] = 0
    if i < j, M[i][j] = minimum(M[i][k] + M[k+1][j] + di-1dkdj)   i <= k <= j - 1
def order(P, i, j):
    if i == j:
        print(f'A{i}', end='')
    else:
        k = P[i][j]
        print('(', end='')
        order(P, i, k)
        order(P, k+1, j)
        print(')', end='')

def minimum(M, d, i, j):
    minV = INF
    minK = 0
    for k in range(i, j):
        value = M[i][k] + M[k+1][j] + d[i-1] * d[k] * d[j]
        if minV > value:
            minV = value
            minK = k
    return minV, minK

def minmult(D):
    N = len(D) - 1
    M = [[-1] * (N + 1) for _ in range(N + 1)]
    P = [[-1] * (N + 1) for _ in range(N + 1)]
    for i in range(1, N + 1):
        M[i][i] = 0
    for diagonal in range(1, N):
        for i in range(1, N - diagonal + 1):
            j = i + diagonal
            M[i][j], P[i][j] = minimum(M, D, i, j)
    return M, P

INF = 999
D = [5, 2, 3, 4, 6, 7, 8]
M, P = minmult(D)

for line in M: # 최소 가중치 값
    print(*line)

for line in P: # 최소 가중치 값일 때의 경로
    print(*line)

print('minimum order: ', end='')
order(P, 1, len(P) - 1)

3-4. 최적 이진검색트리

    • 주어진 n개의 키로 최적 이진검색트리 구하기(최적화 문제)
    • 주어진 n개의 키: K1, K2, ... , Kn
      각 키의 검색 확률: pi(전체 검색 횟수 중 Ki를 검색하는 확률)
      각 키의 비교 횟수: ci(Ki를 검색하는데 필요한 키의 비교 횟수)
      각 키의 평균 검색 비용(시간): pi * ci
      전체 키의 평균 검색 비용(시간): Tavg = Σpici   i = 1 ~ n
    • 전체 키의 평균 검색 비용을 최소화하는 이진검색트리를 찾아야 함
    • 이진검색트리(Binary Search Tree)
      - 각 노드가 하나의 유일한 키를 가짐
      - 모든 노드가 가진 키의 값은 그 노드의 왼쪽 서브트리의 키 값보다 큰 동시에 오른쪽 서브트리의 키 값보다 작음
    • 1. 재귀 관계식
           A: 이진검색트리를 만드는 최소 검색비용의 행렬
           A[i][j]: Ki에서 Kj까지 이진검색트리를 만드는 최적 검색 비용
         => Ki ... Kj를 (Ki ... Kk-1)Kk(Kk+1 ... Kj)로 분할하는 재귀 관계식 찾기
      2. 상향식 방법
         초기화: A[i][i] = pi
         최종 목표: A[1][n]
    • 트리 k를 키 Kk가 루트 노드에 있는 최적 이진검색트리라고 할 때, 키 비교 횟수는 서브 트리의 비교 횟수+1(루트에서 비교)
    • m != k인 Km에 대해서 트리 k에 Km을 놓기 위해 비교 횟수를 추가할 때, Km의 평균 검색비용에 pm을 추가
      A[1][k-1] + A[k+1][n] + Σpm   m = 1 ~ n
    • 최적 트리: k개의 트리 중 평균 검색비용이 가장 작은 트리
      A[1][n] = minimum(A[1][k-1] + A[k+1][n] + Σpm   m = 1 ~ n)
class BSTNode:
    def __init__(self, key):
        self.key = key
        self.left = None
        self.right = None

def preorder(node):
    if not node:
        return
    else:
        print(node.key, end=' ')
        preorder(node.left)
        preorder(node.right)

def inorder(node):
    if not node:
        return
    else:
        inorder(node.left)
        print(node.key, end=' ')
        inorder(node.right)

def tree(R, i, j):
    k = R[i][j]
    if k == 0:
        return
    else:
        node = BSTNode(keys[k])
        node.left = tree(R, i, k - 1)
        node.right = tree(R, k + 1, j)
        return node

def minimum(A, P, i, j):
    minV = INF
    minK = 0
    for k in range(i, j + 1):
        value = A[i][k - 1] + A[k + 1][j]
        for m in range(i, j + 1):
            value += P[m]
        if minV > value:
            minV = value
            minK = k
    return minV, minK

def optbst(P):
    N = len(P) - 1
    A = [[-1] * (N + 1) for _ in range(N + 2)]
    R = [[-1] * (N + 1) for _ in range(N + 2)]
    for i in range(1, N + 1):
        A[i][i - 1] = 0
        A[i][i] = P[i]
        R[i][i - 1] = 0
        R[i][i] = i
    A[N + 1][N] = 0
    R[N + 1][N] = 0
    for diagonal in range(1, N):
        for i in range(1, N - diagonal + 1):
            j = i + diagonal
            A[i][j], R[i][j] = minimum(A, P, i, j)
    return A, R

INF = 999
keys = [0, 10, 20, 30, 40, 50]
P = [0, 35, 12, 22, 8, 15]
A, R = optbst(P)

for line in A: # 최소 비용
    print(*line)

for line in R: # 최소 비용일 때 루트 노드 인덱스
    print(*line)

root = tree(R, 1, len(P) - 1)
print('preorder: ', end='')
preorder(root)
print('\ninorder: ', end='')
inorder(root)

 

 

 

※ 강의 링크: https://inf.run/v8Rn

3-0. 동적 계획법(Dynamic Programming)

  • 문제를 더 작은 문제로 분할하되, 상향식으로 문제를 해결하는 방법
  • 메모이제이션(Memoization): 가장 작은 입력 사례의 해답을 테이블에 저장하고 필요할 때 꺼내 쓰는 방법
  • 1. 문제를 해결할 수 있는 재귀 관계식을 구함
    2. 가장 작은 입력사례로부터 상향식 방법으로 문제를 해결

3-1. 이항 계수

  • 분할 정복법으로 구현 --- 중복으로 호출하게 됨
def bin(N, K):
    if K == 0 or N == K:
        return 1
    else:
        return bin(N - 1, K - 1) + bin(N - 1, K)

for N in range(10):
    for K in range(N + 1):
        print(bin(N, K), end=' ')
    print()
  • 동적 계획법으로 구현
    1. 재귀 관계식
         B[i][j] = B[i -1][j - 1] + B[i - 1][j]     (0 < j < i)
                                        1                (j = 0 or j = i)
    2. 상향식 방법 --- 파스칼의 삼각형이 가진 특성 이용
def bin(N, K):
    B = [[0] * (K + 1) for _ in range(N + 1)]
    for i in range(N + 1):
        for j in range(min(i, K) + 1):
            if j == 0 or j == i:
                B[i][j] = 1
            else:
                B[i][j] = B[i - 1][j - 1] + B[i - 1][j]
    return B[N][K]

for N in range(10):
    for K in range(N + 1):
        print(bin(N, K), end=' ')
    print()
  • 동적 계획법으로 구현 --- 값이 대칭되는 성질 및 1차원 리스트 사용
def bin(N, K):
    if K > N // 2:
        K = N - K
    B = [0] * (K + 1)
    B[0] = 1
    for i in range(1, N + 1):
        j = min(i, K)
        while j > 0:
            B[j] = B[j] + B[j - 1]
            j -= 1
    return B[K]

for N in range(10):
    for K in range(N + 1):
        print(bin(N, K), end=' ')
    print()

3-2. 최단 경로(플로이드 알고리즘)

  • 주어진 그래프에서 모든 정점의 쌍에 대한 최단 경로 구하기
  • 그래프는 방향성, 가중치 그래프이고 최단 경로는 단순 경로로 같은 정점을 두 번 거치지 않음
  • 최적화 문제: 하나 이상의 해답 후보가 존재하고, 해답 후보 중 가장 최적의 값을 가진 해답을 찾는 문제
  • 1. 재귀 관계식
         D: 각 정점의 쌍이 가지는 최단 경로의 길이를 나타내는 행렬
         D[i][j]: vi에서 vj로 가는 최단 경로의 길이
         Dk: k개의 중간 정점을 지나는 최단 경로 길이 행렬
         Dk[i][j]: vi에서 vj로 k개의 중간 정점을 지나는 최단 경로 길이
       => 인접 행렬 W에서 최단 경로의 행렬 D와 재귀 관계식 구하기
    2. 상향식 방법
       초기화: D0 = W --- 다른 어떤 정점도 지나지 않는 최단 경로의 길이
       최종 목표: Dn = D --- 다른 모든 정점을 지날 수 있는 최단 경로의 길이
  • D0 = W, Dk는 Dk-1로부터 구함(1 <= k <= n)
  • Dk[i][j]는 하나의 정점을 더 지나도 새로운 최단 경로가 없는 경우 Dk-1[i][j]과 같음
    하나의 정점(vk)를 더 지나서 새로운 최단 경로가 있으면 Dk[i][j] = Dk-1[i][k] + Dk-1[k][j]
def floyd(W):
    D = W
    N = len(W)
    P = [[-1] * N for _ in range(N)]
    for k in range(N):
        for i in range(N):
            for j in range(N):
                if D[i][j] > D[i][k] + D[k][j]:
                    D[i][j] = D[i][k] + D[k][j]
                    P[i][j] = k
    return D, P

def path(P, i, j):
    if P[i][j] != -1:
        path(P, i, P[i][j])
        print(f'v{P[i][j]} -> ', end='')
        path(P, P[i][j], j)

INF = 999
W = [
    [0, 1, INF, 1, 5],
    [9, 0, 3, 2, INF],
    [INF, INF, 0, 4, INF],
    [INF, INF, 2, 0, 3],
    [3, INF, INF, INF, 0]
]

D, P = floyd(W)
for line in D: # 최소 가중치 값
    print(*line)

for line in P: # 최단 경로
    print(*line)

start = 4
end = 2
print(f'shortest path from v{start} to v{end}: v{start} -> ', end='')
path(P, start, end)
print(f'v{end}')

 

 

 

※ 강의 링크: https://inf.run/v8Rn

2-6. 트로미노 퍼즐

  • 트로미노(tromino): 정사각형이 3개 붙어있는 것
  • 가로와 세로로 m개의 정사각형이 연결되어 있는 바둑판이 있고, 이 바둑판의 1칸은 X 표시가 되어 있음(단, m은 2의 거듭제곱)
  • 다음 조건을 만족하도록 트로미노를 바둑판 전체에 채우려고 함
    - X 표시가 되어 있는 칸은 채울 수 없음
    - 트로미노는 겹쳐 놓거나 바둑판 밖으로 빠져나가게 둘 수 없음
  • 분할: 4개의 사분면으로 분할 후 X가 없는 사분면의 모서리 채우기
    정복: 채워진 4개의 사분면을 재귀 호출
import random

# 입력: M, X의 Row, X의 Col
M = 8
xrow = random.randint(0, M - 1)
xcol = random.randint(0, M - 1)
board = [[0] * M for _ in range(M)]
board[xrow][xcol] = -1
tromino_cnt = 0

def fillCenterExcept(board, mrow, mcol, N): # N사분면 제외하고 나머지 사분면의 모서리에 X를 추가
    global tromino_cnt

    tromino_cnt += 1
    if N != 1:
        board[mrow - 1][mcol] = tromino_cnt
    if N != 2:
        board[mrow - 1][mcol - 1] = tromino_cnt
    if N != 3:
        board[mrow][mcol - 1] = tromino_cnt
    if N != 4:
        board[mrow][mcol] = tromino_cnt

def tromino(board, srow, scol, size, xrow, xcol):
    if size == 1:
        return
    else:
        mrow = srow + (size // 2) # middle of row
        mcol = scol + (size // 2)
        xrow1, xcol1 = mrow - 1, mcol     # 1사분면에 추가할 X의 위치
        xrow2, xcol2 = mrow - 1, mcol - 1 # 2사분면에 추가할 X의 위치
        xrow3, xcol3 = mrow, mcol - 1     # 3사분면에 추가할 X의 위치
        xrow4, xcol4 = mrow, mcol         # 4사분면에 추가할 X의 위치

        # 이미 X가 있는 사분면은 제외
        if xrow < mrow and xcol >= mcol:
            fillCenterExcept(board, mrow, mcol, 1)
            xrow1, xcol1 = xrow, xcol
        elif xrow < mrow and xcol < mcol:
            fillCenterExcept(board, mrow, mcol, 2)
            xrow2, xcol2 = xrow, xcol
        elif xrow >= mrow and xcol < mcol:
            fillCenterExcept(board, mrow, mcol, 3)
            xrow3, xcol3 = xrow, xcol
        elif xrow >= mrow and xcol >= mcol:
            fillCenterExcept(board, mrow, mcol, 4)
            xrow4, xcol4 = xrow, xcol

        tromino(board, srow, mcol, size // 2, xrow1, xcol1)
        tromino(board, srow, scol, size // 2, xrow2, xcol2)
        tromino(board, mrow, scol, size // 2, xrow3, xcol3)
        tromino(board, mrow, mcol, size // 2, xrow4, xcol4)

# 출력: 각 트로미노에 번호를 부여하여 빈 칸 채우기
tromino(board, 0, 0, M, xrow, xcol)
for i in range(M):
    for j in range(M):
        if board[i][j] < 0:
            print('%3s'%'X', end='')
        else:
            print('%3d'%board[i][j], end='')
    print()

 

 

 

※ 강의 링크: https://inf.run/v8Rn

2-4. Strassen의 행렬 곱셈

  • 행렬 곱셈의 정의에 충실하게 구현할 경우, 시간 복잡도 Θ(𝑛 ^ 3 )
A = [
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 1, 2, 3],
    [4, 5, 6, 7],
]

B = [
    [8, 9, 1, 2],
    [3, 4, 5, 6],
    [7, 8, 9, 1],
    [2, 3, 4, 5],
]

def matrixmult(A, B):
    N = len(A)
    C = [[0] * N for _ in range(N)]
    for i in range(N):
        for j in range(N):
            for k in range(N):
                C[i][j] += A[i][k] * B[k][j]
    return C

result = matrixmult(A, B)

for line in result:
    print(*line)
  • 쉬트라센의 방법을 사용할 경우, 시간 복잡도 Θ(𝑛 ^ 2.81)

A = [
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 1, 2, 3],
    [4, 5, 6, 7],
]

B = [
    [8, 9, 1, 2],
    [3, 4, 5, 6],
    [7, 8, 9, 1],
    [2, 3, 4, 5],
]

def matrixmult (A, B):
    N = len(A)
    C = [[0] * N for _ in range(N)]
    for i in range(N):
        for j in range(N):
            for k in range(N):
                C[i][j] += A[i][k] * B[k][j]
    return C

def divide(A):
    N = len(A)
    M = N // 2
    A11 = [[0] * M for _ in range(M)]
    A12 = [[0] * M for _ in range(M)]
    A21 = [[0] * M for _ in range(M)]
    A22 = [[0] * M for _ in range(M)]
    for i in range(M):
        for j in range(M):
            A11[i][j] = A[i][j]
            A12[i][j] = A[i][M + j]
            A21[i][j] = A[M + i][j]
            A22[i][j] = A[M + i][M + j]
    return A11, A12, A21, A22

def madd(A, B):
    N = len(A)
    C = [[0] * N for _ in range(N)]
    for i in range(N):
        for j in range(N):
            C[i][j] = A[i][j] + B[i][j]
    return C

def msub(A, B):
    N = len(A)
    C = [[0] * N for _ in range(N)]
    for i in range(N):
        for j in range(N):
            C[i][j] = A[i][j] - B[i][j]
    return C

def conquer(M1, M2, M3, M4, M5, M6, M7):
    C11 = madd(msub(madd(M1, M4), M5), M7)
    C12 = madd(M3, M5)
    C21 = madd(M2, M4)
    C22 = madd(msub(madd(M1, M3), M2), M6)
    M = len(C11)
    N = M * 2
    C = [[0] * N for _ in range(N)]
    for i in range(M):
        for j in range(M):
            C[i][j] = C11[i][j]
            C[i][M + j] = C12[i][j]
            C[M + i][j] = C21[i][j]
            C[M + i][M + j] = C22[i][j]
    return C

def strassen(A, B):
    N = len(A)
    if N <= threshold:
        return matrixmult(A, B)
    A11, A12, A21, A22 = divide(A)
    B11, B12, B21, B22 = divide(B)
    M1 = strassen(madd(A11, A22), madd(B11, B22))
    M2 = strassen(madd(A21, A22), B11)
    M3 = strassen(A11, msub(B12, B22))
    M4 = strassen(A22, msub(B21, B11))
    M5 = strassen(madd(A11, A12), B22)
    M6 = strassen(msub(A21, A11), madd(B11, B12))
    M7 = strassen(msub(A12, A22), madd(B21, B22))
    return conquer(M1, M2, M3, M4, M5, M6, M7)

threshold = 2
result = strassen(A, B)

for line in result:
    print(*line)

2-5. 큰 정수의 산술

  • 특정 컴퓨터/언어가 표현할 수 없는 큰 정수의 산술 연산
  • 10진수를 소프트웨어적으로 표현하는 방법: 리스트를 이용하여 각 자리수를 하나의 원소로 저장
    ex) 567,832 > lst = [2, 3, 8, 7, 6, 5]

1) 덧셈: n개의 자리수를 각각 더하면서 올림수를 고려

def largeadd(A, B):
    N = len(A) if len(A) > len(B) else len(B)
    result = []
    carry = 0
    for k in range(N):
        i = A[k] if k < len(A) else 0
        j = B[k] if k < len(B) else 0
        value = i + j + carry
        carry = value // 10
        result.append(value % 10)
    if carry > 0:
        result.append(carry)
    return result

A = [3, 2, 1]
B = [5, 4]

result = largeadd(A, B)
print(result[::-1])

2) 곱셈 --- 분할정복으로 구현

  • n개의 자리수로 된 숫자를 n/2개의 자리수로 분할(n이 홀수일 경우 분할된 값의 자리수가 다를 수 있음
    ex) 567,832 = 567 * 10 ^ 3 + 832
          9,423,723 = 9,423 * 10 ^ 3 + 723 --- 10의 지수 m은 n/2
  • 두 개의 정수를 분할하여 곱함
      A = (x * 10 ^ m + y)(w * 10 ^ m + z) = xw * 10 ^ 2m + (zx + yw) * 10 ^ m + yz
  • 재귀호출을 4번해서 시간 복잡도가 전통 방식과 같음(n ^ 2)
def largeadd(A, B):
    N = len(A) if len(A) > len(B) else len(B)
    result = []
    carry = 0
    for k in range(N):
        i = A[k] if k < len(A) else 0
        j = B[k] if k < len(B) else 0
        value = i + j + carry
        carry = value // 10
        result.append(value % 10)
    if carry > 0:
        result.append(carry)
    return result

def largemult(A, B): # 전통적인 방법으로 곱셈
    i = A[0] if 0 < len(A) else 0
    j = B[0] if 0 < len(B) else 0
    value = i * j
    carry = value // 10
    result = []
    result.append(value % 10)
    if carry > 0:
        result.append(carry)
    return result

def exp(A, M): # A * (10 ** M)
    if A == 0:
        return [0]
    else:
        return M * [0] + A

def div(A, M): # A // (10 ** M)
    if len(A) < M:
        A.append(0)
    return A[M:]

def rem(A, M): # A % (10 ** M)
    if len(A) < M:
        A.append(0)
    return A[:M]

def prod(A, B):
    N = len(A) if len(A) > len(B) else len(B)
    if len(A) == 0 or len(B) == 0:
        return [0]
    elif N <= threshold:
        return largemult(A, B)
    else:
        M = N // 2
        X = div(A, M)
        Y = rem(A, M)
        W = div(B, M)
        Z = rem(B, M)
        p1 = prod(X, W)
        p2 = largeadd(prod(X, Z), prod(W, Y))
        p3 = prod(Y, Z)
        return largeadd(largeadd(exp(p1, 2*M), exp(p2, M)), p3)

# 임계값, 특정 자리수까지
threshold = 1

A = [4, 3, 2, 1]
B = [9, 8, 7, 6, 5]

print(exp(A, 3))
print(div(A, 3))
print(rem(A, 3))
print(prod(A, B)[::-1])

3) 곱셈 --- 효율성 개선

  • 재귀호출의 횟수를 3번으로 줄임
    A = xw * 10 ^ 2m + (zx + yw) * 10 ^ m + yz
    R = (x + y)(w + z) = xw + (zx + yw) + yz
    (zx + yw) = R - xw - yz
def largeadd(A, B):
    N = len(A) if len(A) > len(B) else len(B)
    result = []
    carry = 0
    for k in range(N):
        i = A[k] if k < len(A) else 0
        j = B[k] if k < len(B) else 0
        value = i + j + carry
        carry = value // 10
        result.append(value % 10)
    if carry > 0:
        result.append(carry)
    return result

def largesub(A, B):
    N = len(A) if len(A) > len(B) else len(B)
    result = []
    borrow = 0
    for k in range(N):
        i = A[k] if k < len(A) else 0
        j = B[k] if k < len(B) else 0
        value = i - j + borrow
        if value < 0:
            value += 10
            borrow = -1
        else:
            borrow = 0
        result.append(value % 10)
    if borrow < 0:
        print('음의 정수는 처리할 수 없음')
    return result

def largemult(A, B): # 전통적인 방법으로 곱셈
    i = A[0] if 0 < len(A) else 0
    j = B[0] if 0 < len(B) else 0
    value = i * j
    carry = value // 10
    result = []
    result.append(value % 10)
    if carry > 0:
        result.append(carry)
    return result

def exp(A, M): # A * (10 ** M)
    if A == 0:
        return [0]
    else:
        return M * [0] + A

def div(A, M): # A // (10 ** M)
    if len(A) < M:
        A.append(0)
    return A[M:]

def rem(A, M): # A % (10 ** M)
    if len(A) < M:
        A.append(0)
    return A[:M]

def prod(A, B):
    N = len(A) if len(A) > len(B) else len(B)
    if len(A) == 0 or len(B) == 0:
        return [0]
    elif N <= threshold:
        return largemult(A, B)
    else:
        M = N // 2
        X = div(A, M)
        Y = rem(A, M)
        W = div(B, M)
        Z = rem(B, M)
        R = prod(largeadd(X, Y), largeadd(W, Z))
        p1 = prod(X, W)
        p3 = prod(Y, Z)
        p2 = largesub(R, largeadd(p1, p3))
        return largeadd(largeadd(exp(p1, 2*M), exp(p2, M)), p3)

# 임계값, 특정 자리수까지
threshold = 1

A = [4, 3, 2, 1]
B = [9, 8, 7, 6, 5]

print(prod(A, B)[::-1])

 

 

 

※ 강의 링크: https://inf.run/v8Rn

2-0. 분할 정복

  • 분할-정복 두 단계로 문제를 해결하는 알고리즘 설계 기법
    - 분할(Divide): 문제의 입력사례를 둘 이상의 작은 입력사례로 분할
    - 정복(Conquer): 작은 입력사례들을 각각 정복, 충분히 작지 않을 경우 재귀 호출
    - 통합: 필요할 경우, 작은 입력사례의 해답을 통합하여 원래 입력사례의 해답을 도출
  • 하향식(Top-Down) 방식

2-1. 이분 검색(Binary Search)

  • 순차 탐색: 정렬되지 않은 리스트에서 주어진 키가 존재하는가?
  • 이분 검색: 정렬된 리스트에서 주어진 키가 존재하는가?
  • 분할: 정가운데 원소를 기준으로 리스트를 두 개로 분할
    정복: 키 값이 정가운데 원소보다 크면 오른쪽, 작으면 왼쪽을 재귀 호출
    획득: 선택한 리스트에서 얻은 답을 리턴
  • 재귀 함수로 구현
lst = [1, 2, 5, 8, 13]
target = 13

def bs(start, end):
	if start > end:
		return -1
	else:
		mid = (start + end) // 2
		if lst[mid] == target:
			return mid
		elif lst[mid] > target:
			return bs(start, mid - 1)
		else:
			return bs(mid + 1, end)

result = bs(0, len(lst) - 1)
print(result)
  • 반복문으로 구현
lst = [1, 2, 5, 8, 13]
target = 13
start = 0
end = len(lst) - 1
result = -1

while start <= end:
	mid = (start + end) // 2
	if lst[mid] == target:
		result = mid
		break
	elif lst[mid] > target:
		end = mid - 1
	else:
		start = mid + 1

print(result)

2-2. 합병 정렬(Merge Sort)

  • 분할: 원소 n개인 리스트를 n/2개의 원소를 가진 두 개의 리스트로 분할
    정복: 왼쪽 리스트와 오른쪽 리스트를 각각 재귀적으로 합병 정렬
    통합: 각각 정렬된 두 개의 리스트를 정렬된 하나의 리스트로 합병하여 리턴
  • 재귀 함수로 구현 --- 리스트 left, right를 추가로 사용하여 메모리 낭비 
lst = [98, 11, 65, 3, 24, 7]

def merge(left, right):
	temp = []
	i = j = 0
	while i < len(left) and j < len(right):
		if left[i] < right[j]:
			temp.append(left[i])
			i += 1
		else:
			temp.append(right[j])
			j += 1
	if i < len(left):
		temp += left[i:]
	else:
		temp += right[j:]
	return temp

def mergesort(arr):
	n = len(arr)
	if n <= 1:
		return arr
	else:
		mid = n // 2
		left = mergesort(arr[0:mid])
		right = mergesort(arr[mid:])
		return merge(left, right)

result = mergesort(lst)
print(result)
  • 리스트를 추가적으로 사용하지 않고 구현
lst = [98, 11, 65, 3, 24, 7]

def merge(start, mid, end):
	temp = []
	i = start
	j = mid + 1
	while i <= mid and j <= end:
		if lst[i] < lst[j]:
			temp.append(lst[i])
			i += 1
		else:
			temp.append(lst[j])
			j += 1
	if i <= mid:
		temp += lst[i:mid+1]
	else:
		temp += lst[j:end+1]
	for k in range(start, end + 1):
		lst[k] = temp[k - start]

def mergesort(start, end):
	if start < end:
		mid = (start + end) // 2
		mergesort(start, mid)
		mergesort(mid + 1, end)
		merge(start, mid, end)

mergesort(0, len(lst) - 1)
print(lst)

2-3. 퀵 정렬(Quick Sort)

  • 추가적인 리스트를 사용하지 않는 내부 정렬
  • 분할: 기준 원소(pivot)를 정해서 이를 기준으로 좌우로 분할
    정복: 왼쪽 리스트와 오른쪽 리스트를 각각 재귀적으로 퀵 정렬
    통합: 정렬된 리스트를 리턴
  • 재귀 함수로 구현 1
lst = [98, 11, 65, 3, 24, 7]

def partition(start, end):
	pivot = lst[start]
	j = start
	for i in range(start + 1, end + 1):
		if lst[i] < pivot:
			j += 1
			lst[i], lst[j] = lst[j], lst[i]
	pivot = j
	lst[start], lst[j] = lst[j], lst[start]
	return pivot

def quicksort(start, end):
	if start < end:
		pivot = partition(start, end)
		quicksort(start, pivot - 1)
		quicksort(pivot + 1, end)

quicksort(0, len(lst) - 1)

print(lst)
  • 재귀 함수로 구현 2 --- 널리 사용하는 방법
lst = [98, 11, 65, 3, 24, 7]

def partition(start, end):
	pivot = lst[start]
	i = start + 1
	j = end
	while i <= j:
		while i <= j and lst[i] < pivot:
			i += 1
		while lst[j] > pivot:
			j -= 1
		if i < j:
			lst[i], lst[j] = lst[j], lst[i]
	pivot = j
	lst[start], lst[pivot] = lst[pivot], lst[start]
	return pivot

def quicksort(start, end):
	if start < end:
		pivot = partition(start, end)
		quicksort(start, pivot - 1)
		quicksort(pivot + 1, end)

quicksort(0, len(lst) - 1)

print(lst)

 

 

 

※ 강의 링크: https://inf.run/v8Rn

+ Recent posts