// p for predicate/property// precondition: p(0) = false// precondition: p(1 << NBITS) = last ix we process.// precondition: p is monotonic;//   once it switches to true, does not switch back to false.if (p(1 << NBITS) == 0) { return 1 << NBITS; }else {  assert(p(1<<NBITS) == 1);  int ans = 0;  for (int i = NBITS-1; i >= 0; i--) {    int k = 1 << i;    assert(p(ans + 2*k) == 1);    if (p(ans + k) == 0) {      ans = ans + k;    }  }}// postcondition:// ans is largest index such that// has_some_poperty(ans) = 0

This implies optimality once the loop ends. At the end of the loop we have i = -1. So:

2k[-1] = 2(1/2) = 1finalans = ans[-1]---p(ans[-1] + 2k[-1]) = 1=> p(finalans+1) = 1
k value: k[i]=2i(k-1) value: k[i1]=2i1=2k[i]Ind: p(ans[i]+2k[i])=0Case (a): p(ans[i]+k[i])=0Update: ans[i1]ans[i]+k[i]p(ans[i1]+2k[i1])=p((ans[i]+k[i])+2k[i1])=p(ans[i]+k[i]+k[i])=p(ans[i]+2k[i])=1 (By Induction Hyp.) \begin{aligned} &\text{k value: } k[i] = 2^i \\ &\text{(k-1) value: } k[i-1] = 2^{i-1} = 2k[i] \\ &\text{Ind: } p(ans[i] + 2k[i]) = 0 \\ &\text{Case (a): } p(ans[i] + k[i]) = 0 \\ &\text{Update: } ans[i-1] \equiv ans[i] + k[i] \\ &p(ans[i-1] + 2k[i-1]) \\ &= p((ans[i] + k[i]) + 2k[i-1]) \\ &= p(ans[i] + k[i] + k[i])\\ &= p(ans[i] + 2k[i]) \\ &= 1 ~\text{(By Induction Hyp.)} \end{aligned}
k value: k[i]=2i(k-1) value: k[i1]=2i1=2k[i]Ind: p(ans[i]+2k[i])=0Case (b): p(ans[i]+k[i])=1Update: ans[i1]ans[i]p(ans[i1]+2k[i1])=p(ans[i]+2k[i1])=p(ans[i]+k[i]+k[i])=p(ans[i]+2k[i])=1 (By Induction Hyp.) \begin{aligned} &\text{k value: } k[i] = 2^i \\ &\text{(k-1) value: } k[i-1] = 2^{i-1} = 2k[i] \\ &\text{Ind: } p(ans[i] + 2k[i]) = 0 \\ &\text{Case (b): } p(ans[i] + k[i]) = 1 \\ &\text{Update: } ans[i-1] \equiv ans[i] \\ &p(ans[i-1] + 2k[i-1]) \\ &= p(ans[i] + 2k[i-1]) \\ &= p(ans[i] + k[i] + k[i])\\ &= p(ans[i] + 2k[i]) \\ &= 1 ~\text{(By Induction Hyp.)} \end{aligned}

See that we can insert a new invaraiant at the end of the loop which asserts p(ans[i]+k[i]) == 1:

if (p(1 << nbits) == 0) { return 1 << nbits; }else {  assert(p(1<<nbits) == 1);  int ans = 0;  for (int i = nbits-1; i >= 0; i--) {    int k = 1 << i;    assert(p(ans + 2*k) == 1);    int ans2;    if (p(ans + k) == 0) {      ans2 = ans + k;      // ans2 + k      // = (ans + k) + k      // = ans + 2k      // = 1 (from assertion)    } else {       ans2 = ans;      // ans2 + k      // = ans + k      // = 1 [from else branch]    }    // ## new loop end invariant ##    // true from then, else branch.    assert(p(ans2+k) == 1)    ans = ans2;  }}

§ Fully elaborated proof

if (p(1 << nbits) == 0) { return 1 << nbits; }else {  assert(p(1<<nbits) == 1);  int ans = 0;  // p(ans[nbits-1] + 2*(1<<nbits-1))  // = p(0 + 1 << nbits)  // = p(1 << nbits)  // = 1 [from assert]  for (int i = nbits-1; i >= 0; i--) {    int k = 1 << i;    // From previous loop iteratio (i+1):    // ---------------------------------    // p(ans[(i+1)-1] + k[i+1]) == true    // => p(ans[i] + k[i+1]) == true    // => p(ans[i] + 2k[i]) == true    assert(p(ans + 2*k) == true);    if (p(ans + k) == 0) {      ans += ans + k;      // ASSIGNMENT: ans[i-1] = ans[i] + k[i]      // p(ans[i-1] + k[i])      // = p(ans[i] + k[i] + k[i])      // = p(ans[i] + 2k[i])      // = p(ans[i] + k[i+1])      // = 1 (from induction hyp)    } else {       ans = ans; // no-op       // ASSIGNMENT: ans[i-1] = ans[i].       // p(ans[i-1] + k[i])       // = p(ans[i] + k[i])       // = 1 (from else branch)    }    // ## new loop end invariant ##    // p(ans[i-1] + k[i])== 1    assert(p(ans+k) == 1)  }}

§ Simplified implementation

If we are willing to suffer some performance impact, we can change the loop to become significantly easier to prove:

if (p(1 << nbits) == 0) { return 1 << nbits; }else {  assert(p(1<<nbits) == 1);  int ans = 0;  int i = nbits-1;  while(i >= 0) {    assert (p(ans+2*k) == 1);    int k = 1 << i;    if (p(ans + k) == 0) {      ans += ans + k;    } else {        i--;    }    assert(p(ans) == 0)  }}

In this version of the loop, we only decrement i when we are sure that p(ans+k) == 0. We don't need to prove that decrementing i monotonically per loop trip maintains the invariant; Rather, we can try "as many is as necessary" and then decrement i once it turns out to not be useful.

§ Relationship to LCA / binary lifting

This is very similar to LCA, where we find the lowest node that is not an ancestor. The ancestor of such a node must be the ancestor.

int lca(int u, int v) {    if (is_ancestor(u, v)) return u;    if (is_ancestor(v, u)) return v;    // u is not an ancestor of v.    // find lowest parent of u that is not an ancestor of v.    for (int i = l; i >= 0; --i) {        if (!is_ancestor(up[u][i], v))            u = up[u][i];    }    return up[u][0];}