Contents

Sort

Dynamic Programming

Greedy

Graph

Sort

c++ python

Insertion sort

incremental하게 하나씩 원소를 삽입하며 정렬하는 방식

즉, $j$ 번째 이전에 이미 정렬되어있는 상태에서 $A_j$를 삽입한후 제자리를 찾아가는 방식

1
2
3
4
5
6
7
8
9
10
11
insert(A, j, key)
	i = j - 1
    while i > 0 and A[i] > key
    	A[i+1] = A[i]
        i = i - 1
    A[i+1] = key
    

IS(A)
    for j = 2 to n 
        insert(a, j, a[j])

blog

Quicksort (increasing order)

qs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
QS(a, p, r)
	if p >= r
		return
	# select random btw p ~ r
	i = random(p,r)
	swap(a[i], a[r]) # 무조껀 뒤로 보냄
	
	# partition
	i = p - 1
	for j = p to r-1
		if a[j] <= a[r]
			i += 1
			swap(a[j], a[i])
    swap(a[i+1], a[r])
    q = i + 1 # a[q] finds a right position
    
    # divide and conquer
    QS(a, p, q-1)
    QS(a, q+1, r)	

randomized quick sort가 아닐 경우 worst case time complexity

i 번째 원소에서 분할이 될 때 점화식은 다음과 같다.

$T(n) = T(i-1) + T(n-i) + O(n)$

최악의 경우 $i = 1 \,or \,n$ 이고 다음과 같다.

$T(n) = T(n-1) + O(n)$

반복대치를 통해 시간복잡도를 구해보자 \(\cancel{T(0)} = 0\\ \cancel{T(1)} = \cancel{T(0)} + O(1)\\ \cancel{T(2)} = \cancel{T(1)} + O(2)\\ ...\\ T(n) = \cancel{T(n-1)} + O(n)\\ -------------\\ T(n) = O(1)\, + \,...\, + \,O(n) = O(n^2)\)

proof by mathmatical induction 방법을 통해 randomized quicksort 의 expected time complextiy 증명 \(Suppose \;\exist c \;s.t\; T(k) \le ck\log{k} \;\forall \;2\le k <n\) $T(n) = T(i-1) + T(n-i) + O(n)$ 에서 i는 1 부터 n 중에서 uniform하게 선택되기 때문에 \(\begin{matrix} T(n) &=& \frac{1}{n}\sum_{i=1}^n {T(i-1) + T(n-i)} + O(n)\\ &=& \frac{2}{n}\sum_{k=0}^{n-1}T(k) + O(n)\\ &=& \frac{2}{n}\sum_{k=2}^{n-1}T(k) + \{O(0) + O(1) + O(n)\}\\ &\le& \frac{2}{n}\sum_{k=2}^{n-1}ck\log{k} + O(n)\leftarrow 가정\\ &\le& \frac{2}{n}\sum_{k=1}^{n-1}ck\log{k} + O(n)\\ &=& \frac{2c}{n}\{\sum_{k=1}^{[\frac{n}{2}]-1}k\log{k} + \sum_{k=[\frac{n}{2}]}^{n-1}k\log{k}\} + O(n)\\ &\le& \frac{2c}{n}\{\sum_{k=1}^{[\frac{n}{2}]-1}k\log{\frac{n}{2}} + \sum_{k=[\frac{n}{2}]}^{n-1}k\log{n}\} + O(n)\leftarrow \sum_{k=1}^{[\frac{n}{2}]-1}\log{k} \le \log{\frac{n}{2}}이므로\\ &=& \frac{2c}{n}\{\sum_{k=1}^{[\frac{n}{2}]-1}k(\log{n} - 1) + \sum_{k=[\frac{n}{2}]}^{n-1}k\log{n}\} + O(n)\\ &=& \frac{2c}{n}\{\log{n}\sum_{k=1}^{n-1}k - \sum_{k=1}^{[\frac{n}{2}]-1}k\} + O(n)\\ &\le& \frac{2c}{n}\{\log{n}\frac{n(n-1)}{2} - \frac{1}{2}(\frac{n}{2}- 1)(\frac{n}{2})\} + O(n) \leftarrow [\frac{n}{2}] \le \frac{n}{2} 이므로 \\ &=& c(n\log{n} -\log{n} - \frac{n}{8} + \frac{n}{4}) + O(n)\\ &=& O(n\log{n}) \end{matrix}\)

다른방식으로 expected running time $O(nlogn)$ 증명

intuition: quick sort에서 parition 은 $O(n)$ 번 불리게 되어있다. 이때, qicksort의 성능은 pivot이 어디 선택되어 partition 내부 안에서 비교 횟수가 몇번 불리느냐에 달려있다. 모든 $n$ 번의 patition에서 비교 횟수가 불리는 수의 합을 $X$ 라 하면, time complexity는 $O(n+X)$ 이다.

그래서 call 되는 patition 함수들 안에서 비교되는 횟수의 합의 평균 $E[X] $가 성능을 좌우한다. 이 값을 구하기위해 그 안에서 정렬된 숫자를 ${z_i, …,z_j}$ 라고 하면,

picture

그리고, $E[X]$를 estimate하기위해 i.i.d. $ X_{ij} = 1$ (if $z_i$ is compared to $z_j$, o.w., 0) 를 정의하면 ($P[$ $z_i$ is compared with $z_j] = P_{ij}$ 라 하자, iid 특성은 평균값이 확률값과 같음)

$E[X] = E[\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}X_{ij}] = \sum_{i=1}^{n-1}\sum_{j=i+1}^{n}E[X_{ij}] = $ $\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}$$P_{ij}$ 이되는데,

picture

위의 그림으로 부터 알 수 있는 것은, 서로 다른 partition에 있는 $z_i$ 와 $z_j$ 는 절대 비교 안된다는 사실로부터 partition 함수 안에 $z_i$, $z_j$ 가 있어야하며 둘중 하나는 반드시 pivot이 될것 이라는 사실이다. 따라서, $j-i+1$ element 중 $z_i, z_j$가 각각 pivot으로 뽑힐 확률인 $P_{ij} = 2/(j-i+1)$ 이다.

다시 되돌아가서, 계산해보면, $\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}$$2/(j-i+1)$ $\le \sum_{i=1}^{n} \sum_{i \le j}{2/j} = O(nlogn)$

더 나아가서 …

r.f. 항상좋은 quick sort 방법은? median of median 이용 link

quickselect c++

Mergesort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Merge(A, p, q, r)
    n1 = q - p + 1
    n2 = r - q
    let L[1 .. n1 + 1] and R[1 ..n2 + 1] be new arrays
    for i = 1 to n1 
        L[i] = A[p + i - 1]
    for j = 1 to n2 
        R[j] = A[q + j]
    L[n1 + 1] = Inf
    R[n2 + 1] = Inf
    i,j = 1
    for k = p to r
        if L[i] <= R[j]
            A[k] = L[i]
            i += 1
        else
            A[k] = R[j]
            j += 1

MS(A, p, r)
    if p >= r 
        return
    q = (p + r) / 2;
    MS(A, p, q);
    MS(A, q + 1, r);
    Merge(A, p, q, r);

time complexity

마스터정리를 사용하여 시간복잡도를 구해보자. \(T(n) = 2 T(\frac{n}{2}) + O(n)\\ f(n) = O(n)\\ h(n) = n^{\log_{2}{2}} = O(n) = f(n)\\ \therefore T(n) = O(n\log{n})\)

Heapsort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# increasing order sort를 위한 max heap 을 기준으로 설명
heapify(A, i) 
    left = 2i
    right = 2i + 1
    
    if right > n 
    	return
    
    # find largest among A[i], A[left], A[right]
    if A[i] < A[left]
    	largest = left
    else 
    	largest = i 
    if A[largest] < A[right]
    	largest = right
    
    swap(A[i], A[largest])
    
    if largest != i
    	heapify(A, largest)

# bottom up 
build_heap(A)	
	A.heapsize = |A|
    for i = A.length / 2 downto 1 
    	heapify(i) 
        
# divide and conquer
make_heap(A, i)
    left = 2i
    right = 2i + 1
    
    if right > n
    	return 
    
    make_heap(left)
    make_heap(right)
    
    if A[i] < A[left]
    	largest = left 
    else largest = i
    if A[largest] < A[right]
    	largest = right
    
    swap(A[i], A[largest])
    
    if largest != i 
    	heapify(A, largest)
        
HS(A)
	build_heap(A)
	for i = |A| downto 2 
    	swap(A[1], A[i])
        A.heapsize -= 1
        heapify(A,1)

heapify: $T(n) = T(n/2) + 1 = O(logn)$

build_heap: $T(n) = 2T(n/2) + O(logn)$ \(n^{log_2^2} > logn \\ \therefore T(n) = O(n)\) divide conquer 를 통한 build heap c++

Dynamic Programming

divide and conquer(recursive call, top down) 에서 memo를 하면 DP 가 된다.

따라서 크게 top down with memo, bottom up 2가지 접근법

DP를 사용하려면 Optimal substructure, Overlapping subproblem 2가지 성질을 증명해야함

python

Rod cutting

막대 길이에 대한 가격 $p[1..n]$ 이 주어졌을때, 최대한 비싸게 막대를 잘라서 팔아먹는 가격 $r_n$

cut을 한 경우과 안할 경우로 나누어 더 좋은것 선택 하면 다음과 같은 식으로 reculsive formula 가능 $r_n = \max(p_n, \underset{1 \le i < n}{\max}{(r_i + r_{n-i})}) \text{, if } n \ge 1$,

$i=n$을 포함 시키고, $r_i$부분을 $ p_i$로 바꾸고, $ p_n +r_0 = p_n$으로 하면, 수식 간략화가 가능하다. \(\begin{aligned} r_n &= \begin{cases} \underset{1 \le i \le n}{\max}(p_i + r_{n-i}) & \text{if } n \ge 1 \\ 0 & \text{if } n = 0\\ \end{cases} \end{aligned}\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# topdown with memo
lookup(p, n, r)
	# 저장되어 있다면 저장된 값 return 
	if r[n] >= 0 
    	return r[n]
    # base case 
	if n == 0
    	return 0
    q = -INF
    for i = 1 to n 
    	q = max(q, p[i] + lookup(p, n-i, r))
    # memo
    r[n] = q
    return q 

rodcut(p, n)
	let r[0..n] be a new array 
    # initialization
    for i = 0 to n 
    	r[i] = -INF
    return lookup(p, n, r)

# bottom up 
rodcut(p, n)
	let r[0..n] be a new array 
    r[0] = 0
    for j = 1 to n 
    	q = -INF
        # lookup with memo 
        for i = 1 to j
        	q = max(q, p[i] + r[j-i])
        r[j] = q 
    return r[n]

entry 갯수 $n$, 각 entry 계산하는데 $O(n)$ 걸린다. 따라서, $T(n) = O(n^2)$

c++

Matrix-chain multiplication

matrix multiplication 할때, 계산을 효율적으로 하기위해 parenthesization 을 어떻게 효율적으로 할 것인가? $m[i, j]$ 는 matrix chain multiplication $A_i A_{i+1} ..A_j $을 계산하는데 필요한 cost

$A_i \in \mathbb{R}^{p_{i-1} \times p_i}$ \(m[i,j] = \begin{cases} 0 & \text{if }i=j \\ \underset{i \le k <j}{\min}{(m[i,k] + m[k+1,j] + p_{i-1}p_kp_j)} & \text{if } i<j\\ \end{cases}\) $( A_i A_{i+1} ..A_k )$$( A_{k+1} A_{k+2} ..A_j )$ 에 대한 cost $m[i,k] + m[k+1,j] + p_{i-1}p_kp_j$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# topdown with memo
lookup(p, m, i, j)
	# 저장되어있다면 저장된 값 사용
	if m[i,j] < INF
    	return m[i,j]
    # base case
    if i == j
    	return 0
    
    q = INF
	for k= i to j-1
    	q = min(q, lookup(p, m, i, k) + lookup(p, m, k+1, j) + p[i-1]*p[k]*p[j])
    m[i,j] = q
    return q 

MatrixChain(p, n)
	let m[1..n, 1..n] be a new n × n array 
    all m[:,:] be initialized by INF
    return lookup(p, m, 1, n)

# bottom up 
MatrixChain(p) # p = [p[0], ..., p[n]]
	n = |p|-1
    # base case (chain length l = 1 일때)
    for i = 1 to n 
        m[i,i] = 0
    for l = 2 to n 
    	for i = 1 to n-l+1
        	j = i+l-1
            m[i,j] = INF
            for k = i to j-1
            	q = min(q, m[i, k] + m[k+1, j] + p[i-1]*p[k]*p[j])
            m[i,j] = q
    return m[1,n]

bottom up 방식이 좀 어려울수 있다.

chain length $l = 1, …, n$ 까지 minimum cost를 업데이트 하면 optimal cost를 찾을 수 있다.

matrixchain

$T(n) = O(n^3)$

c++

Longest Common Subsequence

$C_{i,j}$ 는 $X_i$ 와 $Y_j$의 LCS 길이 $X_i = <x_1, …, x_i>, Y_j = <y_1, …, y_j>$ \(C_{i,j} = \begin{cases} 0, &\mbox{if } i = 0 \or j = 0 \\ C_{i-1,j-1} + 1 &\mbox{if } i,j > 0 \and x_i = y_j \\ \max\{C_{i-1,j}, C_{i, j-1}\} &\mbox{if } i,j > 0 \and x_i \neq y_j \end{cases}\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# topdown with memo
lookup(X, Y, c, i, j)
	if c[i,j] >= 0
    	return c[i,j]
    # base case
    if i==0 or j==0
    	c[i,j] = 0
    	return c[i,j]
    
    # i > 0 or j > 0
    if X[i] == Y[j]
    	c[i,j] = lookup(X, Y, c, i-1, j-1) + 1
        return c[i,j]
    else
    	c[i,j] = max(lookup(X, Y, c, i-1, j), lookup(X, Y, c, i, j-1))
        return c[i,j]
    
LCS(X, Y)
	m = |X|
	n = |Y|
	let c[0..m, 0..n] be a new array 
    # initialization
    all c[:,:] be initialized by -INF
    return lookup(X, Y, c, m, n)
    
    
    
# bottom up 
LCS(X, Y)
	m = |X|
    n = |Y|
    let c[0..m, 0..n] be a new array 
    all c[:,:] be initialized by -INF
    # base case
    if i == 0 to m
    	c[i,0] = 0
    if j == 0 to m
    	c[0,j] = 0
    for i = 1 to m 
    	for j = 1 to n 
        	if X[i] == Y[j]
            	c[i,j] = c[i-1, j-1] + 1
            else 
            	c[i,j] = max(c[i-1, j], c[i, j-1])
    return c[m,n]

$T(n) = O(mn)$

c++

Assembly Line

pdf 참조

c++

0 - 1 knapsack

0-1 Knapsack problem is not satisfied with Greedy choice property

$i$ 는 item index, $w$ 는 possible weight, $v_i$는 item $i$ 의 가격

$c[i,w]$ 는 item $i$개를 주머니에 넣고, 주머니에 담을수 있는 weight가 $w$ 일때의 주머니 안의 물건들 가격
\(c[i,w] = \begin{cases} 0 &\text{if }i=0 ~ \or w=0 \\ c[i-1,w] &\text{if } i > 0 ~ \and w < w_i \\ max(v_i + c[i-1,w-w_i], c[i-1,w]) &\text{if } i > 0 ~ \and w \ge w_i \end{cases}\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# topdown with memo
# V[i], W[i]는 item i에 대한 pric와 weight 정보 array
# W(array)와 w(scalar)는 다르다.
lookup(V, W, c, i, w)
	if c[i,w] >= 0
    	return c[i,w]
    # base case
    if i==0 || w==0
    	c[i,w] = 0
        return 0
    if W[i] > w 
    	c[i,w] = lookup(V, W, c, i-1, w)
        return c[i,w]
    else
    	c[i,w] = max(V[i] + c[i-1, w-W[i]], c[i-1, w])
		return c[i,w]

knapsack(V, W, maxW)
	n = |V|
	let c[0..n, 0..maxW] be a new array
    all c[..] initialized by -INF
    return lookup(V, W, c, n, maxW)
        
# bottom up
knapsack(V, W, maxW)
    n = |V|
	let c[0..n, 0..maxW] be a new array
    all c[..] initialized by -INF
    for i = 0 to n
    	for w = 0 to maxW
        	if i==0 || w==0
            	# base case 
            	c[i,w] = 0
            else
				if W[i] > w
                	c[i,w] = c[i-1, w]
                else
                	c[i,w] = max(V[i] + c[i-1, w-W[i]], c[i-1, w])
    return c[n, maxW]

$T(n) = O(nW_{max})$

c++

Greedy

python

Activity Selection Problem

여러 starting time, finish time 을가진 activity $a = [s,f]$들이 있을 때 주어진 시간내에 compatible 한 activity들을 가장 많이 포함시킬 수 있도록 scheduling 하는 문제

naive 하게 생각 해보면 $n$개의 activity들 이 있을때 $2^n$ 가지의 모든 subset 에서 각 subset 마다 가지고있는 activity들이 서로 compatible한지 검사하고, 모든 activity들이 compatible하다면 그 갯수를 저장하고, 그 중 최대를 갖는 subset를 찾는다.

$S_k _{k=[1,2^n]} = { \text{최대 n 개의 activity} }$

이때, ${ .. }$안의 각 activity들이 서로 compatible 한가 test (All pair로 검사한다면, $O(n^2)$걸림 )

따라서, $O(2^n n^2)$ 이므로 너무 비싸다.

조금 나아가 생각해보면, activity 들이 만약 finishing time에 의해 sorting 되어 있다고 가정해보자.

그러면 compatible test 에서 linear하게 scan하면 된다. ($i$번째에서 starting time이 $i-1$번째의 finishing time보다 앞선 다면 compatible하지 않은 것이므로 즉, $a_i.s < a_{i-1}.f$ 이면 non compatible 함)

따라서, $O(2^n n)$ 으로 약간 개선된다. (activity 들이 만약 finishing time에 의해 sorting 되어 있다고 가정한 상황이므로 sorting 시간 무시)

DP 이용

여기서 더 개선 하기 위해선, 모든 subset을 보지 않고, overlapping되는 subproblem들을 이용한다.

(새롭게 notation을 정의함)

가정 상황: activity 들이 finishing time에 의해 sorting 되어 있다.

여기선 어떻게 문제를 정의 하느냐에따라 time complexity가 달라질 수 있다.

**approach1 **교재의 방식은

act1

$S_{0,n+1}$ 의 원소 갯수가 최대가 되도록 선택을 하고 싶다! (dummy 원소 $a_0, a_{n+1}$ 두고)

$c[i,j]$를 $S_{ij}$ 안에서 compatible한 activity들의 최대수로 정의 \(c[i,j] = \begin{cases} 0 & \text{if}~ S_{ij} = \empty \\ \underset{i < k < j ~ s.t. ~ a_k ∈ S_{ij}}{\max}(c[i,k] + c[k,j] + 1) & \text{if}~ S_{ij} \neq \empty \end{cases}\) $T(n) = O(n^3) $

approach2 또 다른 방식으로 entry수를 $n$으로 줄여 보았다.

act2

$L_i$ 는 $a_i$를 포함 하는 $0,…, i $에 대해 compatible 한 activity 갯수

따라서, $L[0..n]$까지 업데이트 후에 가장 max인 값을 택해야한다. $$ \begin{aligned} L_i &= \begin{cases} \underset{0 \le k<i ~ s.t ~ a_k.f \le a_i.s}{\max}{(L_k + 1)} & \text{if } a_0.f \le a_i.s &\text{#하나라도 compatible 한 activity존재}
1 & \text{o.w} \end{cases}
result &= \underset{1 \le i \le n}{\max}{L_i}

\end{aligned} $$ $T(n) = O(n^2) $

c++

greedy choice이용

greedy choice는 compatible한 activity중 finishing time이 가장 빠른것을 선택.

redefine for greedy algorithm

$S_{ij} -> S_k $ = {$a_i ∈ S: s_i \ge f_k $}

If we use greedy algorithm, DP formula can be transformed.

\[\begin{aligned} c[0,n+1] &= c[0,m_1] + c[m_1, n+1] + 1 &m_1\text{ is greedy choice} \\ &= c[m_2, n+1] + 1 + 1 &m_2\text{ is greedy choice}\\ &= c[m_3, n+1] + 1 + 1 + 1 &m_3\text{ is greedy choice}\\ &= ~ ... \\ &= c[m_m, n+1] + m \end{aligned}\]

note that $c[0,m] = 0, c[m_1,m_2] = 0, … ,c[m_{m-1},c_m] = 0$ , where $m \le n $

따라서, entry를 줄일 수 있다(entries are decreased by O(n), each entry takes O(1)).

greedy algorithm에 대한 recursive formula 를 적어보았다. $$ \begin{aligned} m_k &= \begin{cases} 0 & \text{if } k=0 &\text{ base case}
k & \text{if } a_{m_{k-1}.f} \le a_k.s &\text{find a first compatible activitiy}
m_{k-1} & \text{o.w } \end{cases} \

A_k &= \begin{cases} A_{k-1} + 1 & \text{if } a_{m_{k-1}}.f \le a_k.s &# k\text{ is greedy choice}
A_{k-1} \end{cases} \end{aligned}
$$ $A_n$ 값을 찾으면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# recursive version 
def Recursive_Greedy(s,f,k,n)
    m = k+1
    # Find appropriate m for using optimal sol
    # m은 compatible한 activity중  finishing time이 가장 빠른 activity index 
    while m <= n and s[m] < f[k]
        m = m + 1 
    
    if m <= n 
        return 'a_m' union Recursive_Greedy(s,f,m,n)
    else:
        return None
        
# iterative version 
def Iterative_Greedy(s,f)
    n = |s|
    A = 'a_1' # A always has a1 
    k = 1 
    for m = 2 to n:
        if s[m] >= f[k]:
            A = A union 'a_{}'.format(m)
            k = m 
    return A 

Therefore, \(T(n) = O(n), \text{ suppose that prepressing sortng }~ a_k \text{ with finishing time }\) c++

Fractional Knapsack

Fractional Knapsack Problem through Greedy algorithm

Greedy choice : first of all, an item with higher Unit price per weight must be selected

preprocessing으로 quick sort를 이용하여 무게당 가격이 높은 순으로 $O(nlogn)$시간에 sorting하고,

그 순서대로 아이템을 $O(n) $시간에 linear scan하며 물건을 담을 수있다면 물건을 담고, 아니면 일부만 담는다.

notation은 0-1 knapsack problem 과 동일

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# assume that V[i], W[i] sort by decreasing order with (V[i]/W[i])
knapsack(V, W, maxW) {
    n = |V|
	residualw = maxW;
    profit = 0
	for i = 1 to n
    	# 담을 수 있다면, 담는다.
		if W[i] <= residualw
			profit = profit + V[i]
			residualw = residualw - W[i]
		# 담을 수 없다면, 일부만 담고 종료
    	else {
			# w[i] > residualw
			profit += V[i]*(residualw/w[i]);
			break;
		}
	}
	return result;

c++

Graph

Implementation Pros and Cons

Describe the linked list representation of a graph

각 정점에 인접한 정점들을 2차원 배열의 자료구조로 표현하는 방법이다.

정점의 개수가 n개인 그래프가 주어졌을 때 n$\times$ n matrix를 준비한다.

가중치가 없는 그래프라면 가중치의 값을 항상 1로 할당한다.

정점 i에서 정점 j로 가는 간선이 존재하면 그 가중치의 값을 원소의 값으로 할당한다.

간선이 존재하지 않으면 값을 0으로 할당한다.

  • Advantage
    • 간선의 존재여부를 확인하는데 상수 시간 (O(1))이 소요된다.
  • Disadvantage
    • 그래프를 생성하는데 $n^2$ 의 메모리와 O($n^2$) 의 시간이 소요된다.

Describe the adjacency matrix representation of a graph

각 정점에 인접한 정점들을 리스트로 표현하는 방법이다.

정점의 개수가 n개인 그래프에서 n 길이의 list를 준비한다.

각 정점에 인접한 정점들을 연결리스트로 매단다.

각 노드는 정점번호, 가중치, 다음 정점의 포인터 로 구성된다.

필요한 총 노드 수는 총 간선수의 두배이다.

  • Advantage
    • 그래프를 생성하는데 2 E 만큼의 메모리가 소요되므로 간선 수가 적은 경우 유용하다.
  • Disadvantage
    • 간선 수가 많은 경우 ( E > V ) 오히려 필요한 오버헤드가 크다.
    • 간선의 존재여부를 확인 하는데 O(n) 시간이 소요된다.

BFS

Queue를 이용하여 search 혹은 shortest path 찾음

가장 쉬운 알고리즘으로는 모든 edge weight(distance)를 1로 둔 방식

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
BFS(G, s)
	let visited[1 ..|G.V|] be a boolean array and all initialized by False
	create queue Q
	Q.push(s)
	visited[s] = True

	while !Q.empty()
		u  Q.pop()
        
		# update u.d timing
        # print u
        
		for v in G.adj[u]
        	# 이전에 방문한적이 없다면 방문
			if visited[v] == False
				Q.push(v)
				visited[v] = True

# for forest G 
BFSAll(G)
	let visited[1 ..|G.V|] be a boolean array and all initialized by False
    for s in G.V
    	if visited[s] == False
            # 기존 BFS에 visited를 argument로 import하여 사용
        	BFS(G, s, visited)
adjacent list 자료구조를 사용한 graph라면 모두 한번씩 보게 되므로 $T(n) = O( V + E )$

blog 설명 python c++

DFS

stack을 이용하여 search 혹은 shortest path 찾음

DFS can be implemented by recursive call without stack

because recursive call means using a stack in our main memory(stack field)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# iterative version
DFS(G, s)
	let visited[1 ..|G.V|] be a boolean array and all initialized by False
	create stack S
	S.push(s)
	visited[s] = True

	while !S.empty()
		u  S.pop()
        
		# update u.d timing
        # print u
        visited[u] = True
        
		for v in G.adj[u]
        	# 이전에 방문한적이 없다면 방문
			if visited[v] == False
				S.push(v)
                
# recursive version
# function call시 내재된 stack이용 
Util(G, u, visited)
	# similar with u ← S.pop() 
    
	# update u.d timing
    # print u
	visited[u] = True;
    
    for v in G.adj[u]
    	if visited[v] == False
        	Util(G, v, visited)
    
    # update v.f set timing
    # ..
    return 

DFS(G, s)
	let visited[1 ..|G.V|] be a boolean array and all initialized by False
    # similar with S.push(s)
    Util(G, s, visited)

# for forest G 
DFSAll(G)
	let visited[1 ..|G.V|] be a boolean array and all initialized by False
    for s in G.V
        if visited[s] == False
        	# same with Until(G, s, visited)
        	DFS(G, s, visited)
adjacent list 자료구조를 사용한 graph라면 모두 한번씩 보게 되므로 $T(n) = O( V + E )$

blog 설명 python c++

Topological Sort

DFS 를 쓰는 application 중 한가지인 Topological sort 방법

기본 가정: DAG(Directed Acyclic graph)에서만 쓸수 있다.

DAG에서 즉, 방향 그래프에 존재하는 각 정점들의 선행 순서를 위배하지 않으면서 모든 정점을 나열하는 것

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
topoDFS(G, u, visited, S)
	visited[u] = True
    for v in G.adj[u]
    	if visited[v] == False
        	topoDFS(G, S, visited, v)
            
	# update v.f set timing
    S.push(v)
    
TopoSort(G)
    create stack S
    create list L
    let visited[1 ..|G.V|] be a boolean array and all initialized by False;

    # DFS를 하면서 finishing time의 increasing order로 Stack S 에 넣는다.
    # adjacent list 에서의 순서에 따라 search 순서가 다를 수 있으므로 생각할때는 다음과 같이 가정
    # Let assume that the node with small number has a higher priority when DFS search  
    for u in G.V
        if (visited[u] == false)
            # DFS search starts at 'u' ...
            topoDFS(G, u, visited, S)
    
    # Stack S를 뒤집은 결과를 L이라하면, 이 결과가 곧 topolgical sort 
    while !S.empty()
        L  S.pop()

    return L
adjacent list 자료구조를 사용한 DFS 를 이용했으므로 $T(n) = O( V + E )$

c++ python greeksforgeeks 설명 한국어 블로그

Single Shortest Path

source vertex 로부터의 shortest path 와 distance를 찾아 내는 알고리즘

Bellman Ford

기본가정: no negative weight cycle(있다면 False return)

Dijkstra’s algorithm과 달리 Bellman Ford 알고리즘은 가중치가 음수인 경우에도 적용 가능. 음수 가중치가 사이클(cycle)을 이루고 있는 경우에는 작동하지 않는다.

naive 하게 그래프 정점 수만큼 그래프 내 모든 엣지에 대해 edge relaxation을 수행한다. 그러면 (negative weight cycle 이 없다는 가정하에) 모든 정점수 만큼의 relaxation을 돌았을때, shortest path를 찾을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Bellman(G, s)
	# shortest distance 값을 저장할 array
	let d[1 ..|G.V|] be a new array
    
    # initialization
    d[k] = INF for all k in G.V except for k == s 
    d[s] = 0
    
    # edge relaxations for all cases O(VE)
    for i = 1 to |G.V|
    	for (u,v) in G.E
        	if d[v] > d[u] + w(u,v)
            	d[v] = d[u] + w(u,v)
    
    # check whether eixist negative weight cycle
    # negative edge 가 있다면 edge relaxation을 했을때
    # shortest path distance보다 작은 distance 가 존재 할 것이다.
    for (u,v) in G.E
    	if d[v] > d[u] + w(u,v)
        	return False
        
    return d

모든 cases 에 대해 edge relaxation을 수행해야하므로 $T(n) =O(VE)$

c++ python 한국어 블로그

DAG

Topological sort를 사용하여 Bellman Ford 를 좀더 개선한 방식

기본가정: Topological sort를 사용해야하므로 DAG에 대해서만 사용가능

Bellman ford 알고리즘은 naive하게 모든 가능한 경우의 수에 대해서 edge relaxation 을 수행하였다.

DAG algorithm은 좀더 효율적이게 topolgical sort를 한 순서의 정점 리스트를 바탕으로 edge relaxation을 수행

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Bellman(g, s)
	let d[1 ..|G.V|] be a new array
	# initialization
    d[k] = INF for all k in G.V except for k == s 
    d[s] = 0
    
    # edge relaxations for a efficient way 
    L = TopoSort(G);
    for u in L
    	for (u,v) in G.E
        	if d[v] > d[u] + w(u,v)
            	d[v] = d[u] + w(u,v)
                
    return d 

Topological sort를 한 List 순으로 진행되므로 $T(n) =O(V + E)$

c++ python

Dijkstra

기본가정: 모든 edge가 non negative 이어야함 (가중치가 음수인 경우 작동하지 않는다)

priority queue 를 이용한 알고리즘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# version 1 
Dijkstra(G, s)
	# initialization
    k.d = INF for all k in G.V except for k == s 
    s.d = 0
     
    # vertices in set S have already shortest path distance
    create set S 
    # priority queue Q(min heap)의 {key=vertex, value=vertex.d]}
    # value가 낮을 수록 priority is higher 
    create priority queue Q
    Q  all G.V 
    
    while !Q.empty()
    	u = Q.pop()
        S  u
       	for v in G.adj[u]
        	if v not in S and v.d > u.d + w(u,v)
            	v.d = u.d + w(u,v)
                # update distance of v in O(log|V|)
                Q.update_value(v, v.d) 

# version 2 (priority queue에 update_value 함수가 없는 경우)
# 대신, 느림
Dijkstra(G, s)
    let d[1 ..|G.V|] be a new array
    # initialization
    d[k] = INF for all k in G.V except for k == s 
    d[s] = 0

    # vertices in set S have already shortest path distance
    create set S 
    # priority queue Q(min heap)의 {key=vertex, value=d[vertex]}
    create priority queue Q
    Q  {s, d[s]}

    while !Q.empty()
        u = Q.pop()
        S  u
        for v in G.adj[u]
            if v not in S and d[v] > d[u] + w(u,v)
                d[v] = d[u] + w(u,v)
                # push updated {v, v.d} in Q
                Q.push(v, v.d) 
version 1 의 time complexity 로 설명하면, $O(( V + E )log V )$

geeksforfeeks c++ python python hw

Dijkstra algorithm correctness 증명

proof by Induction 을 통해 증명하겠다.

loop invariant 는 매 iteration의 시작점에서 $u.d = \delta(s,u)$ 즉, shortest path distance

shortest path 가 결정된 vertex 집합을 $S$라 하고, 매 iteration 마다 정점 하나씩 추가된다.

base case: 시작점 source vertex의 shortest distance 는 0이므로 $s.d = \delta(s,s) = 0$ correct

dijkstra

Induction step: 임의의 iteration 이전까지는 S안에 shortest path distance들이 결정된 vertex들만 들어가다가 dijkstra 알고리즘에의해 처음으로 $\color{red}u.d \neq \delta(s.u)$ 인 $\color{red}u$가 queue에서 뽑혔다고 하자.(모순을 이끌어내겠다.)

이때, $s$부터 $u$ 까지의 shortest path에서 $S$의 경계점 바로 직전과 직후의 정점 $x$와 $y$ 를 생각해 보자.

일단. $x.d = \delta(s,x)$ 가 자명하다($S$가 shortest path distance가 결정된 정점 집합이라고 했으므로)

그래서, $y.d = \delta(s,x) + w(x,y) = \delta(s,y)$ 는 shortest distance 인 상황이며,

이 사실과 negative edge가 없다는 사실로부터 ($\delta(y,u) \ge 0$) \(\begin{aligned} u.d &> \delta(s,u) = y.d + \delta(y,u)\ge y.d \\ \therefore u.d &> y.d \end{aligned}\) 임을 주목해보자.

이 상황에서, 우리의 처음 가정이 맞다면, $u.d \le y.d $이어야한다. ( dijkstra 알고리즘에의해 처음으로 $u.d \neq \delta(s.u)$ 인 u가 queue에서 뽑혔다고 했으므로 $u.d$가 같거나 더 작아야한다.)

하지만, 그렇지 않기 때문에 모순이 된다.

따라서, $\color{red}u.d = \delta(s,u)$ 인 $\color{red}u$가 뽑혀야만 한다.

web 영문 설명

All pair shortest path

Naive DP

$ l_{ij}^{(m)}$ node $i$ 부터 node $j$ 까지 가는데 최대 $m$ 개의 edge를 거쳐서 가는 path의 minimum weight

[intution]

이때, 총 노드의수 를 $n$ 이라하면, 거쳐가는 edge의 수 $m$이 $n-1$보다 많으면 반복되는 node가 존재한다는 뜻이므로 cycle이 있다는 뜻인데, negative edge가 없다고 가정했으므로 당연히 cycle을 돌면 shortest path의 wieght sum 보다 높은 path sum이 된다. 즉, $m = n - 1$ 까지 update하면 optimal solution이 됨

$ {min}{(l_{ij}^{(m-1)}, \underset{1 \le k \le n}{min}{(l_{ik}^{(m-1)} + w_{kj} )} )} = \underset{1 \le k \le n}{min}{(l_{ik}^{(m-1)} + w_{kj} )} ~~~~ \text{if } m \ge 1$

$\because k = j $ 이면 $w_{jj}=0$ 이 되므로 case가 합쳐질수 있다.

따라서, recursive formula 는 다음과 같다.
\(l_{ij}^{(m)} = \begin{cases} \underset{1 \le k \le n}{min}{(l_{ik}^{(m-1)} + w_{kj})} & \text{if } m \ge 1 \\ l_{ij}^{(0) } = \begin{cases} 0 & \text{if } i = j\\ \infty & \text{if } i \neq j \end{cases} & \text{if } m = 0 \\ \end{cases}\)

여기서 또 주목할 만한점은 $m=1 $ 일때는 $l_{ij}^{(1)} = w_{ij}$ 이므로 $l_{ij}^{(0)}$을 굳이 계산할 필요는 없다.

$O(n^4)$ 이 걸리는 아주 비싼 알고리즘이다. $ \because n^3$ entries, each entry takes $O(n)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# update next step for m 
update(L,W)
	n = len(L)
	let L`[1..n, 1..n] be a new array
	for i = 1 to n 
		for j = 1 to n 
			L`[i,j] = INF
			# each entry can be calculated in O(n)
            for k = 1 to n 
            	L`[i,j] = min(L`[i.j], L[i.j] + W[k,j])
    return L`

# given a graph's weight matrix W[1..n, 1..n]
APSP(W)
	n = len(W)
	let L[1..n, 1..n] be a  new array
	
	# initialization
	L = W
	
	# update L for m 
	for m = 2 to n-1
		L =  update(L, W)
    
    retrun L

python

$m$ 에 대해 linear 하게 update 하는 위의 방식을 조금 더 개선해보자.

L 의 계산이 associative한 성질을 가지며, $m \ge n - 1$이면 shortest path weight는 고정되어 L 은 바뀌지 않는다.

따라서, associative 하게 $k$번 계산하여 $2^k \ge n-1$ 일 경우 optimal sol으로 고정된다.

따라서, optimal sol에 도달하기 까지$k = O(logn)$ 번 연산하게 됨.
\(\begin{aligned} L^{(1)} &= W \\ L^{(2)} &= W^2 = WW \\ L^{(4)} &= W^4 = W^2W^2\\ ... \\ L^{(2^k)} &= W^{2^k} = W^kW^k\\ L^{(2^k \ge~ n-1)} &= W^{2^k \ge ~ n-1} (fixed)\\ \end{aligned}\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# given a graph's weight matrix W[1..n, 1..n]
faster_APSP(W)
	n = len(W)
	let L[1..n, 1..n] be a  new array
	
	# initialization
	L = W
	
	# update L for m 
	for m = 2 < n-1
		L =  update(L, L)
		m = m*2
    
    retrun L

$O(n^3logn)$ 으로 계선되었지만 여전히 비싼 알고리즘이다.

Floyd Warshall Algorithm

All pair shortest path with DP 를 조금더 효율적으로 해보자는 접근

기본 가정: no negative cycle

먼저 주어진 그래프에 대한 edge정보로 부터 matrix $C$ 정의 \(C_{ij} = \left \{ \begin{matrix} 0 & \text{if } i=j \\ c(i,j) \ge 0 & \text{if } i \ne j, (i,j) \in E \\ \infty & \text{if } i \ne j, (i,j) \notin E \\ \end{matrix}\right.\) $d_{ij}^{(k)}$: $v_i \text{~} v_j$ 까지 가는데 $v_1, .., v_k$를 거쳐가는지에 대한 유무가 update된 shortest path distance ($k$ 가 증가함에따라 점점 더 많은 노드정보를 거쳐가는것에 대한 정보를 업데이트 된다).

그림 \(d_{ij}^{(k)} = \left \{ \begin{matrix} c(i,j) \ge 0 & \text{if } k=0 \\ min \{ d_{ij}^{(k-1)}, d_{ik}^{(k-1)} + d_{kj}^{(k-1)} \} & \text{if } k \ge 1 \\ \end{matrix} \right.\) Time complexity: $O(n^3)$ because all entry $(1\le i,j,k\le n)$ , is $n^3$, each entry takes $O(n)$ time

back propagation: $P^{(k)}$의 각 entry $P_{ij}^{(k)}$가 의미하는것은 현재까지 업데이트된 $v_k$ 를 지나는 $v_i \rightsquigarrow v_k \rightsquigarrow v_j$ 의 shortest path 정보를 의미한다. ($k = 1,..,n$ 까지 모두 update되어야 진짜 shortest path가 됨)

algorithm

python c++

Johnson

Dijkstra 와 BallmanFord를 이용한 알고리즘

c++ python

MST

MST(Minimum Spanning Tree) 를 구하는 것

기본 용어 설명

light edge: 어떤 정점 집합을 둘로 나누는 cut을 했을때, crossing edge 중에 weight가 가장 작은 edge

safe 하다: weight의 sum 이 minimum을 갖는 spanning tree에 대해서 어떤 edge $(u,v)$ 를 더해도 그 특성이 유지 된다면 safe 하다고 한다.

일반적인 MST 알고리즘은 다음과 같다

1
2
3
4
5
6
MST(G)
	create set A 
	while A does not form a spanning tree
		find an edge (u,v) that is safe for A 
		A = A union {(u,v)}
    return A

Kruskal

greedy method 을 이용하여 네트워크(가중치를 간선에 할당한 undirected 그래프)의 모든 정점을 최소 비용(최소 경로)으로 연결하는 최적 해답을 구하는 것

disjoint set 자료구조를 활용하며, greedy 하게 light edge 를 추가하면 safe함을 이용함

방법: disjoint set 자료구조를 활용하여 independent 한 tree들을 만들고, 작은 weight를 가진 edge 순서대로 crossing하는 edge에 대해서 tree들을 merge해 나간다. 즉, safe 할때, merge한다.

disjoint set에 2 가지 heuritic 이 사용된다.

  1. path compression
  2. union by rank

이 2가지의 heuristic을 사용하면, n 개의 disjoint set이 있을때(n 번의 makeset operation 이 수행된 상태),

makeset, findset,union operation이 모두 m 번 call 된다고하면,

전체 operation을 수행하는데 $O(m\alpha(n))$만큼의 시간이 걸린다.

여기서, $\alpha(n)$ 은 $logn$ 에 upper bounded 되어있어서 효율적인 operation 수행이 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
DisjointSet
	__init__(n)
    	let parent[1..n] be a new array
        let rank[1..n] be a new array
        # DisjointSet을 만드는 순간 n 번의 makeset operation 
    	for i=1 to n 
        	rank[i] = 0
            parent[i] = i 
    
    findset(u)
    	# path compression(한번 representative를 찾을때 이어놓는다)
    	if u != parent[u]
        	parent[u] = findset(parent[u])
        retun parent[u]
	
    union(x, y)
    	# find each representative for the set that includes x and y
    	x = findset(x)
        y = findset(y)
        
        # union by rank(rank가 작은 disjoint set을 큰쪽에 연결)
        # y 의 rank 가 x 보다 같거나 크면, y를 x.p 로 한다. 
        # (rank가 같을때는 y에 연결후, y rank만 1증가) 
        if rank[x] > rank[y]
        	parent[y] = x 
        else 
        	parent[x] = y
        if rank[x] == rank[y]
        	rank[y] = rank[y] + 1

kruskal(G)
	# weight sum 
    sum = 0;
    
    # initialization
    create DisjointSet Ds;
    # makeset for all v in G.V : O(V)
    Ds  all v in G.V 
    create a empty MST set A 
    
    # preprocessing
    # sort G.E by (weight)increasing order : O(ElogE) 
    sort(G.E);  
    
    # 서로 다른 distinct component를 crossing하는 edge 중에서 
    # weight가 가장 작은 light edge를 greedy 하게 찾는과정(DisjointSet 이용)
    # O((V+E)α(V)) 여기서 α(V) upper bounded by O(logV)
    for (u,v) in G.E
        if Ds.findset(u) != Ds.findset(v) 
            # (u,v) edge is a greedy choice 
            # update the sum of MST weight 
            sum = sum + w(u,v);
            Ds.union(u, v);
            A = A union {(u,v)}

    return sum;
kruskcal 알고리즘이 전체과정에서 makeset $O( V )$, findset $O( E )$, union $O( V )$ 번 call 되므로
kruskcal 알고리즘에서 for문에서 쓰인 disjoint set의 operation 수는 $ V + E $ 번이므로
time complexity는 $O(( V + E )\alpha( V )$ 인데,

$\alpha(n)$ 은 $logn$ 에 upper bounded 되어 있고,

graph G가 connected 되어있다면, $ E \ge V -1 $ 이고,

graph G가 sparse 하다면 $|E| > |V|^2$ 이므로,
\(\begin{aligned} T(n) &= O((V|+|E|)\alpha(|V|)) \\ &\le O(|E|\alpha(|V|)) \\ &\le O(|E|log|V|) \\ &\le O(|E|log|E|) \\ &\le O(|E|log|V|^2) = O(|E|log|V|) \end{aligned}\)

c++ python

Prim

root note $r$ 을 주면 undirected graph $G$에서 priority queue를 이용하여 모든 vertex중에서 근접한 edge의 weight를 업데이트 해나가면서 light edge 를 찾아 MST를 찾는다. 즉, safe한 edge를 더해나간다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
prim(G, r)
    # MST에서 predecessor vertex를 저장하고, 업데이트 할 array
    let parent[1..|G.V|] be a new array 
    
    # initialization
    u.d = INF for all u in G.V except for k == r 
    r.d = 0
    parent[u] = Nan for all u in G.V 
    
    # priority queue Q(min heap by value)의 {key=vertex, value=vertex.d}
    create priority queue Q 
    Q  all G.V 
    
    while !Q.empty()
    	u = Q.pop()
       	for v in G.adj[u]
        	# light edge가 발견되면 v의 value를 update하고, Q에도 반영
        	if v in Q and w(u,v) < v.d
            	v.d = w(u,v)
                parent[v] = u
                # update distance of v in O(log|V|)
                Q.update_value(v, v.d) 

light edge를 발견하면 Q 에서 나오고 더이상 parent와 d 가 업데이트 되지 않고, 그에 해당하는 edge가 MST가 됨을 주목하자!

따라서, Q에서 모든 vertex를 꺼내면 MST가 찾아지는데

이 알고리즘에 binary heap을 사용했을때, Q.pop()이나 Q.update_value() 연산이 $logV$ 만큼 걸림을 우린 알고있다.

그래서 알고리즘 내부의 수행 시간을 생각해보면,

1
2
3
4
5
6
7
8
9
# O(VlogV)
while !Q.empty()
	u = Q.pop()
	
# adjacent list를 썻다면 O(ElogV) 
# 왜냐하면 모든 edge를 돌게 되고, update_vale가 O(logV) 
while !Q.empty()
 	u = Q.pop()
	for v in G.adj[u]
따라서, $T(n) = O((V+E)logV) = O(ElogV)$ $\because $ graph G가 connected1 되어있다면, $ E \ge V -1 $ 이므로

wiki python hw

Prim algorithm correctness 증명

proof by induction, or mathmatical induction을 이용 하여 증명하겠다!

loop invariant 는 매 iteration의 시작점에서의 tree 는 MST 의 subtree

base case: 처음 시작할때 tree는 아무 노드도 없으니까 당연 optimal

induction step: k번째로 edge를 추가하려는 상황 지금까지의 알고리즘에 의한 tree는 optimal이라 가정

$T^{(k)}$는 MST $T^*$의 subgraph 인 상황

알고리즘 동작대로 해보고 다음 iteration 시작하기전 $T^{(k+1)}$ 역시 $T^* $의 subgraph인지 확인하겠다.

만약 알고리즘에 의해 어떤 edge $e = (u,v)$ 를 추가하는데 $e$가 $T^* $의 edge가 아니라고 가정! (모순을 찾겠다)

일단, $T^* \cup e$ 는 사이클이 반드시 생긴다. ($T^*$는 minimums panning tree2인데 edge를 추가했으니까 )

MST

이 사이클 안에서 $u’$은 $T^{(k)}$ 안에 있고, $v’$ 은 $T^{(k)}$ 밖에 있는 $e$와 다른 어떤 한 edge $e’=(u’,v’)$ 을 고르자

(회색선은 $T^{(k)}$ 의미)

그때 prim 알고리즘에서는 weight가 작은 edge를 선택하므로 $e$를 선택 했다면 $ w(e) \le w(e’)$ 인 상황이다.

$T ‘ = T^* - e’ + e$ 라고하면,

$ T’ \le T^* $ 가 되므로 $T^* $는 optimal 이 아닌 모순적 상황이다.

따라서, 매 iteration 마다 prim 알고리즘에 의해 추가되는 $e$는 MST $T^*$ 에 포함된 edge이다.

한글 blog 증명

Flow Maximization

flow network (2018_2 APM hw3 참조)

(1) flow network definition: 그래프에는 src, sink node가 있고, edge가 있으면 nonnegative capacity 를 갖고, edge가 없으면 capacity가 0 이됨.

flow definition: capacity $c$ , src, sink 노드 $s, t$ 를가진 flow network $G = (V,E)$ 에 대해서 edge $E$ 를 어떤 실수값 $ \R $ 로 mapping 시켜주는 함수 인데, 2가지 성질을 갖는다.

  1. Capacity constraint: $ 0 \le f(u,v) \le c(u,v) $ ,$\forall (u,v) \in E $

    flow 값이 제한됨

  2. Flow conservation: $\sum_{(u,v)\in E}{f(u,v)} = \sum_{(v,w)\in E}{f(v,w)} $ ,$\forall v \in V - {s,t} $

    src, sink 를 제외한 노드 $v$에 들어온 flow 양과 나가는 flow 양이 같다.

**(2) flow maximization problem definition: **Given a flow network G with source s and sink t , find a flow of maximum value from s to t

**LP formula: **$Maximize$ $\sum_{v\in V}{f(s,v)}$ $s.t$ Capacity Constarint, Flow conservation

이때, capacity 는 주어진 graph의 weights $c(u, v) = w(u,v)$

이 문제는 residual network와 augmenting path 라는 개념을 통해

Ford Fulkerson algorithm에 의해 풀릴 수 있다.

Ford Fulkerson

이 알고리즘을 설명하기전에 먼저, residual network $G_f$ 와 augmenting path의 개념을 알아야한다.

Definition of $G_f$: 주어진 flow network $G$ 와 동일한 정점과 간선을 갖는다. 그리고,

residual capacity 정의는 다음과 같다.

(주의사항: edge가 $(u,v),(v,u)$ 둘다 있을때는 $c_f(u,v) = c(u,v) - f(u,v) + f(v,u)$ ) \(c_f(u,v) = \left \{ \begin{matrix} c(u,v) - f(u,v) & \text{if } (u,v) \in E \\ f(v,u) & \text{if } (v,u) \in E \\ 0 & \text{o.w } \\ \end{matrix} \right.\)

augmenting path는 src $s$ 에서 sink $t$ 까지 모든 residual capacity가 0 이상인 simple path를 말한다.

$G_f$ 는 augmentation 연산이 superposition으로 계산될 수 있는 특성을 가진다. ($G_f$는 flow $f$에 의해 생성된 residual network 이고, $f’$ 은 $G_f$의 또 다른 flow. 따라서, flow는 포화 될때 까지 계속 augmented될 수있다.)

$f \uparrow f $ $= f + f’ $

augmentation 연산을 다음과 같다. \(f \uparrow f' (u,v) = \left \{ \begin{matrix} f(u,v) + f'(u,v) - f'(v,u) & \text{if } (u,v) \in E \\ 0 &\text{o.w} \\ \end{matrix}\right.\)

먼저, Ford-Fulkerson 방식에 대한 pseudo code

Time complexity: $O( E f^)$ , $f^$ 은 flow를 업데이트 한 총 횟수 (운이 나쁘면 매우 오래걸릴 수 있다.)

Ford-Fulkerson

Edmonds-Karp algorithm

(Ford-Fulkerson 방법의 한가지 구현 방법)

이제, 본론으로 돌아와서 Edmonds-Karp algorithm의 기본 idea는 다음과 같다.

$G_f$ 에서 $s \rightarrow t$ 로 가는 path를 찾을 때, 모든 edge들의 weight를 1로 한(unit distance로 본) BFS 알고리즘 을 이용함. DFS, BFS c++

이때 주목해야 할 intuition은 각 iteration 마다 찾아진 augmenting path 중에 적어도 한개의 edge는 saturated($c_f(e) = 0$) 되며 (왜냐하면 flow가 update 될때, $G_f$ 의 residual capacity값이 바뀌게 되는데 그에 따라 BFS 하는 방향 이 계속 바뀜 ), 점점 augmenting path의 길이는 길어진다. 이때, 증가되는 augmenting path의 최대 길이는 $V$ $-1$이기 때문에 모든 iteration에서 flow가 증가한 수$ f^* $는 $O( V   E )$ 로 bounded 된다.

pseudo code ref

Edmonds($G, s, t$)

​ $G_f \leftarrow G$

​ for each edge $(u,v)$ in $G.E$

​ $(u,v).f$ = 0

​ # iteration

​ while $\exist ~ p$ from $s$ to $t$ using $BFS(G_f)$

​ $c_f(p) = min { c_f(u, v): (u,v) ~ in ~ p}$

​ # aumentations

​ for each edge $(u,v)$ in $p$

​ if $(u,v) \in G.E$

​ $(u.v).f = (u,v).f + c_f(p)$

​ else

​ $(u.v).f = (u,v).f - c_f(p)$

time complexity: $O( V   E ^2 )$ 왜냐하면, BFS 하는데 $O( E )$, 총 flow augmented 수 $O( V   E )$

Maximum Bipartite Matching

bipartite그래프에서 최대한 많은 수의 matching3을 찾는 문제

이 문제는 Flow Maximization를 푸는 것을 이용할 수있다.

max matching size = total net flow 를 이용한다.

1
2
3
4
5
6
1 Given bipartite graph G
2 Add new vertices s and t.
3 Add an edge from s to every vertex in L.
4 Add an edge from every vertex in R to t.
5 Make all the capacities 1.
6 Solve maximum network flow problem on this new graph G'

maxmatching

pdf 영문 slide 설명

Min Cut

ST-Min Cut 문제는 directed edge weighted graph $G$의 vertex set $V$를 2개의 set $S, T$로 나누는데 $S$는 $s$를 포함한 집합, $T$는 $t$를 포함한 집합이다. 이때, $S$와 $T$를 crossing하는 edge weight를 최소화 하고 싶은게 이 문제이다.

Given an directed (edge weighted) graph $G=(V,E)$, with a source vertex $s$ and a sink vertex $t$, compute a st-cut ($S,T$) so that the sum of the crossing edge weights is the minimum.

이 문제는 Flow Maximization를 푸는 것을 이용할 수있다.

max-flow min-cut theorem (three conditions are equivalent)

  1. $f$ is a maximum flow in $G$. (flow maximization problem solution)

  2. The residual network $G_f$ contains no augmenting paths. (terminate condition of flow maximization)

  3. $ f = c(S, T)$ for some st-cut $(S, T)$ of $G$. (mincut problem solution)

    $ c(S, T) = \sum_{u \in S} \sum_{v \in T}{c(u,v)}$ ,여기서 $c(u,v)$ 는 residual capacity of $(u,v)$

Proof. We will prove 3 $\Rightarrow$ 1, 1 $\Rightarrow$ 2, 2 $\Rightarrow$ 3

(3 $\Rightarrow$ 1) From the capacity constraint, any flow f and any st-cut $(S’, T’)$, we have that $|f| ≤ c(S’, T’).$ \(\begin{aligned} \mbox{total net flow } |f| &= \sum_{u\in S'}\sum_{v\in T'}{f(u,v)} - \sum_{v\in S'}\sum_{u\in T'}{f(v,u)} \\ &\le \sum_{u\in S'}\sum_{v\in T'}{f(u,v)} &\because |f| \ge 0 \\ &\le \sum_{u\in S'}\sum_{v\in T'}{c(u,v)} &\because f(u,v) \le c(u,v)\\ &= c(S',T') \end{aligned}\)

From this inequality, $ f =c(S’, T’)$ implies $f$ is max flow. Thus, we have that 3 $\Rightarrow$ 1.

(1 $\Rightarrow$ 2) 모순을 이끌어 내기 위한 가정으로 1은 True인데, 2는 False라고 가정해보자.

즉, $f$ 는 $G$의 max flow인데, $G_f$ 는 augmenting path $f’$를 가진하고 하자.

그러면, $G_f$의 augmenting path $f’$을 더 함으로서 $ f+f’ = f + f’ $ property에 의해 $f$를 증가시킬 수 있다.

이 상황에서, 우리는 1 이 False($f$는 더이상 max flow가 아님)가 된다.

따라서, 모순이므로, 1이 True면 2는 반드시 True여야한다.

(2 $\Rightarrow$ 3) Let $S$ to be the set of vertices in $G$ that is reachable from $s$ by a path with positive edge capacities (of $G_f$), and let $T = V − S$.

mincut

2가 True이면 augmenting path가 더이상 없으므로 T에 속한 v 에 절대 reachable 할수 없다. 그래서, $ f = c(S,T)$ (3 도 True)라고 할 수 있다.

만약 2가 True가 아니라면, augmenting path가 있다는 것이고, 그말은 즉슨, residual capacity 보다 작은 flow f(u,v) 가 존재한다는 뜻( $\exist f(u,v) < c(u,v)$)이여서 $u$에서 $v$로 갈수 있는 augmenting path가 존재하게 되어 T에 속한 v 에 reachable 할수있게 된다. 즉,2가 False 가 되는 모순이 되므로 반드시 2가 True인 상태에서 3 이 True 가 된다.

Max Cut

Efficient algorithm: Goemans and Williamson (Approximate algorithm)

Approximate algorithm에서 다루도록 하겠다.

Reference

  1. 어떤 그래프가 connected 되었다는건 모든 정점들 간에 path가 반드시 존재하는 그래프 

  2. 최상의 정점(중심)을 루트(Root Node)로 하고, 모든 그룹 멤버들을 자손으로 갖는 트리구조 

  3. A matching in a graph is a subset of its edges, no two of which share an endpoint. 

Leave a comment