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)입니다. 어차피 Big−OBig−O 표기법에서 loglog의 밑은 상수의 곱으로 떼어낼 수 있기 때문에, 두 탐색의 시간복잡도는 log(n)log(n)으로 같습니다.
삼분 탐색 구현과 자세한 설명은 맨 아래 코드 설명의 'ternary_search()' 함수에 대한 설명을 참고하시면 됩니다.
2. 예제 문제
8986번: 전봇대
입력의 첫 줄은 전봇대의 수 N (1 ≤ N ≤ 100,000)이 주어진다. 두 번째 줄에는 전봇대의 위치를 나타내는 N개의 서로 다른 x-좌표 xi(i = 0, ..., N-1)가 빈칸을 사이에 두고 오름차순으로 주어진다. xi는
www.acmicpc.net
3. 문제 풀이
x0x0전봇대의 위치가 0으로 고정이므로, 두 전봇대 사이의 거리를 dd라고 한다면, ii번째 전봇대의 도착 위치는 d∗id∗i로 정해집니다.
ii번째 전봇대의 초기 위치를 x[i]x[i], g(i)=ig(i)=i번째 전봇대가 이동해야 할 거리라 하면, g(i)g(i)는 아래와 같습니다.
g(i)={−d∗i+x[i],if(−d∗i<=x[i])d∗i−x[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가 아직 덜 내려온 겁니다. [rmid∼r]에는 f(lmid)값보다 작은 값이 없습니다. 따라서 r=rmid로 오른쪽 1/3구간을 날려주시면 됩니다.
반대의 경우에는 [l∼lmid]에는 f(rmid)값보다 작은 값이 없는 경우이고, l=lmid로 왼쪽 1/3구간을 날려줍시다.
이때 주의해야 할 점이 있는데, 이분탐색때와 같이 while(l<r) 로 코드를 짜면 무한 루프에 빠질 수 있습니다.
예를 들어, l=2,r=4=>lmid=2,rmid=3 인 경우에 l=lmid 를 반복한다면, l이 갱신되지 않아 l<r 조건을 계속 만족합니다.
따라서 위와 같이 적당한 [l∼r]구간에서 멈추고 직접 찾아주셔야 합니다.
'PS > 알고리즘' 카테고리의 다른 글
엘리스 코드 챌린지 후기 (0) | 2024.08.04 |
---|---|
코딩테스트를 효율적으로 대비하는 방법에 대하여 (5) | 2024.02.11 |
[Algorithm/C++] 1,2차원에서의 구간 합 구하기 (+백준 연습문제, kakao 기출문제) (0) | 2023.08.04 |
[Algorithm/C++] 유용한 비트연산 / builtin 함수 (2) | 2023.06.25 |