Largest Rectangle in Histogram
히스토그램은 간격 없이 나란히 놓인 막대들의 행이며, 각 막대의 너비는 1단위입니다. heights[i]는 i번째 막대의 높이입니다. 히스토그램 안의 직사각형은 서로 이웃한 막대들을 연속해서 덮으며, 그 연속된 막대 중 가장 낮은 막대보다 높을 수 없습니다.
이러한 직사각형이 가질 수 있는 가장 큰 넓이를 반환하세요.
함수
- heightsinteger-array
- 각 막대의 높이(왼쪽에서 오른쪽으로)
- 반환값integer
- 히스토그램에 들어가는 가장 큰 직사각형의 넓이
제약 조건
1 ≤ heights.length ≤ 2 × 1040 ≤ heights[i] ≤ 105- 각 막대의 너비는 1단위이므로,
i부터j까지의 막대 위에 있는 직사각형의 너비는j-i+1단위입니다.
예제
- 입력
- heights = [2, 5, 6, 3, 4, 1]
- 출력
- 12
- 설명
- 막대 5, 6, 3, 4는 모두 높이가 3 이상이므로, 높이가 3인 직사각형이 이 막대들을 가로지릅니다: 3 × 4 = 12. 가장 높은 두 막대인 5와 6만으로는 5 × 2 = 10입니다.
- 입력
- heights = [1, 8, 1, 1]
- 출력
- 8
- 설명
- 8의 막대만으로는 8 × 1 = 8입니다. 더 넓은 직사각형에는 모두 1의 막대가 포함되므로, 최대 1 × 4 = 4입니다.
- 입력
- heights = [3, 3, 3, 3]
- 출력
- 12
- 설명
- 네 개의 막대는 모두 높이가 3이므로, 히스토그램 전체는 하나의 직사각형입니다: 3 × 4 = 12.
제출 시 숨은 테스트 +17개
후속 질문
각 막대의 너비가 두 번째 배열에 주어진다고 가정해 보세요. 한 번의 순회로 해결하는 스택 풀이에서 무엇이 달라질까요?
힌트
하나씩 열어 보세요. 열 때마다 조금 더 알려 줍니다.
가장 큰 직사각형은 그 아래에 있는 막대 중 적어도 하나의 꼭대기에 닿습니다. 그렇지 않다면 직사각형을 더 높게 만들 수 있습니다. 그러므로 각 막대가 높이를 정하는 막대라고 가정해 보세요. 정확히 그 높이인 직사각형의 너비는 최대 얼마까지 될까요?
막대
i와 높이가 같은 직사각형은 양쪽에서 더 낮은 막대를 만날 때까지 왼쪽과 오른쪽으로 뻗어 나갑니다. 각 막대의 양쪽에서 가장 가까운 더 낮은 막대를 알고 있다면, 각 막대가 후보 넓이 하나를 제공하므로 후보는 모두n개뿐입니다.- 높이가 아래에서 위로 갈수록 증가하는 인덱스 스택을 유지합니다. 맨 위 막대보다 높지 않은 막대가 나타나면 맨 위 막대는 더 오른쪽까지 뻗을 수 없습니다. 이를 꺼내면 해당 막대의 직사각형은 스택의 새로운 맨 위와 현재 막대 사이에 있는 막대들을 엄격히 포함합니다. 끝에 높이가 0인 막대를 추가하면 남아 있는 막대가 모두 꺼내집니다.
풀이
직사각형은 어떤 막대에서든 시작하고 끝날 수 있으며, 높이는 직사각형이 포함하는 막대 중 가장 낮은 막대에 따라 달라집니다. 따라서 막대의 모든 연속 구간을 시도하면 약 n²/2단계가 걸립니다. 이를 해결하려면 질문을 뒤집어 생각하면 됩니다. 가장 큰 직사각형의 높이는 그 직사각형에 포함된 막대 중 하나의 높이와 정확히 같으므로, 각 막대는 더 짧은 막대가 막기 전까지 얼마나 멀리 뻗을 수 있는지만 알면 됩니다. 단조 스택을 사용하면 먼저 두 번의 순회로, 그다음 한 번의 순회로 각 막대의 경계 위치를 찾을 수 있습니다.
현재까지의 최솟값을 유지하면서 모든 실행을 시도하세요
정답이지만 가장 큰 테스트에서는 끝나지 않음
핵심 아이디어
직사각형은 start부터 end까지 이어진 막대들의 구간을 덮으며, 높이는 구간에서 가장 낮은 막대의 높이로 제한됩니다. 그러니 모든 구간을 시도해 보세요. start를 고정한 다음, end를 한 번에 막대 하나씩 늘리고 지금까지 확인한 가장 낮은 높이를 계속 기록하세요. 해당 구간에서 가장 넓은 직사각형의 넓이는 lowest × (end-start+1)입니다.
[2, 5, 6, 3, 4, 1]에서 5부터 시작해 보세요. 구간별 넓이는 5 × 1 = 5이고, 6을 포함하면 5 × 2 = 10입니다. 그다음 3이 포함되면 3 × 3 = 9, 4를 포함하면 3 × 4 = 12, 1을 포함하면 1 × 5 = 5가 됩니다. 정답은 12입니다. 구간이 늘어날 때마다 lowest를 갱신하면 각 단계가 O(1)에 처리되므로, 최솟값을 찾기 위해 구간을 다시 훑을 필요가 없습니다.
모든 직사각형은 어떤 구간을 덮으며, 고정된 구간에 들어갈 수 있는 가장 높은 직사각형의 높이는 정확히 가장 낮은 막대의 높이와 같으므로 이 방법은 정확합니다. 하지만 n(n+1)/2개의 구간을 모두 살펴보므로 느립니다. 막대가 2 × 10^4개라면 약 2 × 10^8개이며, 이 개수는 막대의 높이와는 전혀 관계가 없습니다. 대부분의 구간은 끝에 도달하기 훨씬 전에 낮은 막대 때문에 더 이상 늘릴 수 없지만, 무차별 대입 방식은 그래도 계속 구간을 늘립니다.
알고리즘
best를 0으로 설정합니다.- 각
start에 대해lowest를heights[start]로 설정합니다. start부터 마지막 막대까지 각end에 대해, 해당 막대가 더 낮으면lowest를heights[end]로 낮춥니다.lowest × (end-start+1)로best를 갱신합니다.best를 반환합니다.
def largestRectangleArea(heights):
n = len(heights)
best = 0
for start in range(n):
lowest = heights[start]
for end in range(start, n):
# The rectangle over start..end is as tall as the lowest bar in it.
lowest = min(lowest, heights[end])
best = max(best, lowest * (end - start + 1))
return best양쪽에서 가장 가까운 더 짧은 막대
핵심 아이디어
검색 방향을 바꿔 보세요. 가장 큰 직사각형에서는 그 아래에 있는 막대 중 적어도 하나의 높이가 직사각형의 높이와 정확히 같습니다. 그렇지 않다면 직사각형을 더 높일 수 있습니다. 따라서 답은 모든 막대 i에 대해 높이가 정확히 heights[i]이고 가능한 한 넓게 펼쳐지는 직사각형 중 최댓값입니다. 양쪽에서 높이가 더 낮은 막대를 만날 때까지 펼쳐집니다. 그 인덱스를 left[i]와 right[i]라고 하며, 그런 막대가 없으면 -1과 n을 사용합니다. 직사각형은 그 사이에 있는 막대만 포함하므로 너비는 right[i]-left[i]-1입니다. 이렇게 하면 후보가 n²/2개가 아니라 n개입니다.
모든 막대에 대해 left[i]를 찾으려면, 스택 아래쪽에서 위쪽으로 갈수록 높이가 엄격히 증가하도록 인덱스를 쌓으면서 왼쪽에서 오른쪽으로 훑습니다. 막대 i가 오면 그 막대의 높이가 heights[i] 이상인 인덱스를 모두 꺼냅니다. 그 막대들은 i의 가장 가까운 더 낮은 막대일 수 없고, 그 뒤에 오는 어떤 막대의 가장 가까운 더 낮은 막대일 수도 없습니다. i가 더 가깝고 높이도 더 낮지 않기 때문입니다. 맨 위에 남은 항목이 왼쪽에서 가장 가까운 더 낮은 막대입니다. 그런 다음 i를 넣습니다. 오른쪽에서 왼쪽으로 같은 과정을 수행하면 right[i]를 구할 수 있습니다.
[2, 5, 6, 3, 4, 1]의 경우 두 번의 탐색 결과는 left = [-1, 0, 1, 0, 3, -1]과 right = [5, 3, 3, 5, 5, 6]입니다. 인덱스 3에 있는 높이 3의 막대는 인덱스 0의 높이 2인 막대와 인덱스 5의 높이 1인 막대에 막히므로, 그 직사각형의 넓이는 3 × (5-0-1) = 12입니다. 높이 6인 막대는 양옆의 막대에 막혀 6 × 1만 만들 수 있습니다.
각 인덱스는 각 탐색에서 한 번씩만 스택에 들어가고 최대 한 번만 꺼내지므로, 막대 하나가 여러 인덱스를 꺼내더라도 두 탐색 모두 O(n)입니다. 추가 배열 두 개가 필요합니다.
알고리즘
- 스택을 비운 상태로 왼쪽에서 오른쪽으로 이동합니다. 각
i에 대해 스택 맨 위의 막대 높이가heights[i]이상인 동안 꺼냅니다. 스택이 비어 있으면left[i]를 -1로 설정하고, 그렇지 않으면 맨 위 값을 설정한 다음i를 넣습니다. - 같은 방식으로 오른쪽에서 왼쪽으로 이동하며, 스택이 비어 있는 경우
n을 사용해right[i]를 채웁니다. - 모든
i에 대해heights[i] × (right[i]-left[i]-1)을 계산합니다. - 그 면적 중 가장 큰 값을 반환합니다.
def largestRectangleArea(heights):
n = len(heights)
left = [-1] * n # index of the nearest shorter bar on the left, or -1
right = [n] * n # index of the nearest shorter bar on the right, or n
stack = [] # indices whose heights rise strictly from bottom to top
for i in range(n):
while stack and heights[stack[-1]] >= heights[i]:
stack.pop()
if stack:
left[i] = stack[-1]
stack.append(i)
stack = []
for i in range(n - 1, -1, -1):
while stack and heights[stack[-1]] >= heights[i]:
stack.pop()
if stack:
right[i] = stack[-1]
stack.append(i)
best = 0
for i in range(n):
# Bar i is the lowest bar of everything strictly between left[i] and right[i].
best = max(best, heights[i] * (right[i] - left[i] - 1))
return best단조 스택으로 한 번 순회하기
핵심 아이디어
왼쪽에서 오른쪽으로 한 번 훑으면 이미 모든 오른쪽 경계를 확인하지만, 이를 버립니다. 막대 i가 막대 t를 팝할 때 heights[i]는 heights[t]보다 크지 않으므로, i는 t의 직사각형이 오른쪽에서 끝나는 위치입니다. 그리고 스택에서 t 바로 아래에 남은 인덱스는 직사각형이 왼쪽에서 끝나는 위치입니다. 그러므로 팝하는 순간 직사각형의 크기를 계산합니다. heights[t] × (i - below - 1)이며, 여기서 below는 스택의 새로운 맨 위 항목이고, 스택이 이제 비었다면 -1입니다.
불변 조건: 스택의 높이는 아래에서 위로 엄격하게 증가하며, 각 항목 바로 아래의 인덱스는 그 항목보다 낮은 가장 가까운 왼쪽 막대입니다. 두 막대 사이의 모든 막대는 그 항목 자체에 의해 또는 나중에 그 항목이 팝한 막대에 의해 지나가는 동안 팝되었으므로, 그중 해당 항목보다 낮은 막대는 없습니다. 끝내 팝되지 않는 막대는 맨 끝까지 이어지므로, 마지막 막대를 처리한 뒤 높이가 0인 막대를 하나 더 처리합니다. 이 막대는 모든 막대보다 낮으므로 스택을 비웁니다.
[2, 5, 6, 3, 4, 1]을 따라가 봅시다. 2, 5, 6을 푸시하면 스택에는 인덱스 [0, 1, 2]가 들어 있습니다. 인덱스 3의 3은 6을 팝하고(넓이 6 × (3-1-1) = 6), 이어서 5를 팝한 다음(넓이 5 × (3-0-1) = 10), 2에서 멈추고 푸시됩니다. 4를 푸시합니다. 인덱스 5의 1은 4를 팝하고(넓이 4), 이어서 3을 팝합니다. 이 직사각형은 인덱스 1부터 4까지 이어집니다. 3 × (5-0-1) = 12입니다. 2도 팝합니다(2 × 5 = 10. 스택이 비었으므로 너비는 5입니다). 끝맺는 0은 1을 팝합니다(1 × 6 = 6). 최댓값은 12입니다.
>=일 때 팝하면 높이가 같은 막대가 다른 막대를 일찍 멈추게 할 수 있습니다. 이는 안전합니다. 높이가 같은 막대가 스택에서 그 자리를 차지하고 같은 왼쪽 경계를 이어받으며, 나중에 팝될 때 전체 구간을 덮는 직사각형을 만듭니다. [3, 3, 3, 3]에서는 처음 세 개의 3이 너비 1, 2, 3을 기록하고, 마지막 3은 끝맺는 0에 의해 팝되면서 너비 4를 얻으므로, 넓이는 12입니다.
알고리즘
- 빈 인덱스 스택으로 시작하고
best = 0으로 설정합니다. - 0부터
n까지i에 대해 현재 높이를heights[i]로 설정하고,i = n일 때는 0으로 설정합니다. - 스택 맨 위의 막대가 현재 높이보다 크거나 같은 동안, 해당 막대를
t로 꺼냅니다. 너비는i - below - 1이며,below는 새로운 맨 위 값이거나 -1입니다.heights[t] × width로best를 업데이트합니다. i를 푸시합니다.best를 반환합니다.
def largestRectangleArea(heights):
n = len(heights)
stack = [] # indices whose heights rise strictly from bottom to top
best = 0
for i in range(n + 1):
current = heights[i] if i < n else 0 # a bar of 0 past the end empties the stack
while stack and heights[stack[-1]] >= current:
# Bar i stops the popped bar on the right; the bar below it stops it on the left.
height = heights[stack.pop()]
left = stack[-1] if stack else -1
best = max(best, height * (i - left - 1))
stack.append(i)
return best
함정과 경계 사례
스택 루프는 짧으며, 거의 모든 버그는 너비 계산이나 마지막에 남은 막대에서 발생합니다.
- 스택에 남아 있는 막대를 잊는 경우.
[1, 2, 3, 4, 5]처럼 증가하는 히스토그램에서는 루프 안에서 아무것도 꺼내지 않으며, 높이가 0인 마감 막대가 없으면 9가 아니라 0을 반환합니다. - 꺼낸 막대 자체의 인덱스부터 너비를 계산하는 경우. 직사각형은 해당 막대에서 시작하는 것이 아니라 스택에서 그 아래에 있는 막대 바로 다음부터 시작합니다.
[2, 5, 6, 3, 4, 1]에서 인덱스 3의 3은 인덱스 1부터 4까지를 차지합니다.i - t를 사용하면 4가 아니라 2가 됩니다. - 꺼낸 후 스택이 비었을 때 잘못된 너비를 사용하는 경우. 꺼낸 막대는 지금까지 가장 낮은 막대이므로 직사각형은 인덱스 0까지 뻗고 너비는
i입니다.[2, 1, 2]에서 1은 세 막대 전체에 걸쳐 너비를 차지하므로 넓이는 3입니다. - 두 번 순회하는 버전에서 양쪽에 높이가 같은 막대가 있으면 처리를 멈추는 경우. 그러면
[3, 3, 3, 3]에서 각 막대의 너비가 1이 되어 12가 아니라 3을 반환합니다. 경계에 있는 막대가 엄격히 더 낮아지도록>=일 때 꺼내세요. - 가장 높은 막대나 가장 넓은 구간이 정답이라고 가정하는 경우.
[2, 5, 6, 3, 4, 1]에서는 6도 막대 6개의 전체 너비도 정답이 아닙니다. 중간 높이에 중간 너비를 곱한 값이 정답입니다. - 오버플로. 여기서 넓이는
10^5 × 2 × 10^4 = 2 × 10^9까지 도달하며, 이는 부호 있는 32비트 정수에 여전히 들어갑니다. 제한이 더 크다면 64비트로 곱하세요.
자주 묻는 질문4
히스토그램에서 가장 큰 직사각형의 시간 복잡도는 얼마인가요?
단조 스택 솔루션은 O(n) 시간과 O(n) 추가 공간을 사용합니다. 각 인덱스는 한 번 푸시되고 한 번 팝되며, 각 팝 연산은 일정한 양의 작업만 수행합니다. 막대의 모든 구간을 시도하면 O(n²) 시간이 걸리며, 막대가 2 × 10^4개일 때 약 2 × 10^8단계가 소요됩니다.
막대의 사각형은 왜 팝될 때 측정되나요?
막대는 오른쪽에 있는 자신보다 높지 않은 첫 번째 막대에 의해 스택에서 꺼내지므로, 그 지점에서 직사각형이 오른쪽으로 끝납니다. 스택에서 그 아래에 있는 인덱스는 왼쪽에서 가장 가까운 더 낮은 막대이므로, 그 지점에서 왼쪽으로 끝납니다. 막대를 꺼내는 순간 양쪽 끝을 모두 알 수 있으며, 넓이는 height × (i - below - 1)입니다.
히스토그램에서 가장 큰 직사각형 문제를 분할 정복으로 해결할 수 있나요?
네. 전체 구간에서 가장 낮은 막대는 최적의 직사각형 아래에 놓여 직사각형의 넓이가 lowest × width가 되거나, 구간을 왼쪽과 오른쪽 부분으로 나누어 각각 따로 계산하게 합니다. 선형 탐색으로 최솟값을 찾으면 무작위 입력에서는 O(n log n)이지만 정렬된 입력에서는 O(n²)입니다. 구간 최솟값을 위한 세그먼트 트리를 사용하면 항상 O(n log n)입니다. 스택이 더 간단하고 빠릅니다.
히스토그램에서 가장 큰 직사각형 문제는 0/1 그리드에서 최대 직사각형을 구하는 데 어떻게 사용되나요?
격자를 행별로 순회하면서 각 열에서 현재 행까지 연속된 1의 개수를 유지합니다. 0을 만나면 해당 개수를 초기화합니다. 각 행의 개수는 히스토그램을 이루며, 해당 행에서 끝나는 1의 최대 직사각형은 그 히스토그램에서 가장 큰 직사각형입니다. 행마다 스택을 한 번씩 실행하면 O(rows × cols) 시간에 격자를 해결할 수 있습니다.
비슷한 문제
같은 아이디어를 쓰는 문제입니다. 두세 개를 풀면 패턴이 몸에 익습니다.
Python
def largestRectangleArea(heights):
# 여기에 코드를 작성하세요케이스 1
케이스 2
케이스 3
입력
heights = [2, 5, 6, 3, 4, 1]
기대값
12