Sanjoy Dasgupta가 저술한 책인 Algorithms을 참고하였다.
1-1. LIS(Longest Increasing Subsequence. 최장증가순열)
앞에서부터 뒤로 수열의 부분수열을 구성할 때, 증가하는 순서대로 숫자를 고르면서 부분수열의 길이가 최대인 수열을 구하는 알고리즘
간단히 눈으로 구할 수 있는 LIS의 예를 들어보자
5 2 8 6 3 6 9 7
위 수열에서 증가하는 순서대로 숫자를 골라보면 “5, 8, 9”, “2, 3, 6, 9”, “8, 9” … 등이 있으며 LIS는 2, 3, 6, 9임을 쉽게 알 수 있다. 하지만 만약 수열이 굉~장히 길다면? LIS를 찾는 데 시간이 굉장히 오래 걸릴 것이다.
LIS를 위해 알아두어야 할 개념 중, Lower Bound라는 것이 있다.
Lower Bound(LB) : 배열을 정렬된 상태로 유지하면서 새로운 Value가 삽입될 수 있는 가장 작은 인덱스
1 3 3 6 7 (배열의 인덱스는 1부터 시작한다.)
위에서 2와 5를 삽입한다고 하면, “2의 LB = 2”, “5의 LB = 4”이다.
이제 LIS 알고리즘을 자세히 알아보자. 먼저 편의를 위해 다음과 같은 정의를 한다.
$L$ : 증가부분수열, $L(\max)$ : $L$의 마지막 원소(가장 큰 원소),
$x$ : 현재 탐색하고 있는 원소, $i$ : $x$의 Lower Bound, $L(i)$ : $L$의 $i$번째 원소
-
$L$이 비어있거나 $L(\max)$ < $x$인 경우 : $x$를 $L$의 마지막 원소로 추가
-
$L(\max)$ > $x$인 경우 : $x$의 Lower Bound $i$를 구해 대체
2-1. $L(i) < x$ 인 경우 : $L(i)$의 오른쪽 원소를 $x$로 대체
2-2. $L(i) > x$인 경우 : $L(i)$를 $x$로 대체
글로만 보면 무슨말인지 이해하기 힘들다. 이제 예시를 통해 LIS에 대한 자세한 이해를 해보자.
다음과 같은 수열을 바탕으로 증가부분수열 $L$을 만들어보자.
3 2 5 2 3 1 4
-
먼저, $L$이 비어있으므로 3을 넣어준다.
3
-
그 다음 2를 보자. $L(\max) = 3 > 2$이므로 2의 LB를 구한다. 2의 LB는 1이다. 그리고 $L(i)$ = $L(1) = 3$이므로 3과 2를 비교한다. 3 > 2이므로 위에서 2-2 설명대로 3을 2로 대체해준다.
2
-
그 다음 5를 보자. $L(\max) = 2 < 5$이므로 5를 $L$의 마지막 원소로 추가한다.
2 5
-
그 다음 2를 보자. 이미 $L$에 2가 있으므로 넘어간다.
-
그 다음 3을 보자. $L(\max) = 5 > 3$이므로 3의 LB를 구한다. 3의 LB는 2이다. 그리고 $L(i) = L(2) = 5$이므로 5와 3을 비교한다. 5 > 3이므로 위에서 2-2 설명대로 5를 3으로 대체해준다.
2 3
-
그 다음 1을 보자. $L(\max) = 3 > 1$이므로 1의 LB를 구한다. 1의 LB는 1이다. 그리고 $L(i) = L(1) = 2$이므로 2와 1을 비교한다. 2 > 1이므로 위에서 2-2 설명대로 2를 1로 대체해준다.
1 3
-
마지막으로 4를 보자. $L(\max) = 3 < 4$이므로 4를 $L$의 마지막 원소로 추가한다.
1 3 4
1-2. LIS의 시간복잡도
먼저 수열의 앞에서부터 차례대로 뒤로 가며 탐색하므로 수열의 원소 개수 $O(n)$만큼 시간이 소요된다. 그리고 Lower Bound를 찾아 새로 탐색되는 원소가 놓여질 최적의 위치를 탐색해야하고, 이 과정에서 Binary Search가 사용되므로 Binary Search의 시간복잡도인 $O(\log n)$시간이 소요된다. 따라서 최종 시간복잡도는 이들의 곱인 $O(n\log n)$이다.
[참고]
만약 위 LIS 알고리즘을 사용하지 않고, 조금 무식한 아래와 같은 방법을 사용한다고 하자.
$L(j)$ : $j$번째 원소를 마지막으로 하는 LIS의 길이 = $j$번째 원소가 추가될 수 있는 증가부분수열의 길이 + 1
for $j = 1, 2, \dots , n$ :
$L(j) = \max(L(j) : (i, j) \in E) + 1$
return $max_j L(j)$
위 방법을 사용하게 된다면, $L(i)$의 계산은 $\vert E \vert$ 내에 전체 수행시간이 선형으로 주어진 $j$의 입사차수에 비례하게 되고, $\vert E \vert < \vert V^2 \vert$ 이므로 최종 시간복잡도는 $O(n^2)$이 된다.
2-1. Edit Distance(편집거리 알고리즘)
문자열간의 유사도를 알아내는 알고리즘. 두 문자열이 같아지기 위해 필요한 최소 삽입, 연산, 대체의 최소 횟수를 구한다.
간단한 예를 들어보자.
A = delegate
B = delete
A의 5, 6번째 문자를 삭제하면 B와 같아진다. 따라서 연산횟수는 2이다.
A = process
B = professor
A의 4번째 문자를 f로 대체하고 마지막 위치에 o, r을 삽입하면 B와 같아진다. 따라서 연산횟수는 3이다.
근데 만약 이 비교대상 문자열들이 매우 길다면? 연산횟수를 구하는 데에 매우 오랜 시간이 소요될 것이다. 따라서 이를 위해 Edit Distance를 이용한다.
2-2. Edit Distance(편집거리 알고리즘) 구현 과정
Edit Distance의 알고리즘은 말로 설명하기 힘들다. 바로 예시를 통해 이해해보자.
A = azced
B = abcedf
Edit Distance를 구현하기 위해, 먼저 다음과 같이 연산 횟수 표를 작성한다.
세로에 A, 가로에 B를 공집합부터 시작해 한 문자씩 넣고 공집합과 공집합은 같으므로 연산횟수가 0이기 때문에 0을 넣어준다.
$\emptyset$ | a | b | c | d | e | f | |
---|---|---|---|---|---|---|---|
$\emptyset$ | 0 | ||||||
a | |||||||
z | |||||||
c | |||||||
e | |||||||
d |
그 다음, 각 셀에 숫자를 넣어줘야 한다. 규칙은 다음과 같다.
문자열 $A, B$가 있고, 표를 $D$, 표의 $i$행, $j$열의 값을 $D(i, j)$라 할 때,
- $A[i] = B[j] \rightarrow D(i, j) = D(i-1, j-1)$
- $A[i] \neq B[j] \rightarrow D(i, j) = \min(D(i-1, j), D(i, j-1), D(i-1, j-1)) + 1$
쉽게 설명하면 다음과 같다.
- 행 - 열이 같은 경우 : 왼쪽 위 숫자 가져오기
- 행 - 열이 다른 경우 : 왼쪽, 왼쪽 위, 위 숫자 중 최솟값에 1을 더해 가져오기
이 규칙을 이용해 차근차근 표를 완성해보자. 최종 결과는 다음과 같다.
$\emptyset$ | a | b | c | d | e | f | |
---|---|---|---|---|---|---|---|
$\emptyset$ | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
a | 1 | 0 | 1 | 2 | 3 | 4 | 5 |
z | 2 | 1 | 1 | 2 | 3 | 4 | 5 |
c | 3 | 2 | 2 | 1 | 2 | 3 | 4 |
e | 4 | 3 | 2 | 2 | 2 | 2 | 3 |
d | 5 | 4 | 4 | 3 | 2 | 3 | 3 |
먼저 2행부터 보자. $\emptyset \neq$ a 이므로 2행 3열의 값은 왼쪽 숫자 + 1 = 0 + 1 = 1이 된다.
그 다음 $\emptyset \neq$ b 이므로 2행 4열의 값은 왼쪽 숫자 + 1 = 1 + 1 = 2가 된다. 이렇게 쭉 2행을 완성한다.
그 다음 3행을 보자. a $\neq \emptyset$이므로 위 숫자 + 1 = 0 + 1 = 1이 된다.
그 다음 a = a 이므로 0이고, a $\neq$ b 이므로 왼쪽, 왼쪽 위, 위 중 가장 작은 숫자인 왼쪽 숫자 + 1 = 0 + 1 = 1이 된다.
이렇게 쭉~ 완성해나가면 위의 표를 얻을 수 있다. 쉽다!
이제 편집 거리를 구해보자. 편집거리 = 가장 오른쪽 하단의 수 = 3이다.
2-3. 편집 과정 구하기
그 다음으로, 완성된 편집거리 표를 바탕으로 편집 과정을 구해보자. 편집 과정은 가장 오른쪽 하단부터 왼쪽 대각선 위로 거슬러 올라가며 구한다. 과정은 다음과 같다.
-
오른쪽 하단 숫자부터 시작
-
왼쪽, 왼쪽 위, 위에서 최솟값 $S$ 찾음
-
$S$ = 자신 = 왼쪽 위 : 변화 X
-
$S$ < 자신일 경우
4-1. $S$ = 왼쪽 : 열의 단어를 삭제한 것
4-2. $S$ = 왼쪽 위 : 열의 단어를 행의 단어로 바꾼 것
4-3. $S$ = 위 : 행에 해당하는 단어를 추가한 것
위 과정을 참고하여 아래 편집거리 표의 편집과정을 구해보자.
$\emptyset$ | a | b | c | d | e | f | |
---|---|---|---|---|---|---|---|
$\emptyset$ | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
a | 1 | 0 | 1 | 2 | 3 | 4 | 5 |
z | 2 | 1 | 1 | 2 | 3 | 4 | 5 |
c | 3 | 2 | 2 | 1 | 2 | 3 | 4 |
e | 4 | 3 | 2 | 2 | 2 | 2 | 3 |
d | 5 | 4 | 4 | 3 | 2 | 3 | 3 |
먼저 가장 오른쪽 하단인 3부터 시작한다(3 = 자신). 왼쪽(3), 왼쪽 위(2), 위(3)에서 최솟값 $S$는 2이다.
따라서 $S$ < 자신이므로 4-1 ~ 4-3을 참고한다. $S$ = 왼쪽, $S$ = 위 이다.따라서 열의 단어 f를 삭제하고 행의 단어 d를 추가한 것을 알 수 있다.
a b c d e f $\rightarrow$ a b c d e d
그 다음 왼쪽 대각선 위로 올라가 2로 간다(2 = 자신). 왼쪽(2), 왼쪽 위(2), 위(3)에서 최솟값 $S$는 2이다.
따라서 $S$ = 자신이므로 변화가 없다.
그 다음 왼쪽 대각선 위로 올라가 2로 간다(2 = 자신). 왼쪽(1), 왼쪽 위(2), 위(3)에서 최솟값 $S$는 1이다.
따라서 $S$ < 자신이므로 4-1 ~ 4-3을 참고한다. $S$ = 왼쪽이다. 따라서 열의 단어 d를 삭제한 것을 알 수 있다.
a b c d e d $\rightarrow$ a b c d e
이런식으로 차근차근 올라가다보면 어떻게 대체했고 무슨 단어를 삭제했고… 등등 두 문자열이 같아지기 위한 편집과정을 알 수 있다.
2-4. Edit Distance(편집거리 알고리즘) 시간복잡도
두 문자열의 길이를 각각 $m, n$이라 할 때, $m$ x $n$ matrix를 구성하므로 부분문제의 개수가 이들의 곱인 $mn$이 된다. 그리고 왼쪽, 왼쪽 위, 위 중 최솟값을 찾는 데 걸리는 시간은 $O(1)$이므로 최종 시간복잡도는 $O(mn)$이다.