문제
수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오.
예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {10, 20, 10, 30, 20, 50} 이고, 길이는 4이다.
입력
첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000)이 주어진다.
둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (1 ≤ Ai ≤ 1,000)
출력
첫째 줄에 수열 A의 가장 긴 증가하는 부분 수열의 길이를 출력한다.
예제 입력 1
6
10 20 10 30 20 50
예제 출력 1
4
LIS! Longest Increasing Subsequence로 뜻은 가장 긴 증가 부분수열이다.
별거 없다. 다음 숫자가 자신보다 크거나 같으면 된다.
여기서 두가지로 나뉜다.
다음 숫자가 자신보다 크거나 같으면, 단조 증가 (1,1,2,2,3,4)
다음 숫자가 자신보다 무조건 크면, 순증가 (1,2,3,4)
피보나치 수열이 초반 (1,1,2,3,5~~~)로 증가하니 이는 단조증가에 해당한다.
이 LIS문제는 다이나믹 프로그래밍에서 유명한 문제이다.
그렇다면 보통 재귀함수로도 풀 수 있다는 소리.
하지만 그러면 2^n이 되므로 시간복잡도가 증가한다.
그렇기에 다이나믹 프로그래밍으로 풀으란 소리인 것 같다.
가장 기초적인 LIS 문제이므로 여기저기 검색하면 많이 나온다.
그 중 https://shoark7.github.io/programming/algorithm/3-LIS-algorithms 이분의 설명이 젤 잘되어 있는 것 같다.
또한 알고리즘 측면으로는 https://namu.wiki/w/%EC%B5%9C%EC%9E%A5%20%EC%A6%9D%EA%B0%80%20%EB%B6%80%EB%B6%84%20%EC%88%98%EC%97%B4#s-3.2
나무 위키도 괜찮다.
자 이제 문제를 해결해보자.
10, 20 10, 30, 20, 50 이다.
리스트 내부를 거칠 때마다 cnt해주는 방식으로 진행하면 된다.
1. 숫자를 시작할 때마다 cnt=1을 놔준다.
10 | 20 | 10 | 30 | 20 | 50 |
1 |
2. 다음 숫자로 넘어가서 for문을 돌려 만약 이전 숫자들보다 크면, 이전 숫자의 cnt+1과 현재 cnt=1 중 가장 큰 것을 현재 cnt에 넣어준다.
10 | 20 | 10 | 30 | 20 | 50 |
1 | 2 |
3. 다음은 이전 숫자들보다 크지 않으므로 이번 인덱스에서 시작햇을 때 cnt=1한 것을 그대로 놔둔다.
10 | 20 | 10 | 30 | 20 | 50 |
1 | 2 | 1 |
4. 30인 숫자로 넘어왔다. for문을 돌려 이전 숫자들보다 큰지 세어보자.
인덱스 0일 때, 숫자는 10이므로 30이 더 크다. 그러므로 10의 cnt+1과 현재 cnt=1 중, 10의 cnt+1이 더 크므로(2) 현재 cnt는 2가 된다. 다음으로 숫자 20을 보자. 숫자 20보다 30이 더 크다! 그러면 또 20의 cnt+1=3, 현재 cnt=2가 되므로 현재 cnt에 3을 넣어준다. 다음 숫자인 10. 30이 더 크다. 10의 cnt+1=2, 현재 cnt=3이므로 현재 cnt=3이 그대로 남는다.
10 | 20 | 10 | 30 | 20 | 50 |
1 | 2 | 1 | 3 |
5.6. 이런식으로 진행하다보면 리스트의 끝까지 왔을 때 모든 cnt가 채워진다.
10 | 20 | 10 | 30 | 20 | 50 |
1 | 2 | 1 | 3 | 2 | 4 |
최종으로 cnt중 가장 큰 값을 찾아서 출력해주면 이 수열의 LIS의 길이는 4가 된다.
N=int(input())
lst=list(map(int, input().split()))
cnt_lst=[0]*N
for i in range(N):
cnt_lst[i]=1
for j in range(i):
if lst[j]<lst[i]:
cnt_lst[i]=max(cnt_lst[j]+1,cnt_lst[i])
print(max(cnt_lst))
자 그런데 여기서 이 코드의 시간 복잡도는 N^2이다.
왜냐면 N의 for문을 N까지 for문을 한번 더 돌렸기 때문읻자. 그러면 N^2이 되니깐.... 이것은 완전탐색이다.
그렇다면 약간 이 코드를 수정해서 for문을 한번만 돌리면서 자신보다 숫자가 크면 넣고, 작으면 작은 곳에 넣어주는 방식으로 바꾸면 되지 않을까. == 라는 생각을 알고리즘 고인물들이 했다.
바로 이진탐색이다.ㄷㄷ
아까 나무위키의 맨 밑 방법이다.
이 방법을 매우! 쉽게! 알고리즘화하신 분이 있었다.
https://claude-u.tistory.com/442 똑똑한 사람은 너무 많아ㅜ
이 분의 코드를 써봤다.
N=int(input())
lst=list(map(int,input().split()))
dp=[]
from bisect import bisect_left
for i in lst:
k=bisect_left(dp,i)
print(k)
if len(dp)<=k:
dp.append(i)
else :
dp[k]=i
print(dp)
코드에 bisect라는 이진탐색 라이브러리를 사용하셨다. bisect는 이진탐색을 이미 라이브러리로 쫘놓은 함수인데,
bisect_left, bisect_right 함수들은 인자로 리스트와 찾을 값을 넣어준다. 그러면 아웃풋으로 그 값에 대한 인덱스 값을 찾아주는 함수이다.
예를 들어 bisect_left([1,2,3,4],2) 인 경우 [1,2,3,4]에서 2인 인덱스를 이진탐색으로 찾아준다. 여기서 2의 인덱스는 1이다.
하지만 bisect_right인 경우 2를 반환해준다. bisect_right의 경우 찾은 값의 다음 인덱스를 반환해준다.
여기서 난 착각한게 있었다. bisect_left([3,5,7,9],4)를 찾을 때 당연히 인덱스는 0일 줄 알았다. 3과 5사이이고 왼쪽 인덱스를 반환하니깐... 이라고 잘못 생각했다.
아니다. bisect_left든 bisect_right든 그 사이의 값을 찾을 땐, 무조건 다음 인덱스를 찾아준다. 즉 3과 5의 인덱스인 0과 1 중 1을 반환해준다는 소리이다.
그렇게 위의 코드에
3, 5, 7, 9, 2, 1, 4, 8 을 넣었을 때 결과로 1,4,7,8이 나오게 된다!(나무위키의 결과값과 같다!)
이진탐색은 logN의 시간복잡도를 가진다. 여기서 for문으로 N을 한번 돌렸으므로, NlogN이 시간복잡도가 된다.
아까의 완전탐색인 N^2보다 훨씬 줄어든 것을 볼 수 있다.
'알고리즘 > 함수 in python' 카테고리의 다른 글
stack, queue, deque in python (0) | 2021.11.12 |
---|---|
[백준 3036번] (Fraction)분수 표현 in python (0) | 2021.11.10 |
from itertools import 순열, 조합, 곱집합, 중복조합 in python (0) | 2021.11.02 |
[백준 2108번] (Counter)통계학(평균, 중앙값, 최빈값, 범위) in python (0) | 2021.11.01 |
int형 숫자를 0을 앞에 붙인 문자열로 변경 in python (0) | 2021.10.27 |
댓글