“Binary like Yin & Yang”

The divide and conquer paradigm

The main idea of Divide and Conquer is that we can tackle a big problem by solving smaller disjoint sub-problems and then merging these solutions together to get an answer. The interesting part is that if we can find disjoint sub-problems that are similar to the original problem, then we can use recursion.

There are usually three steps in divide and conquer:

  • Divide: Forming the sub-problems.
  • Conquer: Solving each sub-problem.
  • Merge: Using the solutions of the sub-problems to solve the original problem.

Today we will learn about one of the most popular Divide and Conquer algorithms: Binary Search.

Binary search

Image taken from Pinterest

Motivation

Let’s say that you need to find the position of a value in an array. How would you do it?

The most intuitive way of approaching this problem is to go through all elements in the array and check if it is the one you are looking for (linear search).

#include <bits/stdc++.h>

using namespace std;

int main () {
  int num = 10;
  int x = 20;
  vector <int> arr = {2, 4, 6, 7, 8, 10, 20, 4034, 4535, 5635435, 577436536};
  bool found = false;
  for (int i = 0; i < arr.size() && !found; i++) {
    if (arr[i] == x) {
      cout << i + 1 << '\n';
      found = true;
    }
  }
  if (!found) {
    cout << x << " is not in the array\n";
  }
  return (0);
}

This approach has a complexity of \(O(n)\). In general, this complexity is pretty good, however looking for a specific element in an array is a basic operation that usually needs to be done more than once. What happens if we do this linear search thousands of times? How many times can we do it without exceeding the time limit? Is there a better way?

Solving an equation

Let’s say we want to solve the equation: \(f(x) = ax^3 + bx^2 + cx + d = e\), where \(a, b, c, d, e > 0\). The answer must be right to 4 decimal places. How can we do this efficiently? We can use binary search to solve this in \(O(\log n)\) time. The key idea is to realize that \(f\) is an non-decreasing function, then \(x_a \leq x_b \rightarrow f(x_a) \leq f(x_b)\). Therefore, we can run a binary search in \(x\) in order to find the answer. However our binary search will be slightly different as now we don’t need to find a precise answer, but one with a precision of 4 decimal places. This means that we want to find an x such that \(|e - f(x)| < 0.0001\).

Searching for the Answer

Let’s consider this problem.

  • What would be the brute force approach?
  • How can we improve this basic solution?

The key thing is to realize that if we can reach the top with an initial strength \(k_0\), then any initial strength \(k, k > k_0\) can also reach the top. Therefore we can use binary search to look for the minimum value of \(k\) that reaches the top from 1 to \(r + 1\). As we can determine for a given \(k\) if it is possible to reach the top, then this solution would have a complexity of \(O(n \log(r))\).

Finally, we would like to show you an interesting problem. This problem shows how to combine force brute and binary search in an unexpected way.

Please, enter the next link and read carefully the problem: Aggresive cows.

Like always take few minutes before looking the code.

#include <bits/stdc++.h>

using namespace std;

const int size = 1e5 + 1;
int ns[size];
int n, c;

bool query (int x) {
  int cont = 1;
  int p = ns[0];
  for (int i = 1; i < n; i++) {
    if (ns[i] - p >= x) {
      cont++;
      p = ns[i];
    }
  }
  if (cont >= c) return true;
  return false;
}

int bs(int l,int r){
  
  while (l < r) {
    int med = l + (r - l + 1) / 2;
    if (query(med)) l = med; 
    else r = med - 1;
  }
  return l;
}

int main() {
  int t;
  scanf("%d", &t);
  while(t--){
    scanf("%d %d", &n, &c);
    for(int i = 0; i < n; i++) scanf("%d", &ns[i]);
    sort(ns, ns + n);
    int l = ns[0], r = ns[n - 1];
    printf("%d\n", bs(l,r));
  }
  return 0;
}

Recommended readings:

Contest

You can find the contest here.

Easy Calculation

For the statement you can notice that there is just one answer, then \(A \cdot x + B \cdot \sin(x) - C = 0\) must have a change of sign around x, we can find it with binary search.

#include <bits/stdc++.h>
     
#define all(A) begin(A), end(A)
#define rall(A) rbegin(A), rend(A)
#define sz(A) int(A.size())
#define pb push_back
#define mp make_pair
     
using namespace std;
     
typedef long long ll;
typedef pair <int, int> pii;
typedef pair <ll, ll> pll;
typedef vector <int> vi;
typedef vector <ll> vll;
typedef vector <pii> vpii;
typedef vector <pll> vpll;

int main () {
  ios::sync_with_stdio(false); cin.tie(0);
  int tc;
  cin >> tc;
  while (tc--) {
    int a, b, c;
    cin >> a >> b >> c;
    double l = -1e9, r = 1e9;
    for (int it = 0; it < 100; it++) {
      double m = (l + r) / 2.0;
      if (a * m + b * sin(m) - c < 0) l = m;
      else r = m;
    }
    cout << fixed << setprecision(6) << l << '\n';
  }
  return (0);
}

Frodo and pillows

If you put \(x\) pillows in Frodo’s position, then the optimal is to put \(x - 1\) pillows to its left and right, \(x - 2\) to the next position to the left and right, and so on. Moreover, if we can put \(x\) pillows in Frodo’s position with this strategy, we can put \(x - 1\) pillows, too. Then, we can fix the number of pillows with binary search and calculate how many pillows are needed.

#include <bits/stdc++.h>
     
#define all(A) begin(A), end(A)
#define rall(A) rbegin(A), rend(A)
#define sz(A) int(A.size())
#define pb push_back
#define mp make_pair
     
using namespace std;
     
typedef long long ll;
typedef pair <int, int> pii;
typedef pair <ll, ll> pll;
typedef vector <int> vi;
typedef vector <ll> vll;
typedef vector <pii> vpii;
typedef vector <pll> vpll;

int main () {
  ios::sync_with_stdio(false); cin.tie(0);
  int n, m, k;
  cin >> n >> m >> k;
  int l = 1, r = m;
  auto sum = [&] (ll val, ll pos) {
    ll sum = val * (val + 1) / 2;
    if (pos <= val) {
      sum -= (val - pos) * (val - pos + 1) / 2;
    } else {
      sum += pos - val;
    }
    return sum;
  };
  auto check = [&] (int val) {
    return (val + sum(val - 1, k - 1) + sum(val - 1, n - k) <= m);
  };
  while (l != r) {
    int m = (l + r + 1) / 2;
    if (check(m)) l = m;
    else r = m - 1;
  }
  cout << l << '\n';
  return (0);
}

Sagheer and Nubian Market

If you fix the value of \(k\) you can compute the cost of each item and take the minimum \(k\) prices. Moreover, if you can buy \(k\) items, you can buy \(k - 1\) items, too. Then, we can find \(k\) using binary search.

#include <bits/stdc++.h>
     
#define all(A) begin(A), end(A)
#define rall(A) rbegin(A), rend(A)
#define sz(A) int(A.size())
#define pb push_back
#define mp make_pair
     
using namespace std;
     
typedef long long ll;
typedef pair <int, int> pii;
typedef pair <ll, ll> pll;
typedef vector <int> vi;
typedef vector <ll> vll;
typedef vector <pii> vpii;
typedef vector <pll> vpll;

int main () {
  ios::sync_with_stdio(false); cin.tie(0);
  int n, s;
  cin >> n >> s;
  vector <int> arr(n);
  for (int& elem: arr) cin >> elem;
  auto get = [&] (int k) {
    vector <ll> n_arr(n);
    for (int i = 0; i < n; i++) n_arr[i] = arr[i] + 1LL * (i + 1) * k;
    sort(all(n_arr));
    return accumulate(begin(n_arr), begin(n_arr) + k, 0LL);
  };
  int l = 0, r = n;
  while (l != r) {
    int m = (l + r + 1) / 2;
    if (get(m) <= s) l = m;
    else r = m - 1;
  }
  cout << l << ' ' << get(l) << '\n';
  return (0);
}

Modified GCD

We can find the divisor of \(a\) and \(b\), then for each query iterate for the divisors of \(a\), and for each divisor of \(a\) that is in \([l, r]\) check if it is in the divisors of \(b\) using binary search.

#include <bits/stdc++.h>
     
#define all(A) begin(A), end(A)
#define rall(A) rbegin(A), rend(A)
#define sz(A) int(A.size())
#define pb push_back
#define mp make_pair
     
using namespace std;
     
typedef long long ll;
typedef pair <int, int> pii;
typedef pair <ll, ll> pll;
typedef vector <int> vi;
typedef vector <ll> vll;
typedef vector <pii> vpii;
typedef vector <pll> vpll;

int main () {
  ios::sync_with_stdio(false); cin.tie(0);
  int a, b;
  cin >> a >> b;
  auto getDivisors = [&] (int num) {
    vi div;
    for (int d = 1; d * d <= num; d++) {
      if (num % d) continue;
      div.pb(d);
      if (d * d != num) div.pb(num / d);
    }
    return div;
  };
  vi div_a = getDivisors(a);
  vi div_b = getDivisors(b);
  sort(all(div_b));
  int q;
  cin >> q;
  while (q--) {
    int l, r;
    cin >> l >> r;
    int ans = -1;
    for (int d: div_a) {
      if (not (l <= d and d <= r)) continue;
      if (binary_search(all(div_b), d)) {
        ans = max(ans, d);
      }
    }
    cout << ans << '\n';
  }
  return (0);
}

Solve it

There must be a change of sign around \(x\), then we can find the value of \(x\) using binary search and check if the \(x\) found holds the equation.

#include <bits/stdc++.h>
     
#define all(A) begin(A), end(A)
#define rall(A) rbegin(A), rend(A)
#define sz(A) int(A.size())
#define pb push_back
#define mp make_pair
     
using namespace std;
     
typedef long long ll;
typedef pair <int, int> pii;
typedef pair <ll, ll> pll;
typedef vector <int> vi;
typedef vector <ll> vll;
typedef vector <pii> vpii;
typedef vector <pll> vpll;

int main () {
  ios::sync_with_stdio(false); cin.tie(0);
  double p, q, r, s, t, u;
  while (cin >> p >> q >> r >> s >> t >> u) {
    auto eval = [&] (double x) {
      return p * exp(-x) + 
             q * sin(x) + 
             r * cos(x) +
             s * tan(x) +
             t * x * x +
             u;
    };
    double l = 0, r = 1;
    for (int it = 0; it < 100; it++) {
      double m = (l + r) / 2.0;
      if (eval(m) > 0) l = m;
      else r = m;
    }
    const double EPS = 1e-8;
    if (abs(eval(l)) < EPS) {
      cout << fixed << setprecision(4) << l << '\n';
    } else {
      cout << "No solution\n";
    }
  }
  return (0);
}

The Stern-Brocot Number System

It holds:

\[\frac{a}{b} < \frac{c}{d} \to ad < bc\]

\[ad < bc \to ad + ab < bc + ab \to a(b + d) < b(a + c) \to \frac{a}{b} < \frac{a + c}{b + d}\]

\[ad < bc \to ad + cd < bc + cd \to (a + c)d < (b + d)c \to \frac{a + c}{b + d} < \frac{c}{d} \]

\[\to \frac{a}{b} < \frac{a + c}{b + d} < \frac{c}{d} \]

Initialiy we have: \(\frac{a}{b} = \frac{0}{1}, \frac{c}{d} = \frac{1}{0}\)

Then, using the above property we can know where should we go and as in each step the numerator or denominator of one bound is increasing, the algorithm will finish in \(O(n + m)\) where \(n, m\) is the numerator and denominator given.

#include <bits/stdc++.h>

using namespace std;

int main () {
  int n, m;
  while (cin >> n >> m) {
    if (n == 1 and m == 1) break;
    int a = 0, b = 1;
    int c = 1, d = 0;
    while (true) {
      int p = a + c;
      int q = b + d;
      if (p == n and q == m) {
        break;
      }
      if (p * m < q * n) {
        cout << 'R';
        a = p, b = q;
      } else {
        cout << 'L';
        c = p, d = q;
      }
    }
    cout << '\n';
  }
  return (0);
}

K-Dominant Character

We can fix each different letter of the given string, then for each letter if it is a \(k\)-dominant character, then it is also a \((k + 1)\)-dominant character, then we can use binary search and the idea of cummulative sums to get the minimum \(k\) for each letter.

#include <bits/stdc++.h>

#define all(A) begin(A), end(A)
#define rall(A) rbegin(A), rend(A)
#define sz(A) int(A.size())
#define pb push_back
#define mp make_pair

using namespace std;

typedef long long ll;
typedef pair <int, int> pii;

int main () {
  ios::sync_with_stdio(false); cin.tie(0);
  string s;
  cin >> s;
  set <char> sigma(all(s));
  int ans = sz(s);
  auto check = [&] (int m, char c) {
    vector <int> frec(30, 0);
    for (int i = 0; i < m; i++) frec[s[i] - 'a']++;
    bool ok = (frec[c - 'a'] > 0);
    for (int i = m; i < sz(s); i++) {
      frec[s[i - m] - 'a']--;
      frec[s[i] - 'a']++;
      ok &= (frec[c - 'a'] > 0);
    }
    return ok;
  };
  for (char c: sigma) {
    int l = 1, r = sz(s);
    while (l != r) {
      int m = (l + r) / 2;
      if (check(m, c)) r = m;
      else l = m + 1;
    }
    ans = min(ans, l);
  }
  cout << ans << '\n';
  return (0);
}