[BOJ] 1725. 히스토그램
04 Nov 2020
Reading time ~5 minutes
참고로 백준 온라인 저지(BOJ)의 6549번 히스토그램에서 가장 큰 직사각형 문제와 입출력 형식은 다르지만 같은 문제입니다. 마찬가지로 알고스팟(Algospot)의 울타리 잘라내기와 릿코드(leetcode)의 largest-rectangle-in-histogram에서도 풀어보실 수 있습니다.
접근방식 1 : 완전탐색(Brute Force)
완전탐색 풀이1
최대 직사각형이 될 수 있는 모든 후보를 만들기 위한 방법으로 처음 떠오른 방식은 밑면의 길이를 차례로 고정하고 그에 맞는 높이를 찾는 방법입니다. 설명의 편의를 위해 주어진 직사각형의 높이가 각각 2, 1, 3, 2, 3 이라 예를 들어 보겠습니다. 아래 그림과 같이 밑면의 길이가 1일 때 만들어지는 최대 직사각형 후보의 개수는 5개 입니다. 마찬가지로 2일 때는 4개, 3일 때는 3개, 4일때는 2개, 5일 때는 1개가 있습니다. 이와 같은 방식으로 최대 직사각형이 될 수 있는 모든 후보를 만들어 볼 수 있습니다.
이와 같은 방식으로 구현한 코드는 아래와 같습니다.
완전탐색(BruteForce) 풀이1 구현
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
int heights[] = new int [N];
for (int i = 0; i < heights.length; i++) {
heights[i] = sc.nextInt();
}
int answer = 0;
int min_height;
// 밑면의 길이를 1로 고정한 후 값을 하나씩 증가시키며 반복.
for (int width = 1; width <= N; width++) {
// 최대 길이 내에서 시작 인덱스를 0으로 고정하고 한 칸씩 오른쪽으로 가면서 반복.
for (int start_index = 0; start_index+width <= N; start_index++) {
// 밑면의 길이와 시작 인덱스를 고정할 때 마다 최소 높이를 구하기 위해서 초기화.
min_height = Integer.MAX_VALUE;
// 최소 높이를 구하기위해 커서를 시작 인덱스부터 밑면의 길이만큼 한 칸씩 이동하며 구간을 탐색.
for (int cursor = start_index; cursor < start_index+width; cursor++) {
// 최소 높이를 구함.
min_height = Math.min(min_height,heights[cursor]);
}
// 이전까지의 후보들의 최대값과 비교해 더 큰 값을 정답 처리.
answer = Math.max(answer,min_height*width);
}
}
System.out.println(answer); // 모든 최대 직사각형 후보들의 계산값 중 최대값을 정답으로 출력.
}
}
for문이 3번 중첩되어 \(O(N^3)\)의 시간복잡도를 가지게 됩니다. 히스토그램 문제의 입력 N의 최대값은 \(100,000(=10^5)\) 이므로 최대 \(10^{15}\)정도의 시간이 걸리게 된다고 생각해 볼 수 있습니다. 현대 컴퓨터에서의 수행시간을 대략 반복문 수행횟수 1억(=\(10^8\)) 당 1초가 걸린다고 가정한다면 약 1천만초(=\(10^7\)) 즉, 날짜로 환산하면 (\(10^7/3600*24\)) 116일 가까이 걸리게 되므로 문제에 명시된 2초의 제한시간으로는 시간초과가 발생함을 예상해 볼 수 있습니다.
완전탐색 풀이2
마찬가지로 모든 밑면의 길이를 탐색하여 최대 직사각형 후보를 만들기 위한 방식입니다. 다만 풀이1과 관점을 약간 달리하여 시작 인덱스를 차례로 고정하고 종료 인덱스를 한 칸씩 오른쪽으로 이동하면서 넓이를 계산하는 방법입니다. 아래 그림과 같이 시작인덱스를 0으로 고정한 후 종료 인덱스를 1부터 5까지 이동하면서 탐색합니다. 다음으로는 시작인덱스를 1로 고정한 후 종료인덱스를 2부터 5까지 이동하면서 탐색합니다. 이와 같은 방식으로 최대 직사각형이 될 수 있는 모든 후보를 만들어 볼 수 있습니다.
완전탐색(BruteForce) 풀이2 구현
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
int heights[] = new int [N];
for (int i = 0; i < N; i++) {
heights[i] = sc.nextInt();
}
int answer = 0;
int min_height;
// 시작인덱스 0으로 고정한 후 한 칸씩 오른쪽으로 가면서 차례로 고정.
for (int start_index = 0; start_index < heights.length; start_index++) {
// 종료인덱스를 시작인덱스의 한 칸 오른쪽 인덱스로 고정한 후 한 칸씩 오른쪽으로 가면서 차례로 고정.
for (int end_index = start_index+1; end_index <= heights.length; end_index++) {
// 시작 및 종료인덱스가 고정될 때마다 최소 높이를 구하기 위해서 초기화.
min_height = Integer.MAX_VALUE;
// 시작인덱스부터 종료인덱스까지 커서를 한 칸씩 이동하며 최소 높이를 계산.
for (int cursor = start_index; cursor < end_index; cursor++) {
// 최소 높이를 구함.
min_height = Math.min(min_height, heights[cursor]);
}
// 이전까지의 후보들의 최대값과 비교해 더 큰 값을 정답 처리.
answer = Math.max(answer,min_height*(end_index-start_index));
}
}
System.out.println(answer); // 모든 최대 직사각형 후보들의 계산값 중 최대값을 정답으로 출력.
}
}
for문이 3번 중첩되어 풀이1과 동일한 \(O(N^3)\)의 시간복잡도를 가지게 됩니다. 마찬가지로 시간초과를 예상해 볼 수 있습니다. 하지만 풀이1과는 달리 시간복잡도를 개선하는 방법이 존재합니다.
완전탐색 풀이3
부분의 최소값을 구하는 방식을 순차적으로 즉시 처리하면 풀이2의 시간복잡도인 \(O(N^3)\)에서 for문의 중첩을 한 겹 벗겨낼 수 있습니다. 그렇게 하면 시간복잡도를 \(O(N^2)\)으로 개선할 수 있습니다.
완전탐색(BruteForce) 풀이3 구현
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
int heights[] = new int [N];
for (int i = 0; i < heights.length; i++) {
heights[i] = sc.nextInt();
}
int answer = 0;
int min_height;
int cursor = 0;
// 시작 인덱스를 0으로 고정한 후 한 칸씩 오른쪽으로 가면서 차례로 고정.
for (int start_index = 0; start_index < heights.length; start_index++) {
// 시작인덱스가 고정될 때마다 최소 높이를 구하기 위해서 초기화.
// 종료인덱스가 고정될 때마다 초기화 하지 않는 이유는 이전 구간의 최소 높이로 재사용되기 때문
min_height = Integer.MAX_VALUE;
for (int end_index = start_index+1; end_index <= heights.length; end_index++) {
// 이전 구간에서 추가된 부분의 높이를 탐색할 커서위치.
cursor = end_index-1;
// 우변의 Math.min 함수 내의 변수는 종료인덱스가 한 칸씩 오른쪽으로 이동할 때마다 이전 구간의 최소 높이로 사용.
// 좌변의 변수는 이전 구간 최소높이와 커서위치 높이를 비교해서 현재 구간의 최소높이를 구함.
min_height = Math.min(min_height, heights[cursor]);
// 이전까지의 후보들의 최대값과 비교해 더 큰 값을 정답 처리.
answer = Math.max(answer,min_height*(end_index-start_index));
}
}
System.out.println(answer); // 모든 최대 직사각형 후보들의 계산값 중 최대값을 정답으로 출력.
}
}
for문이 2번 중첩되어 \(O(N^2)\)의 시간복잡도를 가지게 됩니다. 하지만 마찬가지로 히스토그램 문제의 입력 N의 최대값은 \(100,000(=10^5)\) 이므로 최대 \(10^{10}\)정도의 시간이 걸리게 된다고 생각해 볼 수 있습니다. 현대 컴퓨터에서의 수행시간을 대략 반복문 수행횟수 1억(=\(10^8\)) 당 1초가 걸린다고 가정한다면 약 100초(=\(10^2\)) 가까이 걸리게 되므로 문제에 명시된 2초의 제한시간으로는 시간초과가 발생함을 예상해 볼 수 있습니다. 116일에서 100초로 어느정도 성능이 향상되었지만 아직 제한시간에는 못 미치기에 다른 방법을 생각해내야합니다.
접근방식 2 : 분할정복(Divide And Quanquer)
분할정복(Divide And Quanquer) 풀이
분할정복(Stack) 구현
접근방식 3 : 스택(Stack)
스택 풀이
지금까지는 직사각형의 밑면의 길이를 고정해서 후보를 만들어 보는 접근방식이었습니다. 하지만 이번에는 높이를 고정하고 그때의 높이를 좌우로 최대로 늘려 최대 직사각형 후보를 만드는 방식을 사용합니다. 최대 직사각형의 높이가 될 수 있는 후보는 인덱스의 수만큼 이므로 인덱스 마다의 높이를 모두 탐색해보는 방법입니다.
스택(Stack) 구현
import java.util.Scanner;
import java.util.Stack;
import java.lang.Math;
class Main{
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
int heights []= new int [N+2];
heights[0]=0;
heights[N+1]=0;
for (int i = 1; i <= N; i++) {
heights[i] = sc.nextInt();
}
Stack<Integer> stack = new Stack<Integer>();
long answer = 0;
int width = 0;
int i = 0;
for (int cursor = 0; cursor < heights.length; cursor++) {
while(!stack.isEmpty() && heights[stack.peek()] >= heights[cursor]){
i = stack.peek();
stack.pop();
if(!stack.isEmpty())
width = cursor - (stack.peek() + 1);
answer = Math.max(answer, (long)(width)*heights[i]);
}
stack.push(cursor);
}
System.out.println(answer);
}
}