[알고스팟] FENCE-울타리 잘라내기
3 분 소요
[알고스팟] FENCE-울타리 잘라내기
문제 링크
문제 설명
많이 등장하는 문제이다. 백준에선 히스토그램에서 가장 큰 직사각형이라는 문제로 존재한다.
막대기들을 쭉 세울 때, 막대기 안에 직사각형 모양의 넓이 중 가장 큰 직사각형의 넓이를 구하는 문제이다.
코드 리뷰-1
예전에 풀어봤던 문제인데, 모든 막대의 범위 중, 가장 높이가 작은 막대기를 선택한 후, 그 막대기를 제외한 후 막대기 기준으로 왼쪽 영역과 오른쪽 영역으로 나눈다. 그리고 선택했던 막대기는 범위 중 가장 높이가 작은 막대기이므로, 전체범위만큼 직사각형을 그릴 수 있다.
위에서 구한 직사각형의 넓이와 왼쪽 영역, 오른쪽 영역으로 나누어서 분할 정복으로 푼 넓이 세개를 비교하여 가장 큰 답을 출력한다.
원래 기존에 이런 알고리즘으로 풀 때, 가지고 온 범위 중 가장 높이가 작은 막대기를 선택할 경우 세그먼트 트리를 전처리로 만들어 놓고 최소 막대기의 인덱스를 쿼리작업을 통해 가져왔었다.
이번에 풀 때는 전처리 작업을 하지 않고 재귀호출을 할때마다 그 범위만큼 탐색을 하며 가장 높이가 작은 막대기를 선택하였는데, 세그먼트 트리를 사용하지 않을 경우, 이 알고리즘은 큰 문제가 하나 존재한다.
최악의 경우 매번 범위의 가장 끝에서 가장 작은 막대기가 나올 경우, 분할이 1:N-1로 이루어지므로 재귀의 깊이가 N만큼 깊어지게 된다.
계속해서 가장 작은 막대기가 중간에 존재할 경우엔 nlgn 시간이 걸리지만, 위의 상황과 같을 땐 최악의 경우에 O(N^2)의 상황을 피하지 못한다.
이렇게 짠 알고리즘을 먼저 첨부한다.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std ;
int minheight ( vector < int > & fence , int left , int right ) {
int Min = 100000 ;
int idx = 0 ;
for ( int i = left ; i <= right ; i ++ ) {
if ( Min > fence [ i ]) {
Min = fence [ i ];
idx = i ;
}
}
return idx ;
}
int dq ( vector < int > & fence , int idx , int left , int right ) {
if ( left >= right ) return fence [ right ];
if ( right <= left ) return fence [ left ];
int area = fence [ idx ] * ( right - left + 1 );
int leftidx = minheight ( fence , left , idx - 1 );
int rightidx = minheight ( fence , idx + 1 , right );
int leftarea = dq ( fence , leftidx , left , idx - 1 );
int rightarea = dq ( fence , rightidx , idx + 1 , right );
return max ( leftarea , max ( rightarea , area ));
}
int main () {
ios :: sync_with_stdio ( 0 ); cin . tie ( 0 ); cout . tie ( 0 );
int C ;
cin >> C ;
for ( int c = 1 ; c <= C ; c ++ ) {
int N ;
cin >> N ;
vector < int > fence ;
int Min = 100000 ;
int idx = 0 ;
for ( int n = 0 ; n < N ; n ++ ) {
int h ;
cin >> h ;
fence . push_back ( h );
if ( Min > h ) {
Min = h ;
idx = n ;
}
}
int ans = dq ( fence , idx , 0 , N - 1 );
cout << ans << '\n' ;
fence . clear ();
}
return 0 ;
}
코드 리뷰-2
위의 코드의 경우 왼쪽 재귀와 오른쪽 재귀가 정확히 이쁘게 반반씩 나뉠 땐, nlgn 시간이 걸리지만, 최악의 경우 계속해서 1:N-1로 나누어질 땐, 최소 막대기를 찾는 작업이 재귀깊이 N만큼 이루어지므로 O(nlgn)이 된다.
그러므로 위와같은 코드를 짤 때는, 탐색에대한 쿼리시간을 줄이기 위해 세그먼트 트리를 이용하여 전처리 작업을 해줘야 시간이 줄어든다.
세그먼트 트리를 사용한 코드는 이전에 히스토그램에서 가장 큰 직사각형이라는 포스팅을 참조하면 된다.
이번에는 재귀의 깊이를 줄이기 위해 조금 다른 방향의 알고리즘을 제시한다.
범위안에 최소막대기 인덱스번호를 기준으로 왼쪽 , 오른쪽으로 나누지 않고 항상 N/2,N/2로 나눌 수 있다면 아무리 탐색시간이 N이라 해도 재귀 깊이가 lgN이기 때문에, 항상 O(nlgn)에 풀 수 있게된다.
방법은 범위를 반을 나눈 후, 왼쪽 범위에서 나올 수 있는 가장 큰 직사각형의 넓이, 오른쪽 범위에서 나올 수 있는 가장 큰 직사각형의 넓이, 그리고 반을 항상 지나는 직사각형의 넓이 중 가장 큰 값을 반환하면 된다.
왼쪽 범위에서 나올 수 있는 가장 큰 직사각형의 넓이와 오른쪽에서 나올 수 있는 가장 큰 직사각형의 넓이는 재귀호출을 통해 작은 문제로 분할하여 풀면 된다.
문제는 가운데를 항상 지나는 직사각형의 넓이를 구하는 것인데, 이 직사각형은 왼쪽영역에서 가장 오른쪽에 있는 막대기, 그리고 오른쪽 영역에서 가장 왼쪽에 있는 막대기는 항상 포함하게 된다.
그 후, 한 칸씩 보는 막대기를 좌 우로 늘려가며 최대 넓이를 찾아내는 것이다. 이런 경우 트리 높이마다 N만큼 탐색시간이 걸리지만 높이가 logN밖에 되지 않으므로 nlogn 시간에 해결할 수 있다.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std ;
int dq ( vector < int > & fence , int left , int right ) {
if ( left == right ) return fence [ left ];
int mid = ( left + right ) / 2 ;
int ret = max ( dq ( fence , left , mid ), dq ( fence , mid + 1 , right ));
int ml = mid , mr = mid + 1 ;
int height = min ( fence [ ml ], fence [ mr ]);
ret = max ( ret , height * 2 );
while ( left < ml || mr < right ) {
if ( mr < right && ( ml == left || fence [ ml - 1 ] < fence [ mr + 1 ])) {
height = min ( height , fence [ ++ mr ]);
}
else {
height = min ( height , fence [ -- ml ]);
}
ret = max ( ret , height * ( mr - ml + 1 ));
}
return ret ;
}
int main () {
ios :: sync_with_stdio ( 0 ); cin . tie ( 0 ); cout . tie ( 0 );
int C ;
cin >> C ;
for ( int c = 1 ; c <= C ; c ++ ) {
int N ;
cin >> N ;
vector < int > fence ;
int Min = 100000 ;
int idx = 0 ;
for ( int n = 0 ; n < N ; n ++ ) {
int h ;
cin >> h ;
fence . push_back ( h );
}
int ans = dq ( fence , 0 , N - 1 );
cout << ans << '\n' ;
}
return 0 ;
}
댓글남기기