diff --git a/README.md b/README.md index c75b8b8..926e765 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ A collection of JavaScript problems and solutions for studying algorithms. - [Graph Valid Tree](src/graph/graph-valid-tree.js) - [Number of Connected Components in an Undirected Graph](src/graph/number-of-connected-components-in-an-undirected-graph.js) - [Reconstruct Itinerary](src/graph/reconstruct-itinerary.js) +- [Evaluate Division](src/graph/evaluate-division.js) ### Linked List diff --git a/src/graph/__tests__/evaluate-division-test.js b/src/graph/__tests__/evaluate-division-test.js new file mode 100644 index 0000000..5983c94 --- /dev/null +++ b/src/graph/__tests__/evaluate-division-test.js @@ -0,0 +1,26 @@ +import { assert } from 'chai'; +import { serializeUndirectedGraph, deserializeUndirectedGraph } from 'utils/graph-util'; +import calcEquation from '../evaluate-division'; + +describe('Evaluate Division', () => { + const testCases = [ + [ + [['a', 'b'], ['b', 'c']], + [2.0, 3.0], + [['a', 'c'], ['b', 'a'], ['a', 'e'], ['a', 'a'], ['x', 'x']], + [6.0, 0.5, -1.0, 1.0, -1.0], + ], + [[['a', 'b'], ['c', 'b']], [2.0, 4.0], [['a', 'c'], ['b', 'a']], [0.5, 0.5]], + ]; + + testCases.forEach((testCase, index) => { + it(`should evaluate the division ${index}`, () => { + const equations = testCase[0]; + const values = testCase[1]; + const queries = testCase[2]; + const expected = testCase[3]; + const actual = calcEquation(equations, values, queries); + assert.deepEqual(actual, expected); + }); + }); +}); diff --git a/src/graph/evaluate-division.js b/src/graph/evaluate-division.js new file mode 100644 index 0000000..3688311 --- /dev/null +++ b/src/graph/evaluate-division.js @@ -0,0 +1,118 @@ +/** + * Equations are given in the format A / B = k, where A and B are variables represented as strings, + * and k is a real number (floating point number). Given some queries, return the answers. + * + * If the answer does not exist, return -1.0. + * + * Example: + * Given a / b = 2.0, b / c = 3.0. + * queries are: a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ? . + * return [6.0, 0.5, -1.0, 1.0, -1.0 ]. + * + * The input is: vector> equations, vector& values, + * vector> queries , where equations.size() == values.size(), + * and the values are positive. This represents the equations. Return vector. + * + * According to the example above: + * + * equations = [ ["a", "b"], ["b", "c"] ], + * values = [2.0, 3.0], + * queries = [ ["a", "c"], ["b", "a"], ["a", "e"], ["a", "a"], ["x", "x"] ]. + * + * The input is always valid. You may assume that evaluating the queries will result in no division + * by zero and there is no contradiction. + */ + +/** + * @param {string[][]} equations + * @param {number[]} values + * @param {string[][]} queries + * @return {number[]} + */ +const calcEquation = (equations, values, queries) => { + const result = []; + + // Step 1. Build the undirected graph with adjacency list + const adjList = buildGraph(equations, values); + + // Step 2. For each query, try to find a path in the graph + // that can link the nodes in the query + for (let i = 0; i < queries.length; i++) { + const [from, to] = queries[i]; + const value = dfs(adjList, from, to, 1, new Set()); + + // If value is null, that means there's no such path + result.push(value ? value : -1.0); + + if (value) { + // Update the graph to avoid duplicate computation + adjList.get(from).set(to, value); + adjList.get(to).set(from, 1 / value); + } + } + + return result; +}; + +/** + * @param {Map>} adjList + * @param {string} node + * @param {string} to + * @param {number} product + * @param {Set} visited + */ +const dfs = (adjList, node, to, product, visited) => { + if (!adjList.has(node)) { + return null; // Dead end + } + + visited.add(node); // Mark the current node as visited + + const neighbors = [...adjList.get(node).keys()]; + + for (let i = 0; i < neighbors.length; i++) { + const v = neighbors[i]; + const current = product * adjList.get(node).get(v); + + if (v === to) { + // Found the path, return the product + return current; + } + + if (!visited.has(v)) { + // Continue to search for the path + const value = dfs(adjList, v, to, current, visited); + + if (value) { + return value; + } + } + } + + return null; +}; + +const buildGraph = (equations, values) => { + const adjList = new Map(); + + for (let i = 0; i < equations.length; i++) { + const [from, to] = equations[i]; + const value = values[i]; + + if (!adjList.has(from)) { + adjList.set(from, new Map()); + } + + if (!adjList.has(to)) { + adjList.set(to, new Map()); + } + + // Build the undirected graph + adjList.get(from).set(to, value); + adjList.get(to).set(from, 1 / value); + } + + return adjList; +}; + +export default calcEquation;