문제
너비가 같은 N개의 나무 판자를 붙여 세운 울타리가 있습니다. 시간이 지남에 따라 판자들이 부러지거나 망가져 높이가 다 달라진 관계로 울타리를 통째로 교체하기로 했습니다. 이 때 버리는 울타리의 일부를 직사각형으로 잘라내 재활용하고 싶습니다. 그림 (b)는 (a)의 울타리에서 잘라낼 수 있는 많은 직사각형 중 가장 넓은 직사각형을 보여줍니다. 울타리를 구성하는 각 판자의 높이가 주어질 때, 잘라낼 수 있는 직사각형의 최대 크기를 계산하는 프로그램을 작성하세요. 단 (c)처럼 직사각형을 비스듬히 잘라낼 수는 없습니다.
판자의 너비는 모두 1이라고 가정합니다.
입력
첫 줄에 테스트 케이스의 개수 C (C≤50)가 주어집니다. 각 테스트 케이스의 첫 줄에는 판자의 수 N (1≤N≤20000)이 주어집니다. 그 다음 줄에는 N개의 정수로 왼쪽부터 각 판자의 높이가 순서대로 주어집니다. 높이는 모두 10,000 이하의 음이 아닌 정수입니다.
출력
각 테스트 케이스당 정수 하나를 한 줄에 출력합니다. 이 정수는 주어진 울타리에서 잘라낼 수 있는 최대 직사각형의 크기를 나타내야 합니다.
예제 입력
3 7 7 1 5 9 6 7 3 7 1 4 4 4 4 1 1 4 1 8 2 2
예제 출력
20 16 8
분할정복 풀이
[접근방식]
- 가장 단순하게 푸는 방법? ⇒ 울타리 하나씩 돌아가며 기준으로 잡고 전체 울타리 탐색(완전탐색)
- 완전탐색 가능? ⇒ O(N^2)라 불가능
- 풀이기법 ⇒ 분할정복
- 분할정복에 쓰인 아이디어⇒ 가장 큰 사각형은 3가지 경우가 있음 1) 왼쪽에서 가장 큰 것, 2) 오른쪽에서 가장 큰 것, 3) 겹치는 것 중 가장 큰 것
- 3)번의 경우에서 중앙의 2개를 포함하게 되어있음
- 1)과 2)의 경우 재귀로 넘겨주고 3)의 기저조건은 울타리가 하나일 때로 설정
[어려웠던 점]
- 분할정복 자체를 생각해내기가 어려웠음
- 나는 1), 2), 3)을 각각의 함수로 생각했고 1)과 2)를 재귀로 넘겨준다는 것을 생각하지 못했음
- 3)의 경우에 중앙의 2개를 무조건 가지게 된다는 조건에 대해 잘 생각하지 못했던 것 같음
[분할정복 풀이코드(책 코드 참조)]
#입력값
n = 7
h = [1, 4, 4, 4, 4, 1, 1]
def dfs(left, right):
global h
if left == right: return h[left]
mid = (left+right)//2
lo = mid
hi = mid+1
ret = max(dfs(left, lo), dfs(hi, right))
height = min(h[lo], h[hi])
ret = max(ret, height*2)
while left < lo or hi < right:
if hi<right and (h[lo-1]<h[hi+1] or lo == left):
hi += 1
height = min(height, h[hi])
else:
lo -= 1
height = min(height, h[lo])
ret = max(ret, height*(hi-lo+1))
return ret
print(dfs(0, n-1))
스위핑 풀이
[스위핑이란?]
스위핑 알고리즘이란, 이미 연산이 진행된 이전 공간에 대해서는 연산을 하지않는다는 개념
[접근방식]
- 사각형을 구하기 위해 필요한 정보는 해당 사각형의 왼쪽 끝과 오른쪽 끝 그리고 높이임
- 높이가 오른쪽 끝점의 높이라고 했을 때, 왼쪽으로 연속된 판자들 중 이 높이를 포함하는 가장 낮은 지점이 왼쪽 끝점이 될 것
- 위의 사항을 이용하기 위해 index(폭 구할 때 이용)를 stack에 저장하고 stack에서 빼내면서 폭을 구하고 해당 index로 높이 정보에 접근도 가능
- index를 꺼내는 시점의 경우, 현재 울타리 높이가 stack의 마지막 울타리 높이보다 낮을 때 현 울타리 보다 낮은 울타리 전까지 하나씩 빼면서 넓이를 업데이트 해주면 됨
- 단, 스텍에서 뺄 때, 스택이 비어있는지 확인이 필요함
- 추가적으로 마지막 울타리의 높이가 굉장히 높을 경우가 있으므로 울타리 높이 배열에 0을 추가해주면 이 경우 처리 가능
- 해당 문제를 푸는 알고리즘 요약 (O(N) 풀이 가능)
[(참고) 스텍 생성 울타리를 저장하는 배열에 0 추가 0부터 n까지(n+1 번) 다음을 순회]
- 스텍이 비지 않고 현재 스택에 가장 나중에 저장된 위치의 울타리 높이가 현 울타리 높이 이상일때까지 다음을 반복 a. 스텍에 가장 나중에 저장된 위치 가져옴 b. 너비는 스텍이 비었을 땐 현재위치 i를, 비지 않았다면 i-다음으로 나중에 저장된 위치-1 c. 넓이를 구하고 최대값 갱신
- 현재 idx를 스텍에 넣어줌
[어려웠던 점]
- 스텍까지는 떠올렸었는데, 울타리의 idx를 스텍에 넣는다는 것을 떠올리지 못해서 풀이로 연결시키기 어려웠던 것 같다.
- 스텍을 넣고 빼고하는 지점의 조건에 대해 정당성을 입증하기가 어려웠던 것 같다. 정작 그림을 그려가며 생각해보니, 모든 조건에서 정상작동하였다.
- 울타리의 idx가 0부터 시작한다는 점에서 너비의 조건이 i 혹은 i-다음으로 나중에 저장된 위치-1가 된다는 것을 잘 연결시키지 못했다.
[느낀 점]
- 분할정복 보다 스위핑이 시간복잡도도 낮은데다, 스위핑은 재귀를 부르지 않기 때문에 이해만 잘 한다면 스위핑이 훨씬 간단하고도 강력한 알고리즘이 될 것이다.
- 앞으로 스위핑과 스텍을 이용하는 방법에 대해 항상 유념하고 비슷한 문제를 만나면 잘 활용해봐야겠다.
[스위핑 풀이코드]
#입력값
n = 7
h = [1, 4, 4, 4, 4, 1, 1]
#마지막 울타리 처리를 위해서 0높이의 울타리 추가
h.append(0)
#울타리 idx 담을 stack
stack = []
#업데이트할 결과값
ret = 0
for i in range(n+1):
#현재 울타리가 stack의 마지막 울타리보다 낮으면
#현 울타리보다 작은 울타리 나오기 전까지 뺴주면서 넓이 업데이트
while stack and h[stack[-1]] >= h[i]:
#높이를 stack 마지막걸로 업데이트해가며 진행
height = h[stack[-1]]
stack.pop()
#stack이 비었다면(idx가 0부터 시작)
if not stack:
width = i
#stack이 남아있으면(idx가 0부터 시작)
else:
width = i-stack[-1]-1
#넓이 업데이트
ret = max(ret, height*width)
#stack에 넣어주기
stack.append(i)
print(ret)
참고출처 https://gurumee92.tistory.com/90 https://doctcoder.tistory.com/27
'알고리즘 문제(Python) > 알고스팟(feat.알고리즘문제해결전략)' 카테고리의 다른 글
[알고스팟/종만북/DP/정규식] 와일드카드 (0) | 2022.11.06 |
---|---|
[알고스팟/종만북/DP/DFS] 외발뛰기(Python) (0) | 2022.11.06 |
[알고스팟/종만북/완전탐색/재귀] 시계 맞추기(Python) (0) | 2022.11.06 |
[알고스팟/종만북/완전탐색/재귀] 게임판 덮기(Python) (0) | 2022.11.06 |
[알고스팟/종만북/완전탐색/재귀] 소풍(Python) (0) | 2022.11.06 |