티스토리 뷰
문제 출처 : algospot.com :: KLIS
algospot.com :: KLIS
K-th Longest Increasing Sequence 문제 정보 문제 어떤 정수 수열에서 0개 이상의 숫자를 지우면 이 수열의 부분 수열 (subsequence) 를 얻을 수 있다. 예를 들어 10 7 4 9 의 부분 수열에는 7 4 9, 10 4, 10 9 등이 있
algospot.com
1. 문제 분석
- 이전의 예제와 문제(8.4절 참고)들을 통해서 최대 증가 부분 수열 중 가장 긴 것을 찾아낸 경험이 있다.
- 이를 바탕으로 위 문제를 해결하기 위해서는 다음의 과정을 거쳐야 한다.
- 1. 바탕이 되는 최적화 문제를 푼다.
- 2. 최적화 문제의 최적해를 세는 문제를 푼다.
- 3. 답의 수를 기반으로 답안을 재구성한다.
2. LIS의 개수 세기
(1) 함수 설계하기
- 수열의 i번째에서 시작하는 LIS의 수를 반환하는 경우를 생각해보자.
- 예를 들어, 전체 수열이 S = [1, 4, 3, 2]인 경우 0번 인덱스의 값인 1에서 시작하는 LIS는 [1, 4], [1, 3], [1, 2]로 총 3개다.
- 그러나 1, 2, 3번 인덱스에서 부터 LIS를 만들 경우 LIS의 길이는 모두 1인 LIS를 1개씩만 갖는다.
- (i < j)이고 어떤 수열 S에 대해서 S[i] < S[j] 인 경우 i번째 위치에서 시작하는 LIS의 수를 반환하는 함수 count( i )는 다음과 같이 정의할 수 있다.
- 그리고 한 가지, i번 인덱스에서 부터 만든 LIS의 길이는 j번 인덱스에서 부터 만든 LIS의 길이에 1을 더한 것과 같은 조건이 성립해야 한다.
(2) 코드 구현
MAX = int(2e9)+1
# S[start]에서 시작하는 증가 부분 수열 중 최대 길이를 반환한다.
def lis(start):
# 메모이제이션
# start + 1을 사용하는 이유는 S[-1]이 있다고 가정하고 0번 인덱스부터 cache에 담기 위해(?)
if cacheLen[start + 1] != -1:
return cacheLen[start + 1]
cacheLen[start + 1] = 1 # LIS에 항상 S[start + 1]는 있기 때문에 길이는 최하 1
for next in range(start+1, n):
if start == -1 or S[start] < S[next]:
cacheLen[start + 1] = max(cacheLen[start + 1], lis(next) + 1)
return cacheLen[start + 1]
# S[start]에서 시작하는 최대 증가 부분 수열의 수를 반환
def count(start):
# 기저 사례 : LIS의 길이가 1인 경우
if lis(start) == 1:
return 1
# 메모이제이션
if cacheCnt[start + 1] != -1:
return cacheCnt[start + 1]
cacheCnt[start + 1] = 0
for next in range(start+1, n):
if (start == -1 or S[start] < S[next]) and (lis(start) == lis(next) + 1):
cacheCnt[start + 1] = min(MAX, cacheCnt[start + 1] + count(next))
return cacheCnt[start + 1]
- 기존의 LIS 문제들 처럼 S[-1]의 존재를 가정하고 시작한다.(특정 인덱스에서 시작하는 LIS를 찾는게 아니라 전체 수열에서 LIS를 찾기 위함)
- lis( ) 함수의 경우 i번째 인덱스에서 시작할 때의 LIS의 최대 길이를 구한다. -> 바탕이 되는 최적해를 푼다.
- count( ) 함수의 경우 LIS의 i번째 인덱스에서 시작할 때의 LIS의 개수를 구한다. -> 최적화 문제의 최적해를 세는 문제를 푼다.
- 길이와 개수를 구하는 차이가 있지만 두 함수의 구조가 상당히 유사함을 알 수 있다.
- 위에서 설명한 'i번 인덱스에서 부터 만든 LIS의 길이는 j번 인덱스에서 부터 만든 LIS의 길이에 1을 더한 것과 같다.'는 조건이 count( ) 함수 내의 조건문에서 lis(start) == lis(next) + 1 으로 표현되었다.
3. LIS의 재구성
(1) 함수 설계하기
- 맨 처음 설명한 3가지 과정 중 2가지 과정을 해결했으니 이제 3번째 과정을 해결해야 할 차례이다.
- k번째 답을 찾기 위해 직전의 9.6 예제와 마찬가지로 skip = k-1로 초기화 하고 시작한다.
- S = [5, 1, 6, 4, 3, 2, 8, 7], k=6을 예를 들어 설명하면 다음과 같다.
- S에서 만들 수 있는 최대 길이의 LIS는 길이가 3으로 각각 S[0]과 S[1]에서 시작했을 때 만들 수 있다.
- 그리고 S[0]에서 부터 만들 수 있는 길이가 3인 LIS의 수는 2개, S[1]에서 만들 수 있는 길이가 3인 LIS의 수는 8개가 존재한다.
- 이렇게 총 10개의 길이가 3인 LIS들을 사전순으로 정렬하면 S[0]=5 > S[1]=1 이므로 S[1]에서 시작하는 LIS가 앞에 위치한다.
- skip = k-1에 따라 skip = 5이고, S[1]에서 만들 수 있는 LIS의 수는 8개이기 때문에 우리가 찾는 k번째 LIS는 S[1]에서 시작하는 LIS 중에 존재한다.
- S[1]에서 시작하는 LIS는 모두 1이기 때문에 우리는 LIS의 두 번째 부터의 값을 확인하면서 k(=6)번째를 찾아야 한다.
- 이 과정을 프로그램으로 옮기면 다음과 같이 4가지 동작을 하는 코드로 구현할 수 있다.
- 1. S[start]를 LIS에 추가 : S[start]에서 시작하는 LIS의 첫 번째 원소는 항상 S[start]
- 2. LIS에서 S[start] 다음에 올 수 있는 숫자들의 목록을 작성 : S[start] 뒤에 위치(=next)하면서 S[start]보다 크고 lis(start) = lis(next) + 1를 만족하는 모든 S[next]를 찾아 이를 숫자가 증가하는 순서대로 정렬하여 저장한다. (그냥 저장하는 것이 아니라 Python의 경우 튜플 자료형을 사용하여 저장한다.)
- 3. 목록 중 k번째 LIS에 포함되는 숫자를 찾기 : 정렬한 숫자들을 순서대로 검토하면서 이 숫자에서 시작하는 LIS의 수가 skip보다 작은지 확인한다. 답의 수가 skip을 초과한다면 k번째 LIS가 이 숫자를 사용한다는 의미다. 아니라면 skip에서 해당 숫자가 포함된 LIS의 수를 빼면 된다.
- 4. LIS의 나머지 부분 계산하기 : k번째 LIS는 정해진 숫자에서 시작하는 LIS 중 skip개를 빼고 가장 앞에 있으므로 이를 재귀 호출을 통해 해결한다.
(2) 코드 구현
1) reconstruct( )
# S[start]에서 시작하는 LIS 중 사전순으로 skip개 건너뛴 수열을 lis에 저장
def reconstruct(start, skip, lis_array):
# S[start]는 항상 lis에 포함된다.
if start != -1:
lis_array.append(S[start])
# 뒤에 올 수 있는 숫자들의 인덱스 목록을 만듦
# (숫자, 숫자의 인덱스)형태로 저장
followers = []
# range(start + 1, n)을 그냥 (start + 1, n)로 적는 대참사 발생...
for next in range(start + 1, n):
if (start == -1 or S[start] < S[next]) and lis(start) == lis(next)+1:
followers.append((S[next], next))
followers.sort() # 숫자가 증가하는 순서대로 정렬
# k번째 LIS의 다음 숫자를 찾는다.
for i in range(len(followers)):
# i번째 숫자를 S[start]뒤에 붙였을 때 만들 수 있는 LIS의 개수를 확인한다.
idx = followers[i][1]
cnt = count(idx)
# 나올 수 있는 LIS의 수가 skip보다 적기 때문에 skip에서 해당 수 만큼 제외
if cnt <= skip:
skip -= cnt
# 나올 수 있는 LIS의 수가 skip보다 많다면 k번(skip+1)째 LIS를 재귀 호출을 통해 구한다.
# 또한, k번째 LIS에서 start 뒤에 올 원소는 idx임을 알 수 있다.
else:
reconstruct(idx, skip, lis_array)
break
- 먼저, k번째 LIS의 원소들은 리스트 lis_array에 저장된다.
- followers라는 리스트에 LIS 원소들을 튜플을 통해 (값, 인덱스) 형식으로 후보군을 저장하고 정렬한다.
- 후보군의 원소들을 count( )함수를 통해 해당 원소에서 만들어지는 LIS의 개수를 확인한다.
- LIS의 개수가 skip보다 작다거나 같다면 해당 원소는 생략하고 skip의 값을 줄인다.
- LIS의 개수가 skip보다 크다면 재귀 호출을 통해 해당 후보군의 원소를 lis_array에 저장한다.
- 위 과정을 거쳐 k번째 LIS를 확인할 수 있다.
- 이 함수는 한 번 호출될 때마다 현재 인덱스 뒤에 있는 숫자들을 모두 한 번 순회하고, 이를 정렬한 뒤 다음 숫자를 정한다.
- 이 중 가장 시간이 오래 걸리는 것은 O(n*logn)이 걸리는 정렬 과정이고, 재귀 호출의 깊이는 LIS의 길이와 같으므로 최대 O(n)이다.
- 따라서, 해당 함수의 시간 복잡도는 O(n^2*logn)이라고 말할 수 있다.
- 별도로 위 함수에서 유의할 점은 별도의 기저 사례가 없다는 점이다.
- 보통은 LIS의 길이가 1이면 곧장 종료하도록 구현할 수 있지만 LIS의 길이가 1이라면 뒤에 올 숫자가 없고 followers 리스트에 아무런 값이 저장되지 않아 함수가 자동으로 종료하게 된다.
2) 문제 입출력
# 문제 입-출력
for tc in range(int(input())):
n, k = map(int, input().split())
S = list(map(int, input().split()))
cacheLen = [-1] * (n + 2) # i번째 인덱스에서 만들 수 있는 LIS의 최대 길이를 저장
cacheCnt = [-1] * (n + 2) # i번째 인덱스에서 만들 수 있는 LIS의 최대 개수를 저장
lis_array = []
reconstruct(-1, k-1, lis_array)
print(len(lis_array))
print(lis_array)
- LIS의 길이를 저장하는 cache와 LIS의 개수를 저장하는 cache, 2 개의 메모이제이션 공간이 필요하다.
- 그리고 k번째 LIS를 저장할 lis_array 리스트를 별도로 만들었다.
4. 후기
갈수록 산 넘어 산이다. 그냥 천천히 여러번 문제를 훑어보는 방법밖에 답이 없다고 생각된다. 그렇게 하면 이해가 된다. 문제를 처음 접할 때 한 2번정도 그리고 블로그에 올리면서 다시 2번 정도해서 적어도 4번 정도는 다시 읽어보긴 하는데 확실히 조금씩 조금씩 이해가 되긴한다. 문제는 이해는 되는데 머릿속에 정리가 잘 안되고, 응용이 안되는게 문제다 ㅋㅋㅋㅋ...
그래도 이 책을 읽으면서 사고의 방법이 달라지고 있다. 기존의 k번째 답안을 찾으라고 한다면 무조건 모든 답을 구해서 정렬 후 k번째 답안을 찾았을 텐데, 이전 문제와 이번 문제에서 소개하는 건너뛰는 방법은 많은 참고자료가 될 것 같다. 그러나 문제가 있다면 큰 그림은 설계할 수 있겠는데 재귀호출을 하는 방법이나 어디서 건너뛰어야 하는지 등의 방법은 고안하기 어렵다고 느껴진다. 특히 재구성하는 함수 같은 경우 유사한 문제들을 많이 풀어야 겠다는 생각이 들었다. 못 풀면 뭐 그냥 블로그에 올린 글들 참고해서 해결하면 어떻게든 되지 않을까?
LeeSeok-Jun/Algorithmic_Problem_Soving_Strategies
프로그래밍 대회에서 배우는 알고리즘 문제해결 전략. Contribute to LeeSeok-Jun/Algorithmic_Problem_Soving_Strategies development by creating an account on GitHub.
github.com
'프로그래밍 대회에서 배우는 알고리즘 문제해결 전략 > 9장_동적 계획법 테크닉' 카테고리의 다른 글
9.11 예제 : 여행하는 외판원 문제(Python)_정수 이외의 입력에 대한 메모이제이션 (0) | 2021.05.13 |
---|---|
9.9 문제 : 드래곤 커브(Python) (0) | 2021.05.10 |
9.6 예제 : 모스 부호 사전(Python) (0) | 2021.05.04 |
9.4 문제 : 광학 문자 인식(Python) (0) | 2021.04.23 |
9.2 문제 : 여행 짐 싸기(Python) (0) | 2021.04.21 |
- Total
- Today
- Yesterday
- 그리디
- 난이도:중
- 마르코프 연쇄
- 탐욕법
- 동적 계획법
- python
- 파이썬
- 비트 마스크
- 카카오
- 종만북
- 프로그래밍 대회에서 배우는 알고리즘 문제해결 전략
- 난이도:하
- 프로그래밍 대회에서 배우는 알고리즘 문제 해결 전략
- k번째 답 계산하기
- 프로그래머스
- 난이도:상
- 프로그래밍 대회에서 배우는 알고리즘 문제해결전략
- 구현
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |