From 259c452b2aa9e957349c256827ff6efd06c9d8ad Mon Sep 17 00:00:00 2001 From: Adrito-M Date: Fri, 21 Oct 2022 05:09:52 +0530 Subject: [PATCH 1/4] Algorithm: BinaryLifting --- Graphs/BinaryLifting.js | 99 +++++++++++++++++++++++++++++++ Graphs/test/BinaryLifting.test.js | 82 +++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 Graphs/BinaryLifting.js create mode 100644 Graphs/test/BinaryLifting.test.js diff --git a/Graphs/BinaryLifting.js b/Graphs/BinaryLifting.js new file mode 100644 index 0000000000..d81a929aec --- /dev/null +++ b/Graphs/BinaryLifting.js @@ -0,0 +1,99 @@ +/** + * Author: Adrito Mukherjee + * Binary Lifting implementation in Javascript + * Binary Lifting is a technique that is used to find the kth ancestor of a node in a rooted tree with N nodes + * The technique requires preprocessing the tree in O(N log N) using dynamic programming + * The techniqe can answer Q queries about kth ancestor of any node in O(Q log N) + * It is faster than the naive algorithm that answers Q queries with complexity O(Q K) + * It can be used to find Lowest Common Ancestor of two nodes in O(log N) + */ + +class BinaryLifting { + constructor (root, tree) { + this.connections = {} + this.up = {} // up[node][i] stores the 2^i-th parent of node + for (const [i, j] of tree) { + this.addEdge(i, j) + } + // LOG should be such that 2^LOG is greater than total number of nodes in the tree + this.LOG = 0 + while ((1 << this.LOG) <= Object.keys(this.connections).length) { + this.LOG++ + } + this.dfs(root, root) + } + + addNode (node) { + // Function to add a node to the tree (connection represented by set) + this.connections[node] = new Set() + } + + addEdge (node1, node2) { + // Function to add an edge (adds the node too if they are not present in the tree) + if (!(node1 in this.connections)) { + this.addNode(node1) + } + if (!(node2 in this.connections)) { + this.addNode(node2) + } + this.connections[node1].add(node2) + this.connections[node2].add(node1) + } + + dfs (node, parent) { + this.up[node] = {} + this.up[node][0] = parent + for (let i = 1; i < this.LOG; i++) { + this.up[node][i] = this.up[this.up[node][i - 1]][i - 1] + } + for (const child of this.connections[node]) { + if (child !== parent) this.dfs(child, node) + } + } + + kthAncestor (node, k) { + for (let i = 0; i < this.LOG; i++) { + if (k & (1 << i)) { + node = this.up[node][i] + } + } + return node + } +} + +function binaryLifting (root, tree, queries) { + const graphObject = new BinaryLifting(root, tree) + const ancestors = [] + for (const [node, k] of queries) { + const ancestor = graphObject.kthAncestor(node, k) + ancestors.push(ancestor) + } + return ancestors +} + +export { binaryLifting } + +// binaryLifting( +// 0, +// [ +// [0, 1], +// [0, 3], +// [0, 5], +// [5, 6], +// [1, 2], +// [1, 4], +// [4, 7], +// [7, 11], +// [7, 8], +// [8, 9], +// [9, 10] +// ], +// [ +// [10, 4], +// [10, 7], +// [7, 2], +// [11, 3] +// ] +// ) + +// [4, 0, 1, 1] diff --git a/Graphs/test/BinaryLifting.test.js b/Graphs/test/BinaryLifting.test.js new file mode 100644 index 0000000000..33db1e6f16 --- /dev/null +++ b/Graphs/test/BinaryLifting.test.js @@ -0,0 +1,82 @@ +import { binaryLifting } from '../BinaryLifting' + +// The graph for Test Case 1 looks like this: +// +// 0 +// /|\ +// / | \ +// 1 3 5 +// / \ \ +// 2 4 6 +// \ +// 7 +// / \ +// 11 8 +// \ +// 9 +// \ +// 10 + +test('Test case 1', () => { + const root = 0 + const graph = [ + [0, 1], + [0, 3], + [0, 5], + [5, 6], + [1, 2], + [1, 4], + [4, 7], + [7, 11], + [7, 8], + [8, 9], + [9, 10] + ] + const queries = [ + [2, 1], + [6, 1], + [7, 2], + [8, 2], + [10, 2], + [10, 3], + [10, 5], + [11, 3] + ] + const kthAncestors = binaryLifting(root, graph, queries) + expect(kthAncestors).toEqual([1, 5, 1, 4, 8, 7, 1, 1]) +}) + +// The graph for Test Case 2 looks like this: +// +// 0 +// / \ +// 1 2 +// / \ \ +// 3 4 5 +// / / \ +// 6 7 8 + +test('Test case 2', () => { + const root = 0 + const graph = [ + [0, 1], + [0, 2], + [1, 3], + [1, 4], + [2, 5], + [3, 6], + [5, 7], + [5, 8] + ] + const queries = [ + [2, 1], + [3, 1], + [3, 2], + [6, 2], + [7, 3], + [8, 2], + [8, 3] + ] + const kthAncestors = binaryLifting(root, graph, queries) + expect(kthAncestors).toEqual([0, 1, 0, 1, 0, 2, 0]) +}) From 20ca12d84f9bfc753fce2cd25edc2dbd57aad07c Mon Sep 17 00:00:00 2001 From: Adrito Mukherjee <98008131+Adrito-M@users.noreply.github.com> Date: Fri, 21 Oct 2022 05:14:21 +0530 Subject: [PATCH 2/4] Update BinaryLifting.js --- Graphs/BinaryLifting.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Graphs/BinaryLifting.js b/Graphs/BinaryLifting.js index d81a929aec..3e96d255ec 100644 --- a/Graphs/BinaryLifting.js +++ b/Graphs/BinaryLifting.js @@ -6,6 +6,7 @@ * The techniqe can answer Q queries about kth ancestor of any node in O(Q log N) * It is faster than the naive algorithm that answers Q queries with complexity O(Q K) * It can be used to find Lowest Common Ancestor of two nodes in O(log N) + * Tutorial on Binary Lifting: https://codeforces.com/blog/entry/100826 */ class BinaryLifting { From d164c323179bd410642294e63076d04352b3435e Mon Sep 17 00:00:00 2001 From: Adrito-M Date: Thu, 27 Oct 2022 18:10:26 +0530 Subject: [PATCH 3/4] made the requested changes --- Graphs/BinaryLifting.js | 67 ++++++++++--------------------- Graphs/test/BinaryLifting.test.js | 2 +- 2 files changed, 23 insertions(+), 46 deletions(-) diff --git a/Graphs/BinaryLifting.js b/Graphs/BinaryLifting.js index 3e96d255ec..0e616fbc28 100644 --- a/Graphs/BinaryLifting.js +++ b/Graphs/BinaryLifting.js @@ -11,51 +11,53 @@ class BinaryLifting { constructor (root, tree) { - this.connections = {} - this.up = {} // up[node][i] stores the 2^i-th parent of node + this.root = root + this.connections = new Map() + this.up = new Map() // up[node][i] stores the 2^i-th parent of node for (const [i, j] of tree) { this.addEdge(i, j) } - // LOG should be such that 2^LOG is greater than total number of nodes in the tree - this.LOG = 0 - while ((1 << this.LOG) <= Object.keys(this.connections).length) { - this.LOG++ - } + this.log = Math.ceil(Math.log2(this.connections.size)) this.dfs(root, root) } addNode (node) { // Function to add a node to the tree (connection represented by set) - this.connections[node] = new Set() + this.connections.set(node, new Set()) } addEdge (node1, node2) { // Function to add an edge (adds the node too if they are not present in the tree) - if (!(node1 in this.connections)) { + if (!this.connections.has(node1)) { this.addNode(node1) } - if (!(node2 in this.connections)) { + if (!this.connections.has(node2)) { this.addNode(node2) } - this.connections[node1].add(node2) - this.connections[node2].add(node1) + this.connections.get(node1).add(node2) + this.connections.get(node2).add(node1) } dfs (node, parent) { - this.up[node] = {} - this.up[node][0] = parent - for (let i = 1; i < this.LOG; i++) { - this.up[node][i] = this.up[this.up[node][i - 1]][i - 1] + this.up.set(node, new Map()) + this.up.get(node).set(0, parent) + for (let i = 1; i < this.log; i++) { + this.up + .get(node) + .set(i, this.up.get(this.up.get(node).get(i - 1)).get(i - 1)) } - for (const child of this.connections[node]) { + for (const child of this.connections.get(node)) { if (child !== parent) this.dfs(child, node) } } kthAncestor (node, k) { - for (let i = 0; i < this.LOG; i++) { + if (k >= this.connections.size) { + return this.root + } + for (let i = 0; i < this.log; i++) { if (k & (1 << i)) { - node = this.up[node][i] + node = this.up.get(node).get(i) } } return node @@ -72,29 +74,4 @@ function binaryLifting (root, tree, queries) { return ancestors } -export { binaryLifting } - -// binaryLifting( -// 0, -// [ -// [0, 1], -// [0, 3], -// [0, 5], -// [5, 6], -// [1, 2], -// [1, 4], -// [4, 7], -// [7, 11], -// [7, 8], -// [8, 9], -// [9, 10] -// ], -// [ -// [10, 4], -// [10, 7], -// [7, 2], -// [11, 3] -// ] -// ) - -// [4, 0, 1, 1] +export default binaryLifting diff --git a/Graphs/test/BinaryLifting.test.js b/Graphs/test/BinaryLifting.test.js index 33db1e6f16..769b877e01 100644 --- a/Graphs/test/BinaryLifting.test.js +++ b/Graphs/test/BinaryLifting.test.js @@ -1,4 +1,4 @@ -import { binaryLifting } from '../BinaryLifting' +import binaryLifting from '../BinaryLifting' // The graph for Test Case 1 looks like this: // From d9667a6097b6d1edd03c3aade727e33ffe4d3d32 Mon Sep 17 00:00:00 2001 From: Adrito-M Date: Thu, 27 Oct 2022 18:24:09 +0530 Subject: [PATCH 4/4] added more comments --- Graphs/BinaryLifting.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Graphs/BinaryLifting.js b/Graphs/BinaryLifting.js index 0e616fbc28..594a5b6676 100644 --- a/Graphs/BinaryLifting.js +++ b/Graphs/BinaryLifting.js @@ -39,6 +39,8 @@ class BinaryLifting { } dfs (node, parent) { + // The dfs function calculates 2^i-th ancestor of all nodes for i ranging from 0 to this.log + // We make use of the fact the two consecutive jumps of length 2^(i-1) make the total jump length 2^i this.up.set(node, new Map()) this.up.get(node).set(0, parent) for (let i = 1; i < this.log; i++) { @@ -52,9 +54,12 @@ class BinaryLifting { } kthAncestor (node, k) { + // if value of k is more than or equal to the number of total nodes, we return the root of the graph if (k >= this.connections.size) { return this.root } + // if i-th bit is set in the binary representation of k, we jump from a node to its 2^i-th ancestor + // so after checking all bits of k, we will have made jumps of total length k, in just log k steps for (let i = 0; i < this.log; i++) { if (k & (1 << i)) { node = this.up.get(node).get(i)