Skip to main content

binary lifting

·440 words·3 mins
note cp algorithm tree
Table of Contents

in a rooted tree, how to find the deck_9360c6ba8176a3c2edfb2711939e3370f952ec2b.svgth ancestor of a node?

straightforward approach
#

if we have the list parent 1, we can travel deck_9360c6ba8176a3c2edfb2711939e3370f952ec2b.svg times upward using the list.

def kth_ancestor(k, v):
    for _ in range(k):
        v = parent[v]
    return v

what is the time complexity? suppose the tree has deck_b29acba03e17f19253981ef97847eee7f483a3e0.svg node, the algorithm runs in deck_7d66f14b6713fabfded752a1c258d9adb6e1107e.svg time, bounded by the height of tree.

what if we have deck_e6f85db3eab8ceace463edf0b385d7003f2fab9e.svg queries? then the algorithm runs in deck_f8924afd570fb0db5969bca4fcd744dda9a0bae9.svg time. if deck_b29acba03e17f19253981ef97847eee7f483a3e0.svg and deck_e6f85db3eab8ceace463edf0b385d7003f2fab9e.svg is large (e.g. deck_6f3c42fed56488e0f63789680f9604ccf6070375.svg), the algorithm is too slow. can we do better? yes, introducing binary lifting.

using binary lifting
#

suppose we want to find the deck_f18b1bd320e43ec1656543f9e8ec39b0a9014ba7.svgth ancestor of a node, we have to travel up deck_f18b1bd320e43ec1656543f9e8ec39b0a9014ba7.svg steps upward in the previous approach. if we somehow know the values of the deck_79edd71641b0e20db3430f92f8d89ca42fc52b90.svgth ancestors of each node, we can travel up in deck_c461d64ce0287c772628b06a59dfbdf618928439.svg steps via the deck_b5447f34f04c91b7480b314d15689b2b2c1366f3.svgst, deck_67244c50dffa96cd02beeccc1b193b63e614587f.svgth, deck_0290b8df3ab5536690e4cebbf58601709bb57f3b.svgnd, deck_8e6ef93adc90ab17a7524d8cb24d893ded639e3b.svgth ancestors.

this is the idea of binary lifting. we first pre-calculate the table of values of the deck_79edd71641b0e20db3430f92f8d89ca42fc52b90.svgth ancestors of each node. then we can answer each query using the table.

the time and space complexity of the pre-calculation is deck_ce26c0eeb3c69b1f51a640e6ea819e34747e231f.svg and each query requires deck_9184899280f7afd324416e0efe21569943546500.svg time, resulting deck_d51d3ce524d11e1a5f2e0849e52fe09aadfa6e40.svg time for the queries.

implementation notes
#

here is the implementation of the precalculation.

def precalc():
    up = [[0] * LOG for _ in range(n)]

    for i in range(n):
        up[i][0] = parent[i]
    for j in range(1, LOG):
        for i in range(n):
            up[i][j] = up[up[i][j - 1]][j - 1]

    return up

the following implementation assumes all nodes have a parent of a node with a smaller value, i.e. parent[i] < i.

def precalc():
    up = [[0] * LOG for _ in range(n)]

    for i in range(n):
        up[i][0] = parent[i]
        for j in range(1, LOG):
            up[i][j] = up[up[i][j - 1]][j - 1]

    return up

then we can find the deck_9360c6ba8176a3c2edfb2711939e3370f952ec2b.svgth ancestor as follow.

def kth_ancestor(k, v):
    for j in range(LOG):
        if (k >> j) & 1:
            v = up[v][j]
    return v

sometimes we have to return -1 if the deck_9360c6ba8176a3c2edfb2711939e3370f952ec2b.svgth ancestor does not exist. then the above implementation will not work since when v travels to the root, it will stay at the root via the self-loop. to avoid this, we need the list depth and add a check before travelling.

def kth_ancestor(k, v):
    if depth[v] < k:
        return -1

    for j in range(LOG):
        if (k >> j) & 1:
            v = up[v][j]
    return v

applications
#

lowest common ancestor

related problems #

further readings
#

Binary Lifting and LCA

references
#

Binary Lifting (Kth Ancestor of a Tree Node)

footnotes
#


  1. assuming parent[root] = root throughout the article. ↩︎