Codeforces Round 1133 (Div. 1, Div. 2) is coming soon! Register here. ×

Hastorius's blog

By Hastorius, history, 18 months ago, In English

So I suck at binary search, and just mess around with +1s until it seems to work, get a WA or two, mess around with more +1s, and occasionally get it right by accident. But while talking to iframe_, I learned about a binary search implementation that is significantly more intuitive, cleaner, and shorter called Bitwise Binary Search. You can probably figure out how it works from just the name. Instead of storing a range with a left and right bound, you store it in a single int(or long). The current range of values in the nth iteration is the 1 through n-1th largest bits in the number to those bits followed by all 1s. This is fairly intuitive, as each binary search iteration will decrease the the search range by half. The first iteration will determine whether the largest bit of the integer is 1 or 0, and depending on which it is, this will limit the range to the bottom or top half. The rest of the bits work in a similar manner. You iteratively find the ith largest bit of the answer each time. There's a couple of really cool features that this has. First of all, it's easy to write:

int current = start;

for(int step = 1<<20; step > 0; step /= 2)
  if(works(current+step)) current += step;

This is clearly a lot shorter than a normal binary search implementation, and the answer will be stored in current. But on top of that, this will, by default, work for both lowerbound and upperbound. It will find the most extreme value that works, whether works returns true for the bottom x values or top x. This is intuitive as well, so implementing it in contest will take minimal time. It can also be very easily adapted to floating point binary search on answers with similar syntax:

float current = start, maxstep = 1e6, minstep = 1e-6;

for(float step = maxstep; step >= minstep; step /= 2)
  if(works(current+step)) current += step;

I finally learned binary search after reaching expert!

  • Vote: I like it
  • +46
  • Vote: I do not like it

»
18 months ago, hide # |
 
Vote: I like it +16 Vote: I do not like it

Fun fact: some LCA implementations use this binary search (called 'binary lifting')

https://cp-algorithms.com/graph/lca_binary_lifting.html

  • »
    »
    18 months ago, hide # ^ |
     
    Vote: I like it +2 Vote: I do not like it

    Everything that has "binary" in its name isn't automatically binary search. Binary lifting is completely separate technique from binary search.

    • »
      »
      »
      18 months ago, hide # ^ |
       
      Vote: I like it +9 Vote: I do not like it

      actually, I would argue they are the same

      • »
        »
        »
        »
        18 months ago, hide # ^ |
         
        Vote: I like it -6 Vote: I do not like it

        I agree that LCA uses this binary search and also binary lifting, but they are still different things. This (binary search):

        int current = start;
        for(int step = 1<<20; step > 0; step /= 2)
          if(works(current+step)) current += step;
        

        and this (binary lifting):

        up[v][0] = p;
        for (int i = 1; i <= l; ++i)
            up[v][i] = up[up[v][i-1]][i-1];
        

        are completely different. But lca:

        for (int i = l; i >= 0; --i) {
            if (!is_ancestor(up[u][i], v))
                u = up[u][i];
        }
        

        is a great example of this binary search

        • »
          »
          »
          »
          »
          18 months ago, hide # ^ |
           
          Vote: I like it 0 Vote: I do not like it

          Yeah, I was talking about the second part...

          • »
            »
            »
            »
            »
            »
            18 months ago, hide # ^ |
            ← Rev. 2  
            Vote: I like it +5 Vote: I do not like it

            The second part:

            up[v][0] = p;
            for (int i = 1; i <= l; ++i)
                up[v][i] = up[up[v][i-1]][i-1];
            

            Doesn't use anything even remotely close to the binary search described in the blog.

            • »
              »
              »
              »
              »
              »
              »
              18 months ago, hide # ^ |
              ← Rev. 2  
              Vote: I like it 0 Vote: I do not like it

              Oops, my apologies, it seems like I made a typo. I meant the last part.

              for (int i = l; i >= 0; --i) {
                  if (!is_ancestor(up[u][i], v))
                      u = up[u][i];
              }
              

              The works function in this case is is_ancestor() and $$$i$$$ is $$$\log_2(\text{step})$$$.

              You can think of $$$\text{current}$$$ in the bitwise binary search as analogous to how far $$$u$$$ has been lifted up.

    • »
      »
      »
      18 months ago, hide # ^ |
       
      Vote: I like it +16 Vote: I do not like it

      Binary lifting usually involves storing certain answers and then using them to answer queries in logN, usually for kth ancestor or something similar. Binary search doesn't involve any preprocessing, and it instead stores the range in each iteration of the query. While the logic of how this works is similar to the binary lifting approach of incremental jumping, I don't think that alone makes it binary lifting instead of search, especially when it functions as a search. I'd argue that the main difference between bin search and lifting is that lifting involves preprocessing so your binary search code can lookup in O(1).

      • »
        »
        »
        »
        18 months ago, hide # ^ |
         
        Vote: I like it 0 Vote: I do not like it

        At least someone here understands that binary search and binary lifting are two different things.

»
18 months ago, hide # |
 
Vote: I like it 0 Vote: I do not like it

It's very useful, I think.

Anyway, I can use a algorithm better than $$$\Theta(\sqrt{n})$$$.

»
18 months ago, hide # |
 
Vote: I like it 0 Vote: I do not like it

Feels like every month this gets reposted... good! I hope more learn about this technique.

»
18 months ago, hide # |
 
Vote: I like it +17 Vote: I do not like it

It's technically $$$binary$$$ $$$lifting$$$. The idea in this implementation is not of $$$binary$$$ $$$search$$$, like you check the $$$mid$$$ $$$value$$$, but rather of $$$binary$$$ $$$ lifting$$$ and trying to see what is the $$$max$$$ $$$2^k$$$ I can add to my answer.

  • »
    »
    18 months ago, hide # ^ |
     
    Vote: I like it +3 Vote: I do not like it

    well it is exactly the same if you think about it. In the first iteration binary search checks if you can get L/2. If yes it checks 3L/4. So it tries adding L/4 to the answer just like lifting.

    • »
      »
      »
      18 months ago, hide # ^ |
      ← Rev. 2  
      Vote: I like it +29 Vote: I do not like it

      A simple example to describe the difference between binary search and binary lifting is LCA. In binary lifting the benefit we get is that our increase is always by some power of 2, that enables us to use spare table for LCA.

      Another example, let's say if you want to binary search on a fenwick tree, now you will have $$$O(log^2$$$ $$$n)$$$ factor but if you use binary lifting, it will be $$$O(log $$$ $$$n)$$$.

      For some simple problems, you can consider as them same, but if you go a little further, they are both different.

  • »
    »
    18 months ago, hide # ^ |
     
    Vote: I like it 0 Vote: I do not like it

    Binary lifting is just this part of lca:

    up[v][0] = p;
    for (int i = 1; i <= l; ++i)
        up[v][i] = up[up[v][i-1]][i-1];
    

    It doesn't have to do anything with this blog

    • »
      »
      »
      18 months ago, hide # ^ |
       
      Vote: I like it 0 Vote: I do not like it

      And this:

      for (int i = l; i >= 0; --i) {
          if (!is_ancestor(up[u][i], v))
              u = up[u][i];
      }
      

      is NOT called binary lifting

»
18 months ago, hide # |
 
Vote: I like it 0 Vote: I do not like it

Congrats on finding what works for you! For me normal binary search is more intuitive, I understand what invariants I need to mantain for left and for right and loop while (left + 1 < right), no ±1 anywhere else.

»
18 months ago, hide # |
 
Vote: I like it 0 Vote: I do not like it

This can also be done if the range is not a power of two, just make sure to not to exceed the upper bound and potentially break it.

int current = lo;
for(int step = 1<<__lg(hi-lo); step > 0; step /= 2)
  if(current+step<=hi && works(current+step)) current += step;
»
18 months ago, hide # |
← Rev. 2  
Vote: I like it 0 Vote: I do not like it

Also, you can make it a bit shorter:

int t = 0;
for (int i = 1 << 30; i >= 1; i >>= 1) {
  t += i * f(i + t);
}

This finds $$$t$$$ where $$$f(t) = 1$$$ and $$$f(t+1) = 0$$$, and (of course) $$$f$$$ is monotonic. The only real change from your code is removing that if condition.

»
18 months ago, hide # |
 
Vote: I like it 0 Vote: I do not like it

This is fun, but if anyone still wants to learn about the classic binary search, you can just look up Errichto's blog. Never had any trouble with the implementation after this.