From 471914a00869a7c6ff3daa7c9f0301929ba0dfdb Mon Sep 17 00:00:00 2001 From: Adrito-M Date: Fri, 28 Oct 2022 19:44:20 +0530 Subject: [PATCH 1/4] algorithm: LCA by binary lifting --- Graphs/LCABinaryLifting.js | 105 +++++++++++++++++++++++++++ Graphs/test/LCABinaryLifting.test.js | 80 ++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 Graphs/LCABinaryLifting.js create mode 100644 Graphs/test/LCABinaryLifting.test.js diff --git a/Graphs/LCABinaryLifting.js b/Graphs/LCABinaryLifting.js new file mode 100644 index 0000000000..885a26244f --- /dev/null +++ b/Graphs/LCABinaryLifting.js @@ -0,0 +1,105 @@ +/** + * Author: Adrito Mukherjee + * Findind Lowest Common Ancestor By Binary Lifting implementation in JavaScript + * The technique requires preprocessing the tree in O(N log N) using dynamic programming) + * It can be used to find Lowest Common Ancestor of two nodes in O(log N) + * Tutorial on Lowest Common Ancestor: https://www.geeksforgeeks.org/lca-in-a-tree-using-binary-lifting-technique + */ + +class LCABinaryLifting { + constructor (root, tree) { + this.root = root + this.connections = new Map() + this.up = new Map() // up[node][i] stores the 2^i-th parent of node + this.depth = new Map() // depth[node] stores the depth of node from root + this.depth.set(root, 1) + for (const [i, j] of tree) { + this.addEdge(i, j) + } + 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.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 (!this.connections.has(node1)) { + this.addNode(node1) + } + if (!this.connections.has(node2)) { + this.addNode(node2) + } + this.connections.get(node1).add(node2) + this.connections.get(node2).add(node1) + } + + 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++) { + 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.get(node)) { + if (child !== parent) { + this.depth.set(child, this.depth.get(node) + 1) + this.dfs(child, node) + } + } + } + + 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) + } + } + return node + } + + getLCA (node1, node2) { + // We make sure that node1 is the deeper node among node1 and node2 + if (this.depth.get(node1) < this.depth.get(node2)) { + [node1, node2] = [node2, node1] + } + // We check if node1 is the ancestor of node2, and if so, then return node1 + const k = this.depth.get(node1) - this.depth.get(node2) + node1 = this.kthAncestor(node1, k) + if (node1 === node2) { + return node1 + } + + for (let i = this.log - 1; i >= 0; i--) { + if (this.up.get(node1).get(i) !== this.up.get(node2).get(i)) { + node1 = this.up.get(node1).get(i) + node2 = this.up.get(node2).get(i) + } + } + return this.up.get(node1).get(0) + } +} + +function lcaBinaryLifting (root, tree, queries) { + const graphObject = new LCABinaryLifting(root, tree) + const lowestCommonAncestors = [] + for (const [node1, node2] of queries) { + const lca = graphObject.getLCA(node1, node2) + lowestCommonAncestors.push(lca) + } + return lowestCommonAncestors +} + +export default lcaBinaryLifting diff --git a/Graphs/test/LCABinaryLifting.test.js b/Graphs/test/LCABinaryLifting.test.js new file mode 100644 index 0000000000..48f6297383 --- /dev/null +++ b/Graphs/test/LCABinaryLifting.test.js @@ -0,0 +1,80 @@ +import lcaBinaryLifting from '../LCABinaryLifting' + +// 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 = [ + [1, 3], + [6, 5], + [3, 6], + [7, 10], + [8, 10], + [11, 2], + [11, 10] + ] + const kthAncestors = lcaBinaryLifting(root, graph, queries) + expect(kthAncestors).toEqual([0, 5, 0, 7, 8, 1, 7]) +}) + +// 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 = [ + [1, 2], + [3, 4], + [5, 4], + [6, 7], + [6, 8], + [7, 8] + ] + const kthAncestors = lcaBinaryLifting(root, graph, queries) + expect(kthAncestors).toEqual([0, 1, 0, 0, 0, 5]) +}) From 0aeccb489875bdc30362a91cb48b8e3aa6e70be3 Mon Sep 17 00:00:00 2001 From: Adrito-M Date: Fri, 28 Oct 2022 19:47:50 +0530 Subject: [PATCH 2/4] removed trailing spaces --- Graphs/LCABinaryLifting.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Graphs/LCABinaryLifting.js b/Graphs/LCABinaryLifting.js index 885a26244f..15ba733b05 100644 --- a/Graphs/LCABinaryLifting.js +++ b/Graphs/LCABinaryLifting.js @@ -81,7 +81,7 @@ class LCABinaryLifting { if (node1 === node2) { return node1 } - + for (let i = this.log - 1; i >= 0; i--) { if (this.up.get(node1).get(i) !== this.up.get(node2).get(i)) { node1 = this.up.get(node1).get(i) From bad596ae125611bf7f4902c0a939df1315a1bee5 Mon Sep 17 00:00:00 2001 From: Adrito-M Date: Sat, 29 Oct 2022 02:52:40 +0530 Subject: [PATCH 3/4] reduced code duplication by importing code from other file --- Graphs/BinaryLifting.js | 2 +- Graphs/LCABinaryLifting.js | 55 +++++--------------------------------- 2 files changed, 8 insertions(+), 49 deletions(-) diff --git a/Graphs/BinaryLifting.js b/Graphs/BinaryLifting.js index 594a5b6676..40d664ba34 100644 --- a/Graphs/BinaryLifting.js +++ b/Graphs/BinaryLifting.js @@ -9,7 +9,7 @@ * Tutorial on Binary Lifting: https://codeforces.com/blog/entry/100826 */ -class BinaryLifting { +export class BinaryLifting { constructor (root, tree) { this.root = root this.connections = new Map() diff --git a/Graphs/LCABinaryLifting.js b/Graphs/LCABinaryLifting.js index 15ba733b05..df0a73f5d4 100644 --- a/Graphs/LCABinaryLifting.js +++ b/Graphs/LCABinaryLifting.js @@ -6,47 +6,21 @@ * Tutorial on Lowest Common Ancestor: https://www.geeksforgeeks.org/lca-in-a-tree-using-binary-lifting-technique */ +import { BinaryLifting } from './BinaryLifting' + class LCABinaryLifting { constructor (root, tree) { - this.root = root - this.connections = new Map() - this.up = new Map() // up[node][i] stores the 2^i-th parent of node + this.binaryLifting = new BinaryLifting(root, tree) + this.log = this.binaryLifting.log + this.connections = this.binaryLifting.connections + this.up = this.binaryLifting.up + this.kthAncestor = this.binaryLifting.kthAncestor this.depth = new Map() // depth[node] stores the depth of node from root this.depth.set(root, 1) - for (const [i, j] of tree) { - this.addEdge(i, j) - } - 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.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 (!this.connections.has(node1)) { - this.addNode(node1) - } - if (!this.connections.has(node2)) { - this.addNode(node2) - } - this.connections.get(node1).add(node2) - this.connections.get(node2).add(node1) - } - 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++) { - 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.get(node)) { if (child !== parent) { this.depth.set(child, this.depth.get(node) + 1) @@ -55,21 +29,6 @@ class LCABinaryLifting { } } - 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) - } - } - return node - } - getLCA (node1, node2) { // We make sure that node1 is the deeper node among node1 and node2 if (this.depth.get(node1) < this.depth.get(node2)) { From 276fdc2b06a3f5c4213d09ea50ee6b6abe8925d2 Mon Sep 17 00:00:00 2001 From: Adrito-M Date: Sat, 29 Oct 2022 03:21:28 +0530 Subject: [PATCH 4/4] made requested changes --- Graphs/LCABinaryLifting.js | 15 ++++++--------- Graphs/test/LCABinaryLifting.test.js | 8 ++++---- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Graphs/LCABinaryLifting.js b/Graphs/LCABinaryLifting.js index df0a73f5d4..211f22f242 100644 --- a/Graphs/LCABinaryLifting.js +++ b/Graphs/LCABinaryLifting.js @@ -8,23 +8,20 @@ import { BinaryLifting } from './BinaryLifting' -class LCABinaryLifting { +class LCABinaryLifting extends BinaryLifting { constructor (root, tree) { - this.binaryLifting = new BinaryLifting(root, tree) - this.log = this.binaryLifting.log - this.connections = this.binaryLifting.connections - this.up = this.binaryLifting.up - this.kthAncestor = this.binaryLifting.kthAncestor + super(root, tree) this.depth = new Map() // depth[node] stores the depth of node from root this.depth.set(root, 1) - this.dfs(root, root) + this.dfsDepth(root, root) } - dfs (node, parent) { + dfsDepth (node, parent) { + // DFS to find depth of every node in the tree for (const child of this.connections.get(node)) { if (child !== parent) { this.depth.set(child, this.depth.get(node) + 1) - this.dfs(child, node) + this.dfsDepth(child, node) } } } diff --git a/Graphs/test/LCABinaryLifting.test.js b/Graphs/test/LCABinaryLifting.test.js index 48f6297383..db77c91352 100644 --- a/Graphs/test/LCABinaryLifting.test.js +++ b/Graphs/test/LCABinaryLifting.test.js @@ -41,8 +41,8 @@ test('Test case 1', () => { [11, 2], [11, 10] ] - const kthAncestors = lcaBinaryLifting(root, graph, queries) - expect(kthAncestors).toEqual([0, 5, 0, 7, 8, 1, 7]) + const lowestCommonAncestors = lcaBinaryLifting(root, graph, queries) + expect(lowestCommonAncestors).toEqual([0, 5, 0, 7, 8, 1, 7]) }) // The graph for Test Case 2 looks like this: @@ -75,6 +75,6 @@ test('Test case 2', () => { [6, 8], [7, 8] ] - const kthAncestors = lcaBinaryLifting(root, graph, queries) - expect(kthAncestors).toEqual([0, 1, 0, 0, 0, 5]) + const lowestCommonAncestors = lcaBinaryLifting(root, graph, queries) + expect(lowestCommonAncestors).toEqual([0, 1, 0, 0, 0, 5]) })