<

PS/알고리즘

[Algorithm/C++] 삼분 탐색(Ternary search) (백준 8986 전봇대)

leedongbin 2023. 6. 23. 16:52

1. 알고리즘 설명

삼분 탐색은 어떤 unimodal한 그래프에서, 최댓값 or 최솟값을 loglog 시간 안에 찾아주는 알고리즘입니다.

이때 unimodal이란, uni(하나의) + mode(봉우리)라는 뜻으로, 이차함수 그래프처럼 봉우리가 하나인 그래프를 의미합니다. (bimodal은 'W'모양의 그래프처럼 봉우리가 두 개, multimodal은 봉우리가 여러 개인 경우입니다)

이분 탐색과 비교하자면, 이분 탐색은 함수의 값이 '/', '\'모양처럼 쭉 단조 증가 or 단조 감소하는 경우에만 사용할 수 있고,

탐색하려는 구간의 길이가 nn일 때, 매 탐색 과정에서 구간이 1/2 크기로 줄어들기 때문에 시간복잡도는 log2(n)log2(n)입니다.

 

삼분 탐색은 함수의 값이 'V'모양처럼 쭉 단조 증가하다가, 어느 순간부터 쭉 단조 감소하는 경우에만 사용할 수 있고,

탐색하려는 구간의 길이가 n일 때, 매 탐색 과정에서 구간이 2/3 크기로 줄어들기 때문에 시간복잡도는 log3/2(n)log3/2(n)입니다. 어차피 BigOBigO 표기법에서 loglog의 밑은 상수의 곱으로 떼어낼 수 있기 때문에, 두 탐색의 시간복잡도는 log(n)log(n)으로 같습니다.

삼분 탐색 구현과 자세한 설명은 맨 아래 코드 설명의 'ternary_search()' 함수에 대한 설명을 참고하시면 됩니다.

 

2. 예제 문제

백준8986 - 전봇대

 

8986번: 전봇대

입력의 첫 줄은 전봇대의 수 N (1 ≤ N ≤ 100,000)이 주어진다. 두 번째 줄에는 전봇대의 위치를 나타내는 N개의 서로 다른 x-좌표 xi(i = 0, ..., N-1)가 빈칸을 사이에 두고 오름차순으로 주어진다. xi는

www.acmicpc.net

 

3. 문제 풀이

x0x0전봇대의 위치가 0으로 고정이므로, 두 전봇대 사이의 거리를 dd라고 한다면, ii번째 전봇대의 도착 위치는 didi로 정해집니다.

ii번째 전봇대의 초기 위치를 x[i]x[i], g(i)=ig(i)=i번째 전봇대가 이동해야 할 거리라 하면, g(i)g(i)는 아래와 같습니다.

g(i)={di+x[i],if(di<=x[i])dix[i],otherwise

도착 위치가 초기 위치보다 왼쪽에 있는 경우, g(i)는 기울기가 음수인 일차함수의 형태고,

도착 위치가 초기 위치보다 오른쪽에 있는 경우, g(i)는 기울기가 양수인 일차함수의 형태입니다.

d가 엄청 작을 때 g(i) 의 기울기는 음수일 것이고, (d가 작을 때, 단조 감소)

d가 엄청 클 때 g(i) 의 기울기는 양수일 것입니다. (d가 클 때, 단조 증가)

따라서 g(i)는 unimodal 그래프이고, 우리가 구해야 할 값 f(d)=ni=1g(i)라고 하면, unimodal 그래프의 합 역시 unimodal하다는 특성 때문에 f(d)역시 unimodal합니다. 따라서 d에 대해 삼분 탐색을 해주면 됩니다.

 

4. 코드

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
vector<ll> x;
ll f(ll d){//두 전봇대 사이의 거리가 d일 때, 이동 거리의 합
ll ret=0;
for(ll i=0;i<x.size();i++)
ret+=abs(x[i]-d*i);
return ret;
}
ll ternary_search(){
ll l=1,r=1e9,lmid,rmid;
while(l+3<r){
lmid=(l+l+r)/3,rmid=(l+r+r)/3;
if(f(lmid)<f(rmid))r=rmid;
else l=lmid;
}
ll ans=LLONG_MAX;
for(ll i=l;i<=r;i++)ans=min(ans,f(i));
return ans;
}
int main(){
int n;scanf("%d",&n);
x.resize(n);
for(int i=0;i<n;i++)scanf("%lld",&x[i]);
printf("%lld",ternary_search());
return 0;
}

 

5. 코드 설명

ll f(ll d){//두 전봇대 사이의 거리가 d일 때, 이동 거리의 합
ll ret=0;
for(ll i=0;i<x.size();i++)
ret+=abs(x[i]-d*i);
return ret;
}

문제 풀이에서 g(x)에 해당하는 부분을, 4번째 줄처럼 간단하게 표현할 수 있습니다. 

ll ternary_search(){
ll l=1,r=1e9,lmid,rmid;
while(l+3<r){
lmid=(l+l+r)/3,rmid=(l+r+r)/3;
if(f(lmid)<f(rmid))r=rmid;
else l=lmid;
}
ll ans=LLONG_MAX;
for(ll i=l;i<=r;i++)ans=min(ans,f(i));
return ans;
}

초기에 l을 엄청 작은 값, r을 엄청 큰 값으로 잡으면, l,r은 'V'자 그래프(f(d))의 양 끝에 있는 상황이고, 극값을 향해 미끄러져 내려오는 상황이라고 생각할 수 있습니다.

이제 l,r의 1/3, 2/3 지점을 각각 lmid,rmid로 잡습니다.

만약 f(lmid)<f(rmid) 라면, rmid가 아직 덜 내려온 겁니다. [rmidr]에는 f(lmid)값보다 작은 값이 없습니다. 따라서 r=rmid로 오른쪽 1/3구간을 날려주시면 됩니다.

반대의 경우에는 [llmid]에는 f(rmid)값보다 작은 값이 없는 경우이고, l=lmid로 왼쪽 1/3구간을 날려줍시다.

이때 주의해야 할 점이 있는데, 이분탐색때와 같이 while(l<r) 로 코드를 짜면 무한 루프에 빠질 수 있습니다.

예를 들어, l=2,r=4=>lmid=2,rmid=3 인 경우에 l=lmid 를 반복한다면, l이 갱신되지 않아 l<r 조건을 계속 만족합니다.

따라서 위와 같이 적당한 [lr]구간에서 멈추고 직접 찾아주셔야 합니다.