Skip to content
Maximimum disjoint ranges

Maximimum disjoint ranges

Movie Festival
CSES

Abridged problem statement

Given nn ranges, pick a subset SS of ranges [si,ei)[s_i, e_i), maximizing S|S|, such that, for any two ranges a,bSa,b \in S, ab=a \cap b = \varnothing.

In other words, pick as many ranges as you can while ensuring that no two of them intersect.

Solution

There is no natural next step we can perform in order to solve this problem. Perhaps a greedy strategy will work? What if we simply pick the first occuring interval every single time? Does this maximize the number of intervals we pick?

Turns out, no. Seeing why is pretty easy too. Consider this case:

0 1 2 3 4 5 6 7 8

Our strategy would pick the interval [0,6)[0, 6), and then [6,8)[6, 8), achieving S=2|S|=2. It is clear that we can achieve S=3|S|=3, however, by picking the blue intervals instead.

So… what do we do?

The right move in most greedy problems is to ‘fix’ the right variable. With this in mind, let us try fixing the time we arrive at the festival, that is, the time after which we can start picking intervals. In fact, let us define a function f(t)f(t) that denotes the maximum number of intervals we can pick if we can only pick intervals that start at or after tt.

The point of doing this is to notice only one thing: that ff is monotonic. Indeed, f(t)f(t1)f(t) \le f(t-1) for any value of tt. This is because the set of intervals associated with t1t-1 is bigger than that associated with tt, so, if f(t1)f(t-1) was currently worse, we could just pick the same set of intervals picked by f(t)f(t) to get them to be equal.

Now, out of out the intervals [si,ei)[s_i, e_i) available to us at t=0t=0, let us try picking all of them. Then, we get

f(0)=1+max0i<nf(ei) f(0) = 1+\max_{0\le i < n} f(e_i)

We could now proceed with some sort of dynamic programming solution; do note that we don’t need to explicitly exclude the interval ii we selected as it will not be re-included in f(ei)f(e_i), because sieis_i \le e_i, and—but wait! We don’t need to do any of that, because we already know that f(t)f(t) is maximized for the smallest tt. So, we can just pick ii such that eie_i is the smallest possible.

Then, we discard intervals with sj<eis_j < e_i and proceed with calculating f(ei)f(e_i) in the exact same way!

Exchange argument

While this solution is complete, with proof, I’ll present another way to prove the same result. Formally, what we are saying is that there exists at least one optimal SS that contains [si,ei)[s_i, e_i), where eie_i is minimized.

The way we prove this is by considering an arbitrary SS. Now, we proceed to transform SS to another set SS', with the hope that SS|S'| \ge |S|. This method of proving greedy algorithms is popularly known as an exchange argument.

So let us do this. Consider an arbitrary set SS.

0 1 2 3 4 5 6 7 8

Now, if SS contains ii, we’re done. Otherwise, sort the intervals contained in SS by their right endpoints, and consider the first one. Call this interval jj.

We note that ejeie_j \ge e_i. Let us make S=(S{j}){i}S' = (S \setminus \{j\}) \cup \{i\}. Now:

  • ii is now the first interval in SS'.
  • Since eieje_i \le e_j, and jj did not intersect with any other intervals in SS, ii will not either.

Therefore, we have managed to construct SS' with SS|S'| \ge |S|, proving that an optimal set containing ii must always exist.

For readers not convinced by the rigour of this proof, we are essentially imagining a huge set of sets of all the possible ways to pick intervals (potentially 2n2^n many). In this set, we want to prove that our target set (SS') is the maximum element. We do this by showing, for every other element in the set (SS), that SS|S'| \ge |S|.

Code

Here’s how we implement this in C++:

#include <bits/stdc++.h>

using namespace std;

int main() {
  int n;
  cin >> n;
  vector<pair<int, int>> a(n);
  for (auto &[s, e] : a) {
    cin >> s >> e;
  }
  sort(a.begin(), a.end(), [&](const pair<int, int> &a, const pair<int, int> &b) {
    return a.second < b.second;
  });
  int ans = 0;
  int t = 0;
  for (auto &[s, e] : a) {
    if (s < t) {
      continue;
    }
    ans++;
    t = e;
  }
  cout << ans << '\n';
}