diff --git a/src/algorithms/math/manhattan_distance/README.md b/src/algorithms/math/manhattan_distance/README.md new file mode 100644 index 0000000000..6166e6d076 --- /dev/null +++ b/src/algorithms/math/manhattan_distance/README.md @@ -0,0 +1,27 @@ +# Manhattan Distance + +In mathematics, the **Manhattan distance**, also known as **L1 distance**, **taxicab distance**, or **city block distance**, between two points in a grid-based space is the sum of the absolute differences of their Cartesian coordinates. It is the distance a car would drive in a city laid out in square blocks, where you can only travel along horizontal and vertical streets. + +The name relates to the grid layout of the streets on the island of Manhattan, which causes the shortest path a car could take between two points to be the length of the path along the grid lines. + +![Manhattan vs Euclidean Distance](https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Manhattan_distance.svg/400px-Manhattan_distance.svg.png) + +In the image above, the red, blue, and yellow lines have the same length (12 units) and are the Manhattan distances between the two black points. The green line is the Euclidean distance, which has a length of approximately 8.49 units. + +## Formula + +The Manhattan distance, `d`, between two points `p` and `q` with `n` dimensions (given by Cartesian coordinates) is the sum of the lengths of the projections of the line segment between the points onto the coordinate axes. + +![Manhattan distance formula](https://wikimedia.org/api/rest_v1/media/math/render/svg/ead7631ca37af0070e989f8415b4cd6886229720) + +For example, the Manhattan distance between the two points `(p1, p2)` and `(q1, q2)` is `|p1 - q1| + |p2 - q2|`. + +## Closest Pair Problem + +Finding the closest pair of points in a set is a fundamental problem in computational geometry. While the brute-force approach of calculating the distance between every pair of points takes `O(n^2)` time, a more efficient algorithm exists for Manhattan distance. + +The provided `minimumManhattanDistance` implementation uses an algorithm that runs in `O(n * log(n) * 2^d)` time, where `n` is the number of points and `d` is the number of dimensions. It works by projecting all points onto `2^(d-1)` different orientations. For each orientation, it sorts the points and checks only the adjacent pairs, as the closest pair for that specific projection will be next to each other in the sorted list. This significantly reduces the number of comparisons needed. + +## References + +- [Manhattan Distance on Wikipedia](https://en.wikipedia.org/wiki/Manhattan_distance) \ No newline at end of file diff --git a/src/algorithms/math/manhattan_distance/__tests__/manhattanDistance.test.js b/src/algorithms/math/manhattan_distance/__tests__/manhattanDistance.test.js new file mode 100644 index 0000000000..427a94f395 --- /dev/null +++ b/src/algorithms/math/manhattan_distance/__tests__/manhattanDistance.test.js @@ -0,0 +1,23 @@ +import manhattanDistance from '../manhattanDistance'; + +describe('manhattanDistance', () => { + it('should calculate Manhattan distance between vectors', () => { + expect(manhattanDistance([[1]], [[2]])).toEqual(1); + expect(manhattanDistance([[2]], [[1]])).toEqual(1); + expect(manhattanDistance([[5, 8]], [[7, 3]])).toEqual(7); + expect(manhattanDistance([[5], [8]], [[7], [3]])).toEqual(7); + expect(manhattanDistance([[8, 2, 6]], [[3, 5, 7]])).toEqual(9); + expect(manhattanDistance([[8], [2], [6]], [[3], [5], [7]])).toEqual(9); + expect(manhattanDistance([[[8]], [[2]], [[6]]], [[[3]], [[5]], [[7]]])).toEqual(9); + }); + + it('should throw an error in case if two matrices are of different shapes', () => { + expect(() => manhattanDistance([[1]], [[[2]]])).toThrowError( + 'Matrices have different dimensions', + ); + + expect(() => manhattanDistance([[1]], [[2, 3]])).toThrowError( + 'Matrices have different shapes', + ); + }); +}); diff --git a/src/algorithms/math/manhattan_distance/__tests__/minimumManhattanDistance.test.js b/src/algorithms/math/manhattan_distance/__tests__/minimumManhattanDistance.test.js new file mode 100644 index 0000000000..32f2b049c0 --- /dev/null +++ b/src/algorithms/math/manhattan_distance/__tests__/minimumManhattanDistance.test.js @@ -0,0 +1,65 @@ +import minimumManhattanDistance from '../minimumManhattanDistance'; + +describe('minimumManhattanDistance', () => { + it('should return 0 for an empty array or an array with a single point', () => { + expect(minimumManhattanDistance([])).toBe(0); + expect(minimumManhattanDistance([[1, 2]])).toBe(0); + }); + + it('should find the minimum distance for 2D points', () => { + const points = [ + [1, 1], + [3, 3], + [1, 4], + ]; + // d([1,1], [3,3]) = |1-3|+|1-3| = 4 + // d([1,1], [1,4]) = |1-1|+|1-4| = 3 + // d([3,3], [1,4]) = |3-1|+|3-4| = 3 + expect(minimumManhattanDistance(points)).toBe(3); + }); + + it('should find the minimum distance for 3D points', () => { + const points = [ + [0, 0, 0], + [2, 2, 2], + [3, 3, 3], + [1, 5, 1], + ]; + // d([2,2,2], [3,3,3]) = |2-3|+|2-3|+|2-3| = 1+1+1 = 3 + expect(minimumManhattanDistance(points)).toBe(3); + }); + + it('should return 0 if there are identical points', () => { + const points = [ + [10, 20, 5], + [1, 1, 1], + [4, 8, 12], + [10, 20, 5], + ]; + expect(minimumManhattanDistance(points)).toBe(0); + }); + + it('should work correctly with negative coordinates', () => { + const points = [ + [-1, -1], + [2, 2], + [-3, 3], + [2, 3], + ]; + // d([2,2], [2,3]) = |2-2|+|2-3| = 1 + expect(minimumManhattanDistance(points)).toBe(1); + }); + + it('should handle a larger set of points', () => { + const points = [ + [0, 0], + [100, 100], + [10, 10], + [90, 90], + [49, 50], + [50, 50], + ]; + // d([49,50], [50,50]) = |49-50|+|50-50| = 1 + expect(minimumManhattanDistance(points)).toBe(1); + }); +}); diff --git a/src/algorithms/math/manhattan_distance/manhattanDistance.js b/src/algorithms/math/manhattan_distance/manhattanDistance.js new file mode 100644 index 0000000000..469776c315 --- /dev/null +++ b/src/algorithms/math/manhattan_distance/manhattanDistance.js @@ -0,0 +1,28 @@ +/** + * @typedef {import('../matrix/Matrix.js').Matrix} Matrix + */ + +import * as mtrx from '../matrix/Matrix'; + +/** + * Calculates the manhattan distance between 2 matrices. + * + * @param {Matrix} a + * @param {Matrix} b + * @returns {number} + * @trows {Error} + */ +const manhattanDistance = (a, b) => { + mtrx.validateSameShape(a, b); + + let distanceTotal = 0; + + mtrx.walk(a, (indices, aCellValue) => { + const bCellValue = mtrx.getCellAtIndex(b, indices); + distanceTotal += Math.abs(aCellValue - bCellValue); + }); + + return distanceTotal; +}; + +export default manhattanDistance; diff --git a/src/algorithms/math/manhattan_distance/minimumManhattanDistance.js b/src/algorithms/math/manhattan_distance/minimumManhattanDistance.js new file mode 100644 index 0000000000..b9e1fc9e8e --- /dev/null +++ b/src/algorithms/math/manhattan_distance/minimumManhattanDistance.js @@ -0,0 +1,65 @@ +/** + * Calculates the Manhattan distance between two points (arrays of numbers). + * @param {number[]} a - The first point. + * @param {number[]} b - The second point. + * @returns {number} The Manhattan distance. + */ +const manhattanDistance = (a, b) => { + let distance = 0; + for (let i = 0; i < a.length; i += 1) { + distance += Math.abs(a[i] - b[i]); + } + return distance; +}; + +/** + * Finds the minimum Manhattan distance between any two points in a set. + * This is an implementation of the algorithm for the closest pair problem in + * Manhattan distance, which has a time complexity of O(n * log(n) * 2^d), + * where n is the number of points and d is the number of dimensions. + * + * @param {number[][]} points - An array of points, where each point is an array of numbers. + * @returns {number} The minimum Manhattan distance found. + */ +const minimumManhattanDistance = (points) => { + if (!points || points.length < 2) { + return 0; + } + + const n = points.length; + const d = points[0].length; + let minDistance = Infinity; + + // Generate all 2^d sign patterns (orientations). + // We only need 2^(d-1) because d(p,q) is the same for a mask and its inverse. + const limit = 1 << (d - 1); + for (let mask = 0; mask < limit; mask += 1) { + // Compute projection values for the current orientation. + const projections = []; + for (let i = 0; i < n; i += 1) { + let projectedValue = 0; + for (let j = 0; j < d; j += 1) { + // Determine the sign for the current dimension based on the bitmask. + const sign = ((mask >> j) & 1) ? 1 : -1; + projectedValue += sign * points[i][j]; + } + projections.push({ value: projectedValue, index: i }); + } + + // Sort points based on their projected values. + projections.sort((a, b) => a.value - b.value); + + // Check consecutive pairs in the sorted list. + // The closest pair for this orientation will be adjacent after sorting. + for (let i = 0; i < n - 1; i += 1) { + const pIndex = projections[i].index; + const qIndex = projections[i + 1].index; + const distance = manhattanDistance(points[pIndex], points[qIndex]); + minDistance = Math.min(minDistance, distance); + } + } + + return minDistance; +}; + +export default minimumManhattanDistance;