Binary Lifting

This page briefly introduces binary lifting.

Definition

Binary lifting, as its name suggests, means ‘growing by powers’. When performing a recurrence, if the state space is large and the usual linear recurrence cannot meet the time and space complexity requirements, we can use growth by powers and only compute the values at positions that are integer powers of kk in the state space as representatives. When values at other positions are needed, we use the property that ‘any integer can be represented as a sum of several powers of kk’ to combine the previously computed representative values into the required value. Therefore, using a binary lifting algorithm also requires the state space of the problem being recurred on to be partitionable according to powers of kk. Usually, kk is chosen to be 22.1

This method is used in many algorithms. Its most common applications are the RMQ problem and finding the LCA (lowest common ancestor).

Applications

RMQ

See: RMQ topic

RMQ is short for Range Maximum/Minimum Query and denotes the maximum (minimum) value in a range. The method of using binary lifting to solve RMQ is the sparse table.

Binary lifting on trees for LCA

See: Lowest Common Ancestor

Examples

Problem 1

Problem

How can we weigh all weights in [0,31][0,31] using as few weights as possible? (Weights may only be placed on one side of the balance.)

Solution

The answer is to use the five weights 1, 2, 4, 8, and 16, which can weigh every weight in [0,31][0,31]. Similarly, to weigh every weight in [0,127][0,127], we can use 1, 2, 4, 8, 16, 32, and 64. By choosing powers of 2 as the weights each time, we can use very few weights to weigh any desired weight.

Why are so few needed? To weigh every weight in [0,1023][0,1023], we need only 10 weights; to weigh every weight in [0,1048575][0,1048575], we need only 20. If the target weight doubles, the number of weights only needs to increase by 1. This is called logarithmic growth, because the number of weights required is proportional to the logarithm of the target weight range.

Problem 2

Problem

Given a cycle of length nn and a constant kk, each move goes from point ii to point (i+k) mod n+1(i+k)\bmod n+1. A total of mm moves are made. Each point has a weight aia_i. Find the sum of the weights of the starting points of the mm moves, modulo 109+710^9+7.

Data range: 1≤n≤1061\leq n\leq 10^6, 1≤m≤10181\leq m\leq 10^{18}, 1≤k≤n1\leq k\leq n, 0≤ai≤1090\le a_i\le 10^9.

Solution

It is clearly impossible to simulate mm moves by brute force, since mm can be as large as 101810^{18}; brute-force simulation would take too much time.

Therefore, we need some preprocessing to combine some information in advance so that the result can be obtained more quickly at query time. Recording the result for every possible number of moves would be infeasible in both time and space.

So how should we preprocess? Look at the first example. Do you have an idea now?

Returning to this problem, we need to preprocess some information and use it to combine the answer as quickly as possible. At the same time, the amount of preprocessed information must not be too large. Thus, we can preprocess information in units of powers of 2; this requires processing only a small amount of information during preprocessing and does not require much work during combination.

For this problem, we preprocess the result after jumping 1, 2, 4, 8, and so on steps from each point (the point reached and the sum of point weights). Then, to jump 13 steps, we only need to jump 1+4+8 steps: first jump 1 step from the starting point, then jump 4 steps from the endpoint reached, then jump 8 steps, while accumulating the precomputed sums of point weights. This gives the sum of point weights for 13 jumps.

For each 2i2^i-step jump starting from each point, record go[i][x] as the endpoint after point xx jumps 2i2^i steps, and sum[i][x] as the sum of point weights obtained after point xx jumps 2i2^i steps. During preprocessing, use two nested loops. The information for a 2i2^i-step jump can clearly be viewed as first jumping 2i−12^{i-1} steps and then another 2i−12^{i-1} steps, since 2i−1+2i−1=2i2^{i-1}+2^{i-1}=2^i. Thus, sum[i][x] = sum[i-1][x]+sum[i-1][go[i-1][x]], and go[i][x] = go[i-1][go[i-1][x]].

There are also some implementation details to note. To ensure that no point is counted twice or omitted, we generally preprocess point-weight sums on a ‘left-closed, right-open’ interval. That is, for one step, we record only the weight of the current point; for two steps, we record the weights of the current point and the next point. Equivalently, the weight of the endpoint is never included in sum. Then, during preprocessing, the sums of the two parts can simply be added directly, without worrying that the endpoint of the first segment and the starting point of the second segment will be counted twice.

Here m≤1018m\leq 10^{18}. Although this may look frightening, preprocessing values no greater than 6565 for ii is enough to solve the problem easily, much faster than brute-force enumeration. In technical terms, the time complexity of this approach is Θ(nlog⁡m)\Theta(n\log m) for preprocessing and Θ(log⁡m)\Theta(\log m) per query.

Reference code
#include <cstdio>
using namespace std;

constexpr int mod = 1000000007;

int modadd(int a, int b) {
  if (a + b >= mod) return a + b - mod;  // replace modulo with subtraction to speed up computation
  return a + b;
}

int vi[1000005];

int go[75][1000005];  // make the array slightly larger to avoid out-of-bounds access; define the smaller dimension first when possible
int sum[75][1000005];

int main() {
  int n, k;
  scanf("%d%d", &n, &k);
  for (int i = 1; i <= n; ++i) {
    scanf("%d", vi + i);
  }

  for (int i = 1; i <= n; ++i) {
    go[0][i] = (i + k) % n + 1;
    sum[0][i] = vi[i];
  }

  int logn = 31 - __builtin_clz(n);  // a quick way to take a logarithm
  for (int i = 1; i <= logn; ++i) {
    for (int j = 1; j <= n; ++j) {
      go[i][j] = go[i - 1][go[i - 1][j]];
      sum[i][j] = modadd(sum[i - 1][j], sum[i - 1][go[i - 1][j]]);
    }
  }

  long long m;
  scanf("%lld", &m);

  int ans = 0;
  int curx = 1;
  for (int i = 0; m; ++i) {
    if (m & (1ll << i)) {  // see the relevant bitwise-operation material; this asks whether bit i of m is 1
      ans = modadd(ans, sum[i][curx]);
      curx = go[i][curx];
      m ^= 1ll << i;  // set bit i to zero
    }
  }

  printf("%d\n", ans);
}

Footnotes

  1. Quoted from the binary lifting section of Li Yudong's Guide to Advanced Algorithm Competitions, 0x06. ↩

This content comes from OI Wiki: Binary Lifting. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.