prices의 가격들은 매초 변하는 가격을 뜻하고 가격이 떨어지지 않는 기간은 현재 가격보다 더 낮은 가격이 나온 시간까지 매초(가격의 수) 세면 된다는 뜻이다.
그래서 예시에서 \1은 그 뒤에 \1보다 낮은 시간이 없기 때문에 4초(4개의 가격) 동안 가격이 떨어지지 않는 것이고 마지막 \3은 그 뒤에 남아있는 시간이 없기 때문에 0초가 된 것이다.
단지 지문에서 "가격이 떨어지지 않은 기간은 몇 초인지"라고만 있었기에 이를 헷갈려서 질문에 올린 사람들이 많은 것 같다.
어려운 문제가 아니므로 바로 전체 코드로 넘어가겠다.
def solution(prices):
answer = []
for i in range(len(prices)):
cnt=0
for j in range(i+1,len(prices)):
if prices[i]<=prices[j]:
cnt+=1
else:
cnt+=1
break
answer.append(cnt)
return answer
3번째 줄은 prices의 전체 범위를 탐색하겠다는 것을 뜻하고 5번째 줄은 현재 가격보다 떨어지는 시간이 나올 때까지 남은 시간을 반복하여 세는 반복문이다.
만약 현재 가격보다 낮은 시간이 있다면 그 뒤에 가격은 고려할 필요가 없기에 break를 통해 바로 반복문을 탈출하면 된다.
그렇게 카운트한 값을 answer에 추가하면 된다.
다만 의문인 점은 prices의 길이는 100,000(10만) 이하이고 완전 탐색은 O(n^2)의 시간 복잡도가 걸리므로 1초당 5천만~1억 정도 연산이 가능한 Python을 기준으로 최악의 테스트 케이스가 나온다면 시간 초과가 걸릴 수밖에 없다.
하지만 Input이 작은 정확성까진 이해가 됐으나 상당히 큰 Input이 들어오는 효율성까지 통과된 거로 봐서는 최악의 경우까진 Input이 들어오지 않게 설정한 것 같다.
대문자 하나에 대응하는 소문자 문자열이 정해져 있으므로 Python의 딕셔너리를 사용하면 쉽게 해결할 수 있다.
대응하는 문자들을 딕셔너리에 저장하고 압축된 문자열이 들어왔을 때 차례대로 소문자 문자열로 치환하면 쉽게 풀 수 있는 문제다.
바로 전체 코드로 넘어가겠다.
n=int(input())
word={}
res=""
for i in range(n):
temp=list(input().split())
word[temp[1]]=temp[0]
question=input()
for i in question:
res+=word[i]
num=list(map(int,input().split()))
print(res[num[0]-1:num[1]])
2번째 word는 딕셔너리를 선언했고 for문을 돌면서 입력받은 값을 저장하면 된다.
이때 주의해야 할 점은 압축된 문자열을 풀어야 하므로 입력받은 대문자를 딕셔너리의 키, 소문자 문자열 패턴을 딕셔너리의 값으로 저장해야 한다는 것이다.
res에 치환한 문자열을 저장하고 입력받은 인덱스를 기준으로 출력하면 쉽게 풀 수 있다.
인덱스로 범위 출력 시 왼쪽 인덱스는 포함, 오른쪽 인덱스는 미만까지 출력하는 점을 주의해야 한다.
대기목록에서 꺼낸 문서보다 중요도가 높은 문서가 대기목록에 남아있다면 꺼낸 문서를 다시 대기 목록에 넣는다.
대기 목록에는 최대 100개의 문서가 있다.
중요도는 숫자가 클수록 중요하다.
대기 목록인 priorities의 첫 원소를 차례대로 빼면서 나머지 원소 중 최대 중요도를 비교하여 뺀 문서보다 중요도 높은 문서가 있다면 뒤에 추가하는 형식으로 풀면 될 것이다.
이때 최대 100개의 문서밖에 없으므로 O(n)의 시간 복잡도를 가지는 max 함수로 최대 중요도를 비교하면 된다.
동시에 location에 해당하는 문서가 몇 번째에 출력되는지 묻는 문제이므로 원소를 pop 할 때마다 location을 감소시키고 음수가 되면 대기 목록의 마지막에 추가되었다고 생각하면 된다.
간단한 문제이므로 바로 전체 코드로 넘어가겠다.
def solution(priorities, location):
answer = 0
while len(priorities)>0: #대기 목록에 문서가 있는 동안 반복
temp=priorities.pop(0) #첫 번째 문서 꺼내기
if len(priorities)==0: #껴낸 문서가 마지막 일때 종료
answer+=1
else:
if temp>=max(priorities): #꺼낸 문서의 중요도가 제일 높을 때
if location==0: #찾고자 하는 문서일 때 종료
answer+=1
break
else:
location-=1
answer+=1
#꺼낸 문서보다 중요도가 높은 문서가 대기 목록에 존재할 때
else:
priorities.append(temp)
location-=1
if location<0:
location=len(priorities)-1
return answer
3번째 줄은 대기 목록에 문서가 있는 동안 반복하기 위해 while문을 선언하였고 4번째 줄은 pop(0)을 통해 0번째 인덱스의 원소를 제거하며 반환한다.
또한 5번째 줄은 꺼낸 문서가 마지막 문서일 때 (더 이상 대기 목록에 문서가 없을 때) 고려할 조건이 없으므로 answer을 증가시킨 후 바로 종료한다.
8번째 줄의 if문은 max로 대기 목록의 중요도를 비교하여 더 높은 중요도가 없으면 출력하는 문서로 판단하고 location을 감소시킨다.
이때 location이 0이라면 꺼낸 문서가 찾는 문서이므로 answer을 증가시킨 뒤 break를 통해 반복문을 빠져나온다.
17번째 else는 대기 목록에 더 높은 중요도가 있을 때 뒤에 추가하는 구문으로 만약 location이 음수가 되면 대기 목록의 마지막에 추가했다는 의미이므로 현재 priorities 길이의 -1로 변경하면 된다.
이 문제는 Input Size가 상당히 작아서 굳이 시간 복잡도를 계산할 필요까진 없었던 것 같다.
조건만 잘 확인하고 구현한다면 충분히 쉽게 통과할 수 있을 것이다.
이 프린터 문제는 우선순위 큐란 알고리즘이다.
다만 우선순위 큐는 힙(Heap)이란 자료 구조를 사용하고 내부 구조에서 최댓값, 최솟값을 갱신하는데 O(logn)의 효율적인 시간 복잡도를 가진다.
파이썬은 Heapq란 모듈을 통해 쉽게 우선순위 큐를 구현 가능하며 해당 문제에서는 큐를 연습하기 위해 일부러 Input을 적게 주고 우선순위 큐의 결과를 나타낼 수 있도록 유도한 것 같다.
코테에서도 은근히 우선순위 큐를 사용하는 문제가 나오므로 우선순위 큐를 구현하는 힙에 대해서 포스팅하겠다.
문제를 보자마자 떠올린 생각은 day를 하루씩 증가시키면서 progresses 리스트에 speeds 리스트의 원소를 계속 더하고 progresses의 길이가 0이 될 때까지 같은 날에 0번째 인덱스의 원소가 100 이상이면 pop 하면서 카운트하여 answer에 추가하는 방식이었다.
애초에 작업의 개수가 100개 이하고 작업 진도가 1, 작업 속도가 1일 때 최대 99일이므로 Input이 작아 가능성은 있어 보였다.
하지만 Python의 pop함수에서 0번째 인덱스를 제거하는 pop(0)은 O(n)의 시간 복잡도를 가지므로 원소를 제거하는 과정에서 추가적으로 불필요한 연산이 생기기에 상당히 비효율적인 방식이라 다른 방법을 생각해봤다.
그 결과 굳이 매번 원소들을 더하는 게 아니라 day를 증가시키면서 같은 day일 경우를 카운트하여 answer에 추가하고 기존의 리스트에 대한 변경 없이 현재 어떤 인덱스를 가리키는지 알려주는curr_index를 선언한다면 매번 원소를 제거할 때 드는 시간 복잡도도 없어질 것이라 판단하였다.
전체 코드를 통해 설명하면 이해가 더 빠를 것이다.
def solution(progresses, speeds):
answer = []
day=0 #개발 일수
curr_index=0 #현재 작업 Index
#작업 Index를 0번 부터 끝까지 반복
while curr_index<len(progresses):
#현재 작업이 100% 넘을 때까지 day 증가
while progresses[curr_index]+speeds[curr_index]*day<100:
day+=1
#완료된 작업 개수 카운트
cnt=0
#완료된 작업 카운트
while curr_index<len(progresses) and progresses[curr_index]+speeds[curr_index]*day>=100:
cnt+=1
curr_index+=1
answer.append(cnt)
return answer
7번째 줄을 통해 progresses 리스트를 전체 반복하고 10번째 줄을 통해 현재 작업이 100%가 될 때까지 day를 증가시킨다.
day를 찾으면 같은 날일 때 몇 개의 작업을 배포할 수 있는지 세기 위해 14번째 줄에 cnt를 선언하여 0으로 초기화한다.
초기화된 cnt를 기반으로 17번째 줄에서 배포 가능한 작업들을 세며 curr_index를 증가시킨다.
이때 while문에서 curr_index<len(progresses)를 먼저 선언한 이유는 curr_index가 증가할 때 progresses의 리스트 범위를 벗어나 Index Error를 일으킬 수 있기 때문이다.
프로그래밍 언어에서 조건을 판단할 때앞에 있는 조건이 False라면 뒤에 있는 조건을 확인하지 않기 때문에이처럼 Index Error를 방지하는 조건이나 시간 복잡도를 최대한 줄일 수 있는 조건은되도록이면 앞에 쓰는 습관을 들이자.
마지막으로 카운트한 작업 수를 answer에 추가하면 끝이다.
이렇게 구현한 방식은 리스트의 원소를 제거하는 등 불필요한 연산을 줄였기 때문에 day를 증가시키는 연산(최대 99번), progresses 리스트를 탐색하는 연산(최대 100번)을 하면 최대 약 10,000번의 연산을 한다.
엄밀히 말하자면 큐로 푼 게 아니라 리스트의 인덱스 접근을 통해 풀었고 처음에는 상당히 비효율적인 방식을 생각했었다.
항상 문제의 조건을 정확히 파악하여 자신이 생각한 방식의 시간 복잡도를 계산해보고 보다 더 효율적인 방식이 있는지 끊임없이 고민하는 습관이 상당히 중요한 것 같다.
자료구조(Data Structure)를 공부할 때 가장 처음 접하는 것은 스택(Stack)과 큐(Queue)다.
그전에 자료구조란 Data를 효율적으로 저장하고 접근하여 사용하는 방법을 뜻한다.
적합한 상황에 적절한 자료구조를 사용하면 아주 효율적인 알고리즘을 사용할 수 있는 장점이 있다.
이번 시간에 가장 기본 중에 기본인 스택과 큐에 대해 알아보겠다.
스택 (Stack)
먼저 스택은 나중에 들어오는 Data가 먼저 나가는 LIFO(Last In First Out) 형태이다.
통에다가 블록을 넣고 빼는 방식을 연상하면 이해가 쉬울 것이다.
Data를 넣는 연산은 Push라고 한다.
반대로 Data를 빼는 연산은 Pop이라고 한다.
그림을 통해 쉽게 이해할 수 있듯이 나중에 들어간 Data가 먼저 나오고 먼저 들어간 Data가 나중에 나오는 자료구조를 스택이라고 표현한다.
만약 C 같이 다른 언어였다면 구조체를 선언하여 현재 얼마나 Data가 들어왔는지 판단하는 Top이란 변수를 통해 구현하겠지만 Python은 그럴 필요가 없다.
보통 라이브러리를 사용하는 경우를 제외하면 구현의 편의성을 위해 배열을 미리 선언(정적 할당)하고 잘못된 인덱스로 접근하는 Index Error를 방지하기 위해 Top이란 변수를 선언하여 오류를 방지한다.
또한 필요하다면 배열의 크기를 변경하는 복잡한 과정을 거쳐야 한다.
하지만 Python은 배열이 아닌 리스트를 지원하기 때문에 비교적 Data 추가 삭제에 대한 고민을 덜 수 있다.
큐 (Queue)
다음은 큐다.
큐는 먼저 들어온 Data가 먼저 나가는 FIFO(First In First Out) 형태이다.
즉, 차례대로 줄을 서서 기다리는 형태를 생각하면 된다.
큐에서 Data를 넣는 연산은 Enqueue라고 한다.
Data를 빼는 연산은 Dequeue라고 한다.
스택과는 다르게 먼저 들어간 Data가 먼저 빠지는 것을 확인할 수 있다.
구현
Python은 앞서 말한 대로 배열이 아닌 리스트를 지원하기 때문에 리스트와 관련되어 지원하는 함수들로 아주 쉽게 스택과 큐를 구현 가능하다.
여러 가지 함수들이 있지만 구현하는데 필요한 함수는 append()와 pop()이다.
각각 뒤에 원소를 추가하고 빼는 함수인데 pop(x)처럼 안에 인자를 넣으면 x번째 원소를 반환하고 제거한다는 의미이다.
이를 이용하여 x에 0을 넣으면 0번째 원소를 제거하므로 쉽게 큐를 구현할 수 있다.
다음은 앞서 설명한 과정을 그대로 구현한 전체 코드이다.
stack=[] #스택 선언
print("현재 스택 :",stack)
stack.append(1) # 1 추가
print("현재 스택 :",stack)
stack.append(2) # 2 추가
print("현재 스택 :",stack)
stack.pop() #끝 원소 하나 제거
print("현재 스택 :",stack)
stack.pop() #끝 원소 하나 제거
print("현재 스택 :",stack, end="\n\n")
queue=[] #큐 선언
print("현재 큐 :",queue)
queue.append(1) # 1 추가
print("현재 큐 :",queue)
queue.append(2) # 2 추가
print("현재 큐 :",queue)
queue.pop(0) #첫 원소 하나 제거
print("현재 큐 :",queue)
queue.pop(0) #첫 원소 하나 제거
print("현재 큐 :",queue)
복잡한 과정 없이 함수 두 개로 간단하게 구현하였으며 보통 그래프 탐색에서 스택은 깊이 우선 탐색(DFS), 큐는 너비 우선 탐색(BFS)에 자주 쓰인다.
따라서 코테준비를 위해 상당히 자주 사용할 문법이므로 알아야 한다.
데크 (Deque)
사실 큐는 구현 방법에 따라 다양한 이름이 있는데 가장 단순한 큐(Queue), 배열의 비효율성을 개선하기 위한 원형 큐(Circular Queue), 앞과 뒤 둘 다 Data를 넣고 뺄 수 있는 데크(Deque), 자료구조 중 하나인 힙(Heap)을 이용한 우선순위 큐(Priority Queue) 이런 식으로 장단점을 개선한 방식이 존재한다.
Python의 특성 상 원형 큐는 구현할 필요가 없고 우선순위 큐는 파이썬의 모듈 중 하나인 heapq를 사용하면 된다.
이번에 추가적으로 설명할 자료구조는 데크다.
데크 혹은 덱이라 부르는데 이 자료구조는 앞과 뒤에서 Enqueue와 Dequeue가 가능하다.
파이썬의 pop()은 O(1)의 시간 복잡도가 들지만 첫 번째 원소를 제거하기 위해 pop(0)을 사용하게 되면 끝에서부터 원소를 찾기에 O(n)의 시간 복잡도가 걸린다.
따라서 Input이 너무 큰 상황이라면 효율을 위해 데크를 사용하여 시간 복잡도를 줄일 수 있다.
주요 함수는 append(), appendleft(), pop(), popleft()이고 left가 붙으면 첫 번째에, 붙지 않으면 마지막에 적용된다.
데크 구현
다음은 데크의 연산 과정을 확인할 수 있는 전체 코드이다.
from collections import deque #데크 모듈 선언
deque=deque()
print("현재 데크 :",deque)
deque.append(1) #오른쪽에 1 추가
print("현재 데크 :",deque)
deque.appendleft(2) #왼쪽에 2 추가
print("현재 데크 :",deque)
deque.popleft() #첫 번째 원소 제거
print("현재 데크 :",deque)
deque.pop() #끝 원소 제거
print("현재 데크 :",deque)
처음 보면 모듈 선언이 다소 어려울 수 있겠지만 익숙해진다면 그냥 리스트로 구현하는 것보다 시간 복잡도적으로 유리한 구현이 가능해진다.