Notion에서 작성한 글이라, 여기에서 더 깔끔하게 보실 수 있습니다! 😮😊
[Minimum Operations to Make a Subsequence]
target
을 arr
의 부분 수열로 만들까? 고민해 보니,target
의 원소들 중, arr
와 target
의 최장 부분 공통 수열(이하 LCS; Longest Common Subsequence)을 이루지 않는 원소들에 대해서 arr
의 적절한 위치에 삽입해주면 되겠다고 생각했다.target
의 가능한 한 많은 원소들이 arr
의 부분 수열이어야 삽입할 원소의 개수가 최소가 되기 때문이다.len(target)-len(LCS(target, arr))
이 답일 것이라고 생각했으나, Constraints에 따르면 target
, arr
의 길이는 최대 기 때문에 일반적으로 LCS를 구하는 방법인 으로는 TLE가 날 것이 당연했지만, 로직이 맞는지 확인해 보자는 생각으로 일단 코드를 짜 봤다.def LCS(A, B):
n, m = len(A), len(B)
dp = [[0]*(m+1) for _ in range(n+1)]
for i in range(1, n+1):
for j in range(1, m+1):
dp[i][j] = dp[i-1][j-1]+1 if A[i-1] == B[j-1] else max(dp[i-1][j], dp[i][j-1])
return dp[-1][-1]
def minOperations(target, arr):
return len(target)-LCS(target, arr)
target
that consists of distinct integers 가 분명 무슨 의미가 있을 것 같아 보였는데, 어떻게 이를 이용할지도 감이 잡히질 않았다.target
이 distinct integers로 구성되어 있다고 해도 이분 탐색을 적용할 수 없는데 왜 굳이 distinct일까? 일단 이분 탐색을 사용할 수 있도록 정렬을 해 볼까? 라는 생각을 했고,target
을 정렬해 놓은 다음, arr
의 각 원소를 순회하면서 target
의 어느 위치(인덱스)에 해당 원소가 존재하는지를 새로운 수열 seq
에 기록해 놓으면, 상대적인 순서가 기록이 되는 것이나 마찬가지이므로 이 값들이 증가하는 부분 수열, 즉 최장 증가 부분 수열(이하 LIS; Longest Increasing Subsequence)의 길이를 구한다면 이것이 곧 LCS의 길이라는 생각이 들어 바로 코드를 짰다.def bs_lb(a, v):
lo, hi = 0, len(a)-1
while lo <= hi:
mid = (lo+hi)//2
if a[mid] < v:
lo = mid+1
else:
hi = mid-1
return lo
def bs_lb_2(a, v):
lo, hi = 0, len(a)-1
while lo <= hi:
mid = (lo+hi)//2
if a[mid][0] < v:
lo = mid+1
else:
hi = mid-1
return lo
def LIS(seq): # O(n lg n)
dp = []
for a in seq:
i = bs_lb(dp, a)
dp[i:i+1] = a,
return len(dp)
def minOperations(target, arr):
n, m = len(target), len(arr)
target = sorted((v, i) for i, v in enumerate(target)) # O(n lg n)
seq = []
for i in range(m): # O(n lg n)
lo = bs_lb_2(target, arr[i])
if lo < n and target[lo][0] == arr[i]: seq.append(target[lo][1])
return n-LIS(seq)
target
내에서 위치할 인덱스를 찾고, 그 인덱스에 해당 값이 존재하는 것이 arr
의 원소가 target
에 속한다는 것을 의미하므로 이 경우에 대해서만 seq
에 append
한다.bs_lb_2
함수는 리스트 내 튜플의 첫 번째 원소에 대해 이진 탐색을 하는 함수이다.target
을 정렬해버리면 기존 원소들의 상대적인 순서가 변하기 때문에 올바른 LCS의 길이를 알아낼 수 없다는 점을 간과했다.enumerate()
를 이용하여 정렬 전 기존 인덱스 정보를 같이 묶어준 다음 정렬하여 풀 수 있었다.target
이 distinct integer로 구성되어 있다는 점을 이용하지 않는 풀이이다. 이 때 잊고 있었던 Topic을 보니, Hash Table이 있었다. 헉… 😮def bs_lb(a, v):
lo, hi = 0, len(a)-1
while lo <= hi:
mid = (lo+hi)//2
if a[mid] < v:
lo = mid+1
else:
hi = mid-1
return lo
def LIS(seq):
dp = []
for a in seq:
i = bs_lb(dp, a)
dp[i:i+1] = a,
return len(dp)
def minOperations(target, arr):
idx = {v:i for i, v in enumerate(target)}
seq = [idx[a] for a in arr if a in idx]
return len(target)-LIS(seq)
target
의 기존 값과 인덱스를 에 딕셔너리로 만들어 놓으면 새로운 수열 또한 Amortized 에 구성할 수 있게 된다.bisect
from bisect import bisect_left
def LIS(seq):
dp = []
for a in seq:
i = bisect_left(dp, a)
dp[i:i+1] = a,
return len(dp)
def minOperations(target, arr):
idx = {v:i for i, v in enumerate(target)}
seq = [idx[a] for a in arr if a in idx]
return len(target)-LIS(seq)
Memo