Cake 3
Abridged problem statement
You’re given two arrays and of size (), and a number (). Choose any permutation of to maximize the cyclic sum:
where is treated as .
Solution
The permutation in the problem statement can be a bit confusing at first glance. To make things simpler, think of each index as representing a pair .
Now, instead of directly working with the original arrays, imagine we can reorder these indices in any way we want. Then, we choose a subset of size of these reordered indices and maximize the cyclic sum over that subset.
Why is this potentially helpful? We had a permutation before, which was not ordered, but if we did this, we’d be able to just choose a subset of that is ordered.
Fair enough, but this doesn’t help much right now. In fact, it won’t help at all until we make this observation:
For any arbitrary array of length
is minimized when is sorted ()
How would I think of this?
Write a program that generates a random array, then goes through all permutations of it.
Keep track of the best sum and the permutation that caused it. Print this at the end of your program. You’ll notice that the array is always sorted.
Run this a few times (maybe to times) to convince yourself that it’s true.
Of course, I won’t just leave it at that. Here’s how you prove this is true.
Let’s first look at this example: . How does the sum change? Well, changes to . All other terms remain the same. And we notice that . Can we formalize this?
Okay, so maybe let’s try running the bubble sort algorithm on this array to sort it. Each time we swap an adjacent unsorted pair, we go from: to . Since:
is always true when , we’re done?
Not quite. Bubble sort might also fix inversions where . And in this case, the sum actually increases after the swap!
Note
You should expect this. You could see this as you 'breaking' an existing descending sort.Okay, so that didn’t work. No worries, at least we have more intuition now, and we can get to something that does work: induction on the array size.
Assume the result is true for all arrays of size . Can we prove that it works for ?
So I have a sorted array of length consisting of the first elements of . I now want to insert to this array in order to minimize the sum given above. Where would I insert it?
Spoiler
Think about it: we want to insert in a way that minimizes the total added difference with its neighbors. So what position does that?
Well, if we put in a place where it’s already in order with its surroundings, then the jumps to its left and right are as small as possible.
That’s right: we should insert it in the position that keeps the array sorted!
Subtask 1, 2 ()
Sort the arrays as previously described, ensuring that after you’re done.
It is pretty easy to see that for a sorted array, .
Let’s go through each subarray that is at least as big as . We’ll ‘fix’ the two endpoints of the subset we’re gonna pick as and , so now we need to pick elements from the middle.
However nothing but the endpoints matter for , so we just need to pick the largest elements in from . You can use, say, a multiset to achieve this, solving the problem in time.
Code
#include <bits/stdc++.h>
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
const int64_t inf = 1e15;
int n, m;
std::cin >> n >> m;
std::vector<std::pair<int64_t, int64_t>> a(n);
for (auto &[b, a] : a) {
std::cin >> a >> b;
}
std::sort(a.begin(), a.end());
int64_t ans = -inf;
for (int i = 0; i < n; ++i) {
std::multiset<int64_t> st;
int64_t sum = 0;
auto add = [&](int64_t x) {
if (st.size() + 1 <= m - 2) {
st.insert(x);
sum += x;
return;
}
if (x > *st.begin()) {
sum += -*st.begin() + x;
st.erase(st.begin());
st.insert(x);
}
};
for (int j = i + 1; j < i + m - 1; ++j) {
add(a[j].second);
}
for (int j = i + m - 1; j < n; ++j) {
ans = std::max(ans, a[i].second + sum + a[j].second - 2 * (a[j].first - a[i].first));
add(a[j].second);
}
}
std::cout << ans << '\n';
}Full solution
Let’s start with an observation.
Observation: Define as the value of such that choosing and as endpoints will maximize the summation as stated in the problem, with ties broken arbitrarily, but consistently. Then .
Proof: Assume this was not true. In that case, we’d have . However, if this was the case, we could immediately improve our solution for to by picking the exact same values as other than the first element, which would, of course, be .
To understand this better, try doing the reverse: forcing ’s solution on : this would be worse by the fact that is already optimal.
Once we know that is monotonic, we can use divide and conquer to massively speed up our solution!
Strategy: Calcualate together using divide and conquer. At each divide and conquer step, we’ll store the range of values we want to calculate for, and the possible range can take on.
We’ll first compute . Now we know that values to the left of will have a smaller range (). Correspondingly, values to the right will also have a smaller range ().
If we can figure out what is in time, where is the current active value range, then our whole solution would have a time complexity of .
Exercise: Prove this time complexity. It’s not that hard and will ensure that you’ve truly understood this. If you don’t attempt doing this, the next parts of the editorial might be confusing to you.
The next natural question is: how do we efficiently find ? The main bottleneck seems to be translating this code:
ans = std::max(ans, a[i].second + sum + a[j].second - 2 * (a[j].first - a[i].first));to something more efficient.
How do we solve this problem? There are multiple ways: the most obvious one you’re probably thinking of right now is using a merge-sort tree (if you don’t know what this is, I strongly recommend you check out CSES’ range queries section, they have really fun problems!). This is per query, and if we used this for our solution, we’d have ~. Too slow.
But there’s a way to solve this problem in per query as well using either wavelet trees or a persistent segment tree.
However, our purposes require the sum of the maximum values in a given range. This is an easy modification to make once you learn how wavelet trees/persistent segment trees answer order statistic queries.
Either way works and brings your final complexity down to , which comfortably passes.