Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/algorithms/math/manhattan_distance/README.md
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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',
);
});
});
Original file line number Diff line number Diff line change
@@ -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);
});
});
28 changes: 28 additions & 0 deletions src/algorithms/math/manhattan_distance/manhattanDistance.js
Original file line number Diff line number Diff line change
@@ -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;
65 changes: 65 additions & 0 deletions src/algorithms/math/manhattan_distance/minimumManhattanDistance.js
Original file line number Diff line number Diff line change
@@ -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;