티스토리 뷰
문제 출처 : algospot.com :: JLIS
algospot.com :: JLIS
합친 LIS 문제 정보 문제 어떤 수열에서 0개 이상의 숫자를 지운 결과를 원 수열의 부분 수열이라고 부릅니다. 예를 들어 '4 7 6'은 '4 3 7 6 9'의 부분 수열입니다. 중복된 숫자가 없고 오름 차순으로
algospot.com
1. 문제를 해결하기 전 알아둬야 하는 이론
(1) 최적 부분 구조
- 최적 부분 구조(Optimal substructure) : 어떤 문제에 대해서 분할 방식에 성립하는 조건으로 각 부분 문제의 최척해를 통해 전체 문제의 최적해를 얻어낼 수 있는 경우 최적 부분 구조가 성립한다고 말한다. 즉, 각 부분 문제의 최적해가 전체의 최적해로 이어지는 경우를 말한다.
- 반대로 작은 부분 문제의 최적해만으로 전체 문제의 최적해를 구할 수 없다면 최적 부분 구조가 존재하지 않는다고 말한다.
(2) 증가하는 부분 수열
- 증가하는 부분 수열(LIS) : 정수 수열 S에서 0개 이상의 숫자를 지우고 남은 수열 중 부분 수열에 포함된 숫자들이 순 증가하면 이 부분 수열을 증가하는 부분 수열이라고 말한다.
- 예를 들어, 수열 S가 '1 3 4 2 4'라면 '1 2 4'는 S의 증가하는 부분 수열이지만 '1 4 4'는 그렇지 못하다.
2. 해설 풀이 방법
(1) 문제 분석
- jlis(indexA, indexB) = A[indexA]와 B[indexB]에서 시작하는 합친 증가 부분 수열의 최대 길이
- A[indexA]와 B[indexB]중 더 작은 값이 앞에 온다고 가정한다.
- 다음에 올 수열은 A[indexA+1]과 B[index+1]이후의 수열 중 max(A[indexA], B[indexB])보다 큰 수 중 하나가 된다.
- 만일, A[nextA]를 다음 숫자로 선택했을 경우, 합친 증가하는 부분 수열의 최대 길이는 jlis(indexA, indexB) + 1이 된다.
- B[nextB]를 선택할 수도 있는데 재귀 호출을 통해 nextA를 선택했을 때와 nextB를 선택했을 경우 결과적으로 더 길이가 큰 경우를 선택해야 한다.

(2) 코드
def jlis(indexA, indexB):
# 이미 계산된 결과가 있는 경우 해당 값 반환
if cache[indexA + 1][indexB + 1] != -1:
return cache[indexA + 1][indexB + 1]
# indexA와 indexB의 최초 값은 -1부터 시작하기 때문에 cache에 접근할 때는 +1을 해줘야 정상적인 인덱스 접근이 가능하다.
# cache[indexA + 1][indexB + 1] = 2 # JLIS 내부에는 적어도 A[indexA]와 B[indexB]가 들어있기 때문에 2를 초기 값으로 지정(?)
cache[indexA + 1][indexB + 1] = 0
# indexA와 indexB가 -1인 설정이 있어야 두 배열 내 가장 작은 원소를 기준으로 하는 증가하는 부분 수열을 탐색한다.
tempA = -int(10e9) if indexA == -1 else a[indexA]
tempB = -int(10e9) if indexB == -1 else b[indexB]
maxElement = max(tempA, tempB) # 다음에 들어올 수 있는 원소에 대한 기준을 설정
# 다음에 들어올 원소를 찾음
# 기준값보다 큰 원소를 추가하여 증가하는 부분 수열을 재귀적으로 만들어 보고 기존의 값과 비교하여 나오는 최대 길이를 반환한다.
# 원소가 추가되면 합친 LIS의 길이는 1씩 증가되는 것을 반영한다.
for nextA in range(indexA + 1, n):
if maxElement < a[nextA]:
cache[indexA + 1][indexB + 1] = max(cache[indexA + 1][indexB + 1], jlis(nextA, indexB) + 1)
for nextB in range(indexB + 1, m):
if maxElement < b[nextB]:
cache[indexA + 1][indexB + 1] = max(cache[indexA + 1][indexB + 1], jlis(indexA, nextB) + 1)
return cache[indexA + 1][indexB + 1]
- 먼저 두 배열 A와 B에 대해서 0번 인덱스가 아닌 -1 인덱스가 존재한다고 가정한다.(두 배열의 0번 인덱스부터 순회하면서 검사하기 필요한 과정이다.)
- 만일, -1 인덱스를 가정하지 않는다면 A = [10, 20, 30, 1, 2], B = [10, 20, 30]인 경우 a의 원소 1, 2는 증가하는 부분수열에 고려하지 않는 문제가 발생한다.
- -1 인덱스부터 순회하기 때문에 cache에 접근할 때에는 찾고자 하는 인덱스의 값에 +1을 해야 정상적으로 접근할 수 있다.
- 이로 인해, cache의 크기도 1씩 증가한다.
- 단, 배열의 인덱스는 그 자체로 접근 가능하다.
- 중간 주석 코드에 cache[indexA + 1][indexB + 1] = 2로 초기화 하는 부분이 있는데, jlis함수가 동작할 때 기존에 A[indexA]와 B[indexB]가 들어있으므로 최소 길이는 2부터 시작한다는 것을 의미한다.
- 그러나 이렇게 작성할 경우 마지막 출력 값에 -2를 해야 정상적인 답이 나오기 때문에 애초에 그냥 0으로 초기화하였다.
- 위 점화식에 나와있는 것과 코드를 비교할 때, max(max(jlis(nextA, indexB) + 1, max(jlis(indexA, nextB) + 1) 구현을 반복문 2개를 통해 구현을 했는데 어차피 둘 중 최댓값을 찾는 이유기 때문에 동시에 비교할 필요가 없었음을 유의할 필요가 있었다.
# 문제 입출력
for tc in range(int(input())):
n, m = map(int, input().split())
a = list(map(int, input().split()))
b = list(map(int, input().split()))
# cache를 통해 두 배열의 각 특정 인덱스에서 나올 수 있는 합친 LIS의 최대 길이를 저장
# cache 배열의 마지막 부터(재귀 호출할 때 제일 깊숙히 들어가서 서서히 반환하기 때문?) 최적 부분 구조로 인한 최적화 결과가 누적되어 cache의 맨 처음에 저장
cache = [[-1] * (m + 1) for _ in range(n + 1)]
print(jlis(-1, -1))
- a와 b 두 배열의 0번 인덱스부터 순차적으로 순회하기 위해 jlis(-1, -1)로 인자를 넘겼음에 유의해야 한다.
- 또한, -1 인덱스의 가정으로 인해 cache의 크기가 배열의 크기에 대해서 1씩 증가했다.
3. 후기
사실 이번 문제는 책의 내용을 보면서 무엇인가 뚜렷하게 이해햐지 못한 부분이 많다. 이 부분을 몇 번 다시 보긴 했지만 결국 '그냥 그런갑다'하고 넘어가게 되었다... 매번 하는 이야기지만 문제 해결을 위한 설계 과정을 떠올리기가 정말 쉽지가 않다. 그런데 보통의 경우 설계된 과정을 보면 이해가 되었는데 이 문제는 점화식을 보고도 왜 이렇게 할까 라는 의문이 많이 들었다.
증가하는 부분 수열의 경우 다른 코딩테스트 책에서 봤었다. 그러나 다른 책에서는 실제 증가하는 부분 수열을 구하는 문제였고 이 문제의 경우 증가하는 부분 수열의 최대 길이를 구하는 차이점이 있었다. 그리고 최대 길이를 구하기 위해서 일종의 -1 인덱스를 처음 접했는데 이 부분을 이해하기 위해서 테스트 코드를 만들어 실험해서 그 이유를 알 수 있었다. 그러나 남에게 설명해보라 하면 내 스스로도 어렴풋이 이해햔 것이기 때문에 남들이 듣기에 납득이 되지 않을 거 같아 구체적으로 설명은 하지 않았다. 아마 후일에 몇 번 더 복습하다 보면 그래도 이해할 수 있을 것이라 자신한다.
LeeSeok-Jun/Algorithmic_Problem_Soving_Strategies
프로그래밍 대회에서 배우는 알고리즘 문제해결 전략. Contribute to LeeSeok-Jun/Algorithmic_Problem_Soving_Strategies development by creating an account on GitHub.
github.com
'프로그래밍 대회에서 배우는 알고리즘 문제해결 전략 > 8장_동적 계획법' 카테고리의 다른 글
| 8.11 경우의 수와 확률(1)_타일링 방법의 수 세기, 삼각형 위의 최대 경로 개수 세기(Python) (0) | 2021.04.08 |
|---|---|
| 8.9 문제 : Quantization(Python) (0) | 2021.03.31 |
| 8.7 문제 : 원주율 외우기(Python) (0) | 2021.03.31 |
| 8.2 문제 : 와일드카드(Python) (0) | 2021.03.25 |
| 8.1 동적 계획법_도입 (0) | 2021.03.25 |
- Total
- Today
- Yesterday
- 마르코프 연쇄
- 프로그래밍 대회에서 배우는 알고리즘 문제해결전략
- k번째 답 계산하기
- 파이썬
- 동적 계획법
- 난이도:하
- python
- 탐욕법
- 난이도:상
- 난이도:중
- 프로그래머스
- 카카오
- 구현
- 프로그래밍 대회에서 배우는 알고리즘 문제 해결 전략
- 그리디
- 비트 마스크
- 종만북
- 프로그래밍 대회에서 배우는 알고리즘 문제해결 전략
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |