Introduction
Note: binary search is a prerequisite.
Suppose we know that for and otherwise. We can use binary lifting to find the value of . The advantage of this method over normal binary search is that is always gurantees a fixed number of iterations. It is, for example, used to find the lowest common ancestor of two nodes in a tree, and it is useful to keep in mind.
int t = 0;
for (int i = 1 << 30; i >= 1; i >>= 1) {
t += i * f(i + t);
}
std::cout << t << '\n';The reason this works relies on the binary representation of a number. Let’s say was , that is, we’d have jumped too far if we took the bit. It is still possible for us to check every jump length from , since
,
that is, the bits lower than can still exhaustively cover every possible smaller answer.
Another way to think of this is to think of the binary representation of . We’re trying to maximize it, and just like how for numbers of equal lengths, any number starting with a has to be bigger than a number starting with an in base , a number starting with must be greater than a number starting with in binary. On every iteration, we’re checking whether the bit can be .
Also, if we knew the value of beforehand, we could visualize how this algorithm would always find the correct series of jumps, and would eventually converge to .
Minimum/maximum with binary lifting
Given an array , answer queries where, given an index and a number , you need to output the largest index such that .
We can use binary lifting to solve this problem. The only tricky part is coming up with a valid .
Key observation: Refer to the earlier code. At every step, we only ever jump ahead by a power of .
Assume we had some function that would return in constant time. Notice that this function only has possible inputs ( and .
If we did have such a function, then we would be able to apply binary lifting similar to how we’ve done above:
int ans = i, cur = A[i];
for (int j = 19; j >= 0; --j) {
int upd = std::max(cur, g(ans, j));
if (upd <= x * A[i]) {
ans += 1 << j, cur = upd;
}
}
std::cout << ans << '\n';(Note: , which is larger than , the standard constraint on in most problems)
Now, how do we compute efficiently?
Base case: If , then is just .
Recursive case: Otherwise, for , notice that an interval of size can be broken down into two sub-intervals of sizes . . Don’t forget to account for the fact that may exceed .
This computes in time and uses space.
std::vector<std::array<int, 20>> g(n);
for (int i = 0; i < n; ++i) {
g[i][0] = A[i];
}
for (int j = 1; j < 20; ++j) {
for (int i = 0; i < n; ++i) {
g[i][j] = std::max(g[i][j - 1], g[std::min(i + (1 << (j - 1)), n - 1)][j - 1]);
}
}