PrevNext
Rare
 0/8

Optimizing Unimodal Functions

Authors: Justin Ji, Chongtian Ma

Using ternary or binary search to find the mode of unimodal functions.

Edit This Page
Resources
cp-algo

Introduction

In competitive programming, we are often asked to find the minimum or maximum value we can attain under some conditions. More specifically, we are often asked to formulate some function f(x)f(x), and find the maximum or minimum value of f(x)f(x).

If our function happens to be unimodal, we can use either ternary search or binary search to find the mode of our function efficiently.

Definitions

A function is unimodal if one of the following conditions holds:

  1. The function strictly increases first, then reaches a maximum, then strictly decreases.
  2. The function strictly decreases first, then reaches a minimum, then strictly increases.

Let's assume that we want to find the local minimum of our function. Then, a function is convex if consecutive differences are non-decreasing. In calculus terms, if the derivative of the function is non-decreasing, the function is convex.

Note that this implies that a convex function is also a unimodal function.

Ternary Search

For now, let's assume that we want to find the minimum of our function, and that our function is unimodal. This means that the second condition holds, meaning our function strictly decreases first, reaches its minimum, then strictly increases.

The idea behind ternary search is somewhat similar to binary search. At every step of our algorithm, we want to cut out some large portion of our search space from being considered.

Let's consider two points m1m_1 and m2m_2, where l<m1<m2<rl < m_1 < m_2 < r. Here, ll and rr are the endpoints of our current search space. This ends up dividing our search space into three sections.

Now, we just do a bit of casework to see if we can eliminate any sections.

  1. f(m1)>f(m2)f(m_1) > f(m_2) means that we can eliminate [l,m1)[l, m_1)
  2. f(m1)<f(m2)f(m_1) < f(m_2) means that we can eliminate (m2,r](m_2, r]
  3. f(m1)=f(m2)f(m_1) = f(m_2) means that we can eliminate [l,m1)[l, m_1) and (m2,r](m_2, r]

Typically, we treat case 3 as being analogous to cases 1 and 2.

As a mini-proof for why the way we handle case 1 is correct, consider the following:

If f(m1)>f(m2)f(m_1) > f(m_2), then we know the following two facts:

  • f(m1)f(m_1) is not the minimum
  • f(m1)f(m_1) cannot be on the strictly increasing side of the function

Regarding the second point, if f(m1)>f(m2)f(m_1) > f(m_2) and m1m_1 was on the strictly increasing side of the function, that would imply m1>m2m_1 > m_2, which is a contradiction.

Thus, we can conclude that m1m_1 remains on the strictly decreasing section of our function. Because of this, we can conclude that our minimum will not be in the range [l,m1)[l, m_1), so we can eliminate this section from our search space.

Now that we know how to strategically remove sections of our search space, the question remains on how to choose the best values of m1m_1 and m2m_2. The optimal way is to divide our search space into thirds, so we have:

m1=l+rl3, m2=rrl3m_1 = l + \frac{r - l}{3}, \ m_2 = r - \frac{r - l}{3}

Implementation

The time complexity of ternary search takes on the following recurrence:

T(n)=T(2n3)+O(1)T(n) = T \left(\frac{2n}{3} \right) + \mathcal{O}(1)

Per the Master theorem, this is O(logn)\mathcal{O}(\log{n}) if we are working with integers.

If we are ternary searching with a fixed error margin ϵ\epsilon, the time complexity becomes O(log(nϵ))\mathcal{O}\left( \log\left( \frac{n}{\epsilon} \right) \right).

C++

template <typename F> double find_min(double l, double r, double eps, const F &f) {
while (r - l > eps) {
double m1 = l + (r - l) / 3;
double m2 = r - (r - l) / 3;
f(m1) > f(m2) ? l = m1 : r = m2;
}
return l;
}

If our function only takes in integers, then our implementation looks a little different.

C++

template <typename F> int find_min(int l, int r, const F &f) {
while (r - l > 3) {
int m1 = l + (r - l) / 3;
int m2 = r - (r - l) / 3;
f(m1) > f(m2) ? l = m1 : r = m2;
}
int res = l;
for (int i = l + 1; i <= r; i++) {
if (f(i) < f(res)) { res = i; }
}
return res;
}

Binary Search

Actually, in the case of our function only taking in integers, binary search is often a better option, as it's shorter and requires less calls to our function f(x)f(x).

However, if we are working with floating points, binary search may be a bit more problematic because of loss of precision. For this reason, ternary search is still sometimes preferable.

C++

template <typename F> int find_min(int l, int r, const F &f) {
while (l < r) {
int m = (l + r) / 2;
f(m) < f(m + 1) ? r = m : l = m + 1;
}
return l;
}

The way the binary search approach works is that it finds the first point where the function becomes strictly increasing. This works because we assume that our function takes the form of strictly decreasing, reaching its minimum, then strictly increasing. Thus, the first point where our function becomes strictly increasing is the turning point of our function, which is the minimum.

Example - Haybale Distribution

Focus Problem – try your best to solve this problem before continuing!

Explanation

Let's define a function f(y)f(y) that evaluates the cost of transporting all the haybales if we deliver the haybales at point yy. Can we show that this function is either unimodal or convex?

An important fact to know when showing that a function is convex is that the sum of convex functions is convex.

Here, f(y)f(y) is the sum of all the individual cost functions for each barn. The cost function for a given barn is convex, so it turns out that f(y)f(y) is also convex!

Given that a convex function is unimodal, this allows us to ternary search on it. Thus, we can find the minimum in O(logN)\mathcal{O}(\log{N}) per query.

Implementation

Time Complexity: O((N+Q)logN)\mathcal{O}((N + Q) \log{N})

Note that the implementation below uses binary search instead, as it's a bit easier to implement.

C++

#include <bits/stdc++.h>
using ll = long long;
int main() {
int n;
std::cin >> n;
std::vector<int> x(n);
for (int &i : x) { std::cin >> i; }

Warning!

On the official editorial for this problem, you will see the following warning:

Note: Make sure to search only on distinct values of xx, or you might end up with the wrong answer.

Why is this? Recall the definition of unimodality if we are trying to find the local minimum of our function.

A function is unimodal if it strictly decreases first, then reaches a minimum, then strictly increases.

We are very specific about the function having to strictly increase and decrease. This is because we can only allow our function to be flat at the minimum! If we have a flat section anywhere else, it's possible that our ternary search ends up eliminating the wrong section.

Tying back to the problem at hand: if we allow searching on duplicate values of xx, we end up creating flat intervals in our cost function, which can lead to our ternary search producing an incorrect answer.

Problems

StatusSourceProblem NameDifficultyTags
SPOJEasy
Show TagsTernary Search
CFEasy
Show TagsTernary Search
CFEasy
Show TagsTernary Search
CFEasy
Show TagsTernary Search
ACEasy
Show TagsTernary Search
CFNormal
Show TagsTernary Search
Baltic OIVery Hard
Show TagsTernary Search

Module Progress:

Join the USACO Forum!

Stuck on a problem, or don't understand a module? Join the USACO Forum and get help from other competitive programmers!

PrevNext