Jump Pointer Algorithm In C++ - C++ Programming Tutorial
C++ Course / STL Algorithm / Jump Pointer Algorithm In C++

Jump Pointer Algorithm In C++

BLUF: Mastering Jump Pointer Algorithm In C++ is a critical step in becoming a proficient C++ developer. This lesson provides a deep dive into the syntax, performance considerations, and real-world applications of this concept.
Key Performance Insight: Jump Pointer Algorithm In C++

C++ is renowned for its efficiency. Learn how Jump Pointer Algorithm In C++ enables low-level control and high-performance computing in the tutorial below.

The Leap Pointer technique is a sophisticated approach crafted to improve ancestor inquiries within tree formations. This method boosts the effectiveness of tasks like determining the lowest common ancestor (LCA) of two particular nodes. Through preliminary preparation of the tree, it allocates a distinct collection of "jump pointers" to every node, serving as connections to its predecessors at designated intervals. This preparatory phase includes the calculation and retention of these pointers in a manner that facilitates rapid navigation and query resolutions.

Approach 1: Simple Approach

Implementation:

Example

#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
class Tree {
    vector<vector<int>> adj; // adjacency list of the tree
    vector<vector<int>> jump; // jump pointers
    vector<int> depth; // depth of each node
    int max_jump; // maximum number of jumps needed
public:
    Tree(int n) {
        adj.resize(n);
        depth.resize(n);
        max_jump = ceil(log2(n));
        jump.resize(n, vector<int>(max_jump + 1, -1));
    }
    // Function to add an edge between two nodes
    void addEdge(int u, int v) {
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    // Preprocessing step to compute jump pointers and depths
    void preprocess(int root) {
        depth[root] = 0;
        dfs(root, -1);
    }
    // Depth-first search to initialize jump pointers and depths
    void dfs(int node, int parent) {
        jump[node][0] = parent;
        for (int i = 1; i <= max_jump; ++i) {
            if (jump[node][i - 1] != -1)
                jump[node][i] = jump[jump[node][i - 1]][i - 1];
        }
        for (int neighbor : adj[node]) {
            if (neighbor != parent) {
                depth[neighbor] = depth[node] + 1;
                dfs(neighbor, node);
            }
        }
    }
    // Function to find the lowest common ancestor of nodes u and v
    int findLCA(int u, int v) {
        if (depth[u] < depth[v])
            swap(u, v);
        // Bring u and v to the same depth
        int diff = depth[u] - depth[v];
        for (int i = 0; i <= max_jump; ++i) {
            if ((diff >> i) & 1)
                u = jump[u][i];
        }
        if (u == v)
            return u;
        // Find the lowest common ancestor
        for (int i = max_jump; i >= 0; --i) {
            if (jump[u][i] != jump[v][i]) {
                u = jump[u][i];
                v = jump[v][i];
            }
        }
        return jump[u][0];
    }
};
int main() {
    int n = 9; // number of nodes in the tree
    Tree tree(n);
    // Add edges to the tree
    tree.addEdge(0, 1);
    tree.addEdge(0, 2);
    tree.addEdge(1, 3);
    tree.addEdge(1, 4);
    tree.addEdge(2, 5);
    tree.addEdge(2, 6);
    tree.addEdge(3, 7);
    tree.addEdge(3, 8);
    // Preprocess the tree to compute jump pointers and depths
    tree.preprocess(0); // Assuming node 0 is the root
    // Find the LCA of nodes 7 and 8
    int lca = tree.findLCA(7, 8);
    cout << "LCA of nodes 7 and 8 is: " << lca << endl;
    // You can add more tests here to find LCA of other nodes
    int lca1 = tree.findLCA(4, 5);
    cout << "LCA of nodes 4 and 5 is: " << lca1 << endl;
    int lca2 = tree.findLCA(6, 8);
    cout << "LCA of nodes 6 and 8 is: " << lca2 << endl;
    return 0;
}

Output:

Output

LCA of nodes 7 and 8 is: 3
LCA of nodes 4 and 5 is: 0
LCA of nodes 6 and 8 is: 0

Explanation:

Tree Structure:

The representation of a tree involves utilizing an adjacency list, in which every node maintains a collection of its linked nodes (referred to as children).

Initialization:

Several data structures are initialized:

  • adj: It stores the adjacency list of the tree.
  • depth: It stores the depth of each node from the root.
  • jump: A 2D array where jumpi represents the 2^j-th ancestor of node i.
  • max_jump: The maximum number of jumps needed, which is the ceiling of the logarithm base 2 of the number of nodes. This determines the number of levels in the jump array.

Creating Connections:

  • To construct the tree, connections are established by adding edges between nodes, which in turn builds the adjacency list.

Preprocessing:

The tree undergoes preprocessing to calculate the jump pointers and node depths. This process involves initiating a depth-first search (DFS) from the root node.

During DFS:

  • The immediate parent of each node is stored as the first level of its jump pointers.
  • Using dynamic programming, higher levels of jump pointers are computed. If jumpi is known, then jumpi is computed as jumpjump[i][j-1].
  • The depth of each node is calculated as the DFS progresses.

Lowest Common Ancestor (LCA) Query:

To find the LCA of two nodes, u and v:

  • First, ensure both nodes are at the same depth. If not, move the deeper node up using its jump pointers until both nodes are at the same depth.
  • If the nodes are the same after this step, the LCA is found.
  • If not, move both nodes up simultaneously using the highest possible jump pointers until their parents are the same. This ensures the nodes converge at their lowest common ancestor.
  • Following data preparation, the code is capable of responding to numerous LCA inquiries with high efficiency. The precalculated jump pointers facilitate locating the LCA within a logarithmic timeframe.
  • Complexity analysis:

Time Complexity

Preprocessing: The initial processing phase includes executing a depth-first search (DFS) to determine the depth of every node and to establish the jump pointers. The DFS operation requires O(n) time, with n representing the total number of nodes within the tree. When it comes to setting up the jump pointers, the process entails computing a maximum of O(log n) pointers for each of the n nodes, leading to an overall time complexity of O(n log n) for this particular stage.

Query: Each LCA query involves adjusting the depths of the two nodes and using the jump pointers to find the common ancestor.

  • Adjusting the depths takes O(log n) time because we might need to move up to the highest jump pointer.
  • Finding the common ancestor by using the jump pointers also takes O(log n) time.
  • Thus, each query can be answered in O(log n) time.

Space Complexity

Storing the Tree: The adjacency list utilizes O(n) memory space, where n represents the total count of nodes within the tree.

Saving the depth of every node necessitates O(n) memory space.

Jump Pointers: The array of jump pointers necessitates O(n log n) memory allocation. Every node can have a maximum of O(log n) pointers, resulting in an overall storage demand of n * log n.

Approach 2: Bottom-Up Approach

The Jump Pointer technique is employed to effectively determine the Lowest Common Ancestor (LCA) of two nodes within a tree. The Bottom-Up Strategy builds the jump pointers by commencing from the leaf nodes and progressing towards the root. This method prepares the tree in advance to facilitate rapid LCA inquiries.

Implementation:

Example

#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
class Tree {
    vector<vector<int>> adj; // adjacency list of the tree
    vector<vector<int>> jump; // jump pointers
    vector<int> depth; // depth of each node
    int max_jump; // maximum number of jumps needed
public:
    Tree(int n) {
        adj.resize(n);
        depth.resize(n);
        max_jump = ceil(log2(n));
        jump.resize(n, vector<int>(max_jump + 1, -1));
    }
    // Function to add an edge between two nodes
    void addEdge(int u, int v) {
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    // Preprocessing step to compute jump pointers and depths
    void preprocess(int root) {
        depth[root] = 0;
        // Start from the leaves and move upwards
        dfs(root);
    }
    // Depth-first search to initialize jump pointers and depths
    void dfs(int node) {
        // Process children first
        for (int neighbor : adj[node]) {
            if (depth[neighbor] == -1) { // If not visited
                depth[neighbor] = depth[node] + 1;
                jump[neighbor][0] = node; // 2^0-th ancestor is the immediate parent
                // Build jump pointers for higher levels
                for (int j = 1; j <= max_jump; ++j) {
                    if (jump[neighbor][j - 1] != -1)
                        jump[neighbor][j] = jump[jump[neighbor][j - 1]][j - 1];
                }
                dfs(neighbor);
            }
        }
    }
    // Function to find the lowest common ancestor of nodes u and v
    int findLCA(int u, int v) {
        if (depth[u] < depth[v])
            swap(u, v);
        // Bring u and v to the same depth
        int diff = depth[u] - depth[v];
        for (int i = 0; i <= max_jump; ++i) {
            if ((diff >> i) & 1)
                u = jump[u][i];
        }
        if (u == v)
            return u;
        // Find the lowest common ancestor
        for (int i = max_jump; i >= 0; --i) {
            if (jump[u][i] != jump[v][i]) {
                u = jump[u][i];
                v = jump[v][i];
            }
        }
        return jump[u][0];
    }
};
int main() {
    int n = 9; // number of nodes in the tree
    Tree tree(n);
    // Add edges to the tree
    tree.addEdge(0, 1);
    tree.addEdge(0, 2);
    tree.addEdge(1, 3);
    tree.addEdge(1, 4);
    tree.addEdge(2, 5);
    tree.addEdge(2, 6);
    tree.addEdge(3, 7);
    tree.addEdge(3, 8);
    // Preprocess the tree to compute jump pointers and depths
    tree.preprocess(0); // Assuming node 0 is the root
    // Find the LCA of nodes 7 and 8
    int lca = tree.findLCA(7, 8);
    cout << "LCA of nodes 7 and 8 is: " << lca << endl;
    // You can add more tests here to find LCA of other nodes
    int lca1 = tree.findLCA(4, 5);
    cout << "LCA of nodes 4 and 5 is: " << lca1 << endl;
    int lca2 = tree.findLCA(6, 8);
    cout << "LCA of nodes 6 and 8 is: " << lca2 << endl;
    return 0;
}

Output:

Output

LCA of nodes 7 and 8 is: -1
LCA of nodes 4 and 5 is: -1
LCA of nodes 6 and 8 is: -1

Explanation of the Bottom-Up Approach

The tree data structure is depicted using an adjacency list, wherein each node maintains a list of its adjacent nodes, including both children and parent nodes.

Initialization:

Several data structures are initialized:

  • An adjacency list to store the tree structure.
  • A depth array to store the depth of each node from the root.
  • A jump pointer table, which is a 2D array where each entry stores the ancestor of a node at different levels.
  • The maximum number of jumps needed, based on the height of the tree.

Creating Connections:

Nodes are connected by adding edges to establish the tree structure. These edges form the adjacency list, outlining the tree's specific arrangement.

Preprocessing (Bottom-Up DFS):

The preprocessing step involves a Depth-First Search (DFS) starting from the root:

  • Set Depths: The depth of each node is calculated, starting with the root at depth 0. Each child's depth is the parent's depth plus one.
  • Initialize Jump Pointers: For each node, its immediate parent is recorded as the first level of its jump pointers.
  • Compute Higher-Level Jump Pointers: Using dynamic programming, higher-level jump pointers are computed. If the 2^j-th ancestor of a node is known, the 2^(j+1)-th ancestor can be calculated by looking up the 2^j-th ancestor of the 2^j-th ancestor.
  • This process is performed recursively for each node's children before the node itself, ensuring that the jump pointers are computed bottom-up.

Finding the LCA:

To find the LCA of two nodes:

  • Equalize Depths: If the nodes are at different depths, move the deeper node up using its jump pointers until both nodes are at the same depth.
  • Simultaneous Climb: If the nodes are not the same, move both nodes up simultaneously using the highest possible jump pointers where their ancestors are different. Continue this until the nodes converge at their lowest common ancestor.
  • The final move will have brought both nodes to their LCA.
  • Complexity analysis:

Time Complexity

Preprocessing:

The depth calculation involves a single execution of Depth First Search (DFS) across the entire tree, ensuring each node is visited only once. This process operates in O(n) time complexity, with 'n' representing the total number of nodes within the tree.

Calculate Jump Pointers: Each node is assigned up to log(n) jump pointers, resulting in a time complexity of O(n log n) for this process, considering the total of n nodes.

So, the overall time complexity for the preprocessing phase amounts to O(n log n).

Query:

Equalizing Depths: Aligning two nodes to the same depth may require utilizing the jump pointers up to log(n) iterations, which results in a time complexity of O(log n).

To determine the Lowest Common Ancestor (LCA) following depth equalization, we employ the jump pointers iteratively, moving both nodes upwards using this technique up to log(n) iterations. This process also maintains a time complexity of O(log n).

Therefore, the time complexity for every query is O(log n).

Space Complexity

Storing the tree as an adjacency list demands space proportional to O(n).

Storing the depth of every node necessitates O(n) space complexity.

Jump Pointers: The jump pointers data structure consists of a two-dimensional array containing n rows and log(n) columns, resulting in an overall space complexity of O(n log n).

Approach 3: Sparse Table Approach

The Sparse Table Technique is a strategy employed to precompute a tree hierarchy for the purpose of effectively determining the Lowest Common Ancestor (LCA) of two given nodes. It generates a sparse table (a 2D array) where each element sti denotes the ancestor of node i that is 2^j levels above it.

Implementation:

Example

#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
class Tree {
    vector<vector<int>> adj; // adjacency list of the tree
    vector<vector<int>> jump; // sparse table for jump pointers
    vector<int> depth; // depth of each node
    int max_jump; // maximum number of jumps needed
public:
    Tree(int n) {
        adj.resize(n);
        depth.resize(n, -1);
        max_jump = ceil(log2(n));
        jump.resize(n, vector<int>(max_jump + 1, -1));
    }
    // Function to add an edge between two nodes
    void addEdge(int u, int v) {
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    // Preprocessing step to compute jump pointers and depths
    void preprocess(int root) {
        depth[root] = 0;
        dfs(root, -1);
        // Build sparse table for higher jumps
        for (int j = 1; j <= max_jump; ++j) {
            for (int i = 0; i < adj.size(); ++i) {
                if (jump[i][j-1] != -1) {
                    jump[i][j] = jump[jump[i][j-1]][j-1];
                }
            }
        }
    }
// Depth-first search to initialize jump pointers and depths
    void dfs(int node, int parent) {
        jump[node][0] = parent; // 2^0-th ancestor is the immediate parent
        for (int neighbor : adj[node]) {
            if (neighbor != parent) { // Avoid going back to the parent
                depth[neighbor] = depth[node] + 1;
                dfs(neighbor, node);
            }
        }
    }
// Function to find the lowest common ancestor of nodes u and v
    int findLCA(int u, int v) {
        if (depth[u] < depth[v])
            swap(u, v);
        // Bring u and v to the same depth
        int diff = depth[u] - depth[v];
        for (int i = 0; i <= max_jump; ++i) {
            if ((diff >> i) & 1)
                u = jump[u][i];
        }
        if (u == v)
            return u;
        // Find the lowest common ancestor
        for (int i = max_jump; i >= 0; --i) {
            if (jump[u][i] != jump[v][i]) {
                u = jump[u][i];
                v = jump[v][i];
            }
        }
        return jump[u][0];
    }
};
int main() {
    int n = 9; // number of nodes in the tree
    Tree tree(n);
    // Add edges to the tree
    tree.addEdge(0, 1);
    tree.addEdge(0, 2);
    tree.addEdge(1, 3);
    tree.addEdge(1, 4);
    tree.addEdge(2, 5);
    tree.addEdge(2, 6);
    tree.addEdge(3, 7);
    tree.addEdge(3, 8);
    // Preprocess the tree to compute jump pointers and depths
    tree.preprocess(0); // Assuming node 0 is the root
    // Find the LCA of nodes 7 and 8
    int lca = tree.findLCA(7, 8);
    cout << "LCA of nodes 7 and 8 is: " << lca << endl;
    // You can add more tests here to find LCA of other nodes
    int lca1 = tree.findLCA(4, 5);
    cout << "LCA of nodes 4 and 5 is: " << lca1 << endl;
    int lca2 = tree.findLCA(6, 8);
    cout << "LCA of nodes 6 and 8 is: " << lca2 << endl;
    return 0;
}

Output:

Output

LCA of nodes 7 and 8 is: 3
LCA of nodes 4 and 5 is: 0
LCA of nodes 6 and 8 is: 0

Explanation of the Sparse Table Approach

Tree Representation:

The tree structure is depicted through an adjacency list named adj, where each node is linked to its direct offspring.

Initialization:

Several data structures are initialized:

  • depth: An array to store the depth of each node in the tree.
  • jump: A 2D vector where jumpi will store the 2^j-th ancestor of node i.
  • max_jump: The maximum power of 2 that is less than or equal to the depth of the tree.

Adding Connections:

In order to establish the tree structure with the adjacency list, connections are introduced between nodes to represent Edges.

Preprocessing (Depth Calculation):

  • Depth Calculation with DFS: Start with a Depth-First Search (DFS) from a chosen root node. During this traversal:
  • Calculate the depth of each node relative to the root node.
  • Set the immediate parent of each node (jumpi) based on the DFS order.

Building Sparse Table:

  • Setup: Begin by setting up the sparse table jumpi to keep track of the direct parent of each node.
  • Progressing to Greater Distances: Employ a double loop to fill in the upper layers of the sparse table. Calculate the ancestor at the 2^j-th level for every node by utilizing the already determined ancestors.

Querying the LCA:

Equalizing Depths: When determining the Lowest Common Ancestor (LCA) of two nodes, u and v, the first step involves aligning the depths of both nodes. This is achieved by adjusting the depths of u and v to be equal through a process of moving them upwards using their respective jump pointers, until they reach the same depth level.

Starting from the maximum power of 2 and descending to 0:

  • Advance both you and v together by following their jump pointers until they diverge at their closest common ancestor. This method guarantees an efficient way to reach the lowest common ancestor.
  • Complexity analysis:

Time Complexity

Preprocessing:

Depth Computation: The Depth-First Search (DFS) algorithm traverses the entire tree just once, ensuring that each node is visited exactly once. This process is time-complexity efficient, with a runtime of O(n), where n represents the total number of nodes within the tree.

Creating a sparse table entails populating a two-dimensional array of dimensions n x log(n). Each cell is calculated based on the outcomes from the preceding levels, resulting in a time complexity of O(n log n).

So, the overall time complexity for the preprocessing phase amounts to O(n log n).

Query:

Equalizing Levels: Aligning the levels of the two nodes to match through jump pointers requires a time complexity of O(log n).

Finding the Lowest Common Ancestor (LCA): Leveraging the sparse table to determine the LCA subsequent to adjusting depths also requires O(log n) time complexity.

Therefore, the time efficiency for every Lowest Common Ancestor (LCA) inquiry is O(log n).

Space Complexity

Storing the tree as an adjacency list consumes space proportional to O(n).

Storing the depth of every node necessitates O(n) space allocation.

The sparse table consists of a two-dimensional array with dimensions n x log(n), necessitating O(n log n) storage space.

Input Required

This code uses input(). Please provide values below:

Logic Practice
Install Logic Practice
Add to home screen for a faster app-like experience