diff --git a/README.md b/README.md index b83faf8..1cfb56b 100644 --- a/README.md +++ b/README.md @@ -84,10 +84,16 @@ isCyclic(graph); | graph / network | A system of vertices connected in pairs by edges. ([Wikipedia]()) | | vertex / node | The fundamental unit of which graphs are formed. ([Wikipedia]()) | | edge / link / branch / arc | A connection between two vertices in a graph. ([Wikipedia]()) | +| order | The number of vertices in a graph. | +| size | The number of edges in a graph. | | weighted graph | A graph with a numeric weight associated with each edge. ([Wolfram MathWorld](https://mathworld.wolfram.com/WeightedGraph.html)) | -| directed graph | A graph where each edge has a direction. ([Wikipedia](https://en.wikipedia.org/wiki/Directed_graph)) | +| directed graph | A graph where the edges have direction. ([Wikipedia](https://en.wikipedia.org/wiki/Directed_graph)) | +| undirected graph | A graph where the edges do not have a direction. ([Math Insight](https://mathinsight.org/definition/undirected_graph)) | | path | A sequence of edges that connect a set of vertices where each vertex is distinct. ([Wikipedia]()) | -| directed path | A path where all edges are orientated in the same direction. ([Wikipedia]()) | +| directed path | A path where all edges are orientated in the same direction. | +| undirected path | A path where the edges can be orientated in any direction. | +| loop / buckle | An edge which starts and ends at the same node. ([Wikipedia](<(https://en.wikipedia.org/wiki/Loop_(graph_theory))>) | +| cycle | A path that starts and ends at the same node. ([Wikipedia]()) | ## Types @@ -336,6 +342,55 @@ declare const isCyclic: (graph: Graph) => boolean; Returns `true` if the graph provided contains any [cycles]() (including "loops" — when an edge that starts and ends at the same vertex), otherwise returns `false`. +### isUndirected + +```ts +declare const isUndirected: (graph: Graph) => boolean; +``` + +Returns `true` if the graph can be considered an [undirected graph](https://mathinsight.org/definition/undirected_graph) — every edge in the graph (from vertex A to B) has a mutual edge (from vertex B to A) with an equal weight. Loops are considered bidirectional and are allow in a undirected graph. + +```js +let graph = create(2, (i) => String.fromCharCode(65 + i)); +//=> Graph { "A", "B" } + +isUndirected(graph); +//=> true + +graph = addEdge(graph, ["A", "B"]); +//=> Graph { "A" -> "B" } + +isUndirected(graph); +//=> false + +graph = addEdge(graph, ["B", "A"]); +//=> Graph { "A" <-> "B" } + +isUndirected(graph); +//=> true +``` + +### makeUndirected + +```ts +declare const makeUndirected: (graph: Graph, merge?: (a: number, b: number) => number) => Graph; +``` + +Converts a directed graph to an undirected graph by either adding edges to make them mutual or balancing the weights of mutual edges that aren't already equal. + +The `merge` function is used to determine the weight of edges in cases where mutual edges with differing weights already exist. If not provide the default method is to use the highest of the two edge weights (`(a, b) => Math.max(a, b)`). + +```js +let graph = create(3, (i) => String.fromCharCode(65 + i)); +//=> Graph { "A", "B", "C" } + +graph = addEdge(graph, ["A", "B"]); +//=> Graph { "A" -> "B", "C" } + +graph = makeUndirected(graph); +//=> Graph { "A" <-> "B", "C" } +``` + ### order ```ts @@ -566,3 +621,19 @@ declare const vertices: (graph: Graph) => Set; ``` Returns the vertices in the graph. + +### vertexPairs + +```ts +declare const vertexPairs: (graph: Graph) => Set<[string, string]>; +``` + +Returns a list of all pairs of vertices in the graph irrespective of the edges present in the graph. + +```js +let graph = create(3, (i) => String.fromCharCode(65 + i)); +//=> Graph { "A", "B", "C" } + +vertexPairs(graph); +//=> Set { ["A", "A"], ["A", "B"], ["A", "C"], ["B", "B"], ["B", "C"], ["C", "C"] } +``` diff --git a/index.ts b/index.ts index a02529e..869737d 100644 --- a/index.ts +++ b/index.ts @@ -189,6 +189,31 @@ const _isCyclic = ( return false; }; +const isUndirected = (graph: Graph): boolean => { + for (let [u, v] of vertexPairs(graph)) { + if (graph[u][v] !== graph[v][u]) return false; + } + return true; +}; + +const makeUndirected = ( + graph: Graph, + merge: (a: number, b: number) => number = (a, b) => Math.max(a, b), +): Graph => { + const result = clone(graph); + + for (let [u, v] of vertexPairs(graph)) { + const weight = + u === v || graph[u][v] === 0 || graph[v][u] === 0 + ? graph[u][v] || graph[v][u] + : merge(graph[u][v], graph[v][u]); + result[u][v] = weight; + result[v][u] = weight; + } + + return result; +}; + const order = (graph: Graph): number => { let result = 0; for (let u in graph) result += 1; @@ -335,6 +360,19 @@ const transpose = (graph: Graph): Graph => { const vertices = (graph: Graph): Set => new Set(Object.keys(graph)); +const vertexPairs = (graph: Graph): Set<[string, string]> => { + const result: Set<[string, string]> = new Set(); + const vertices = Object.keys(graph); + + for (let u = 0; u < vertices.length; u++) { + for (let v = u; v < vertices.length; v++) { + result.add([vertices[u], vertices[v]]); + } + } + + return result; +}; + export { D3Graph, Graph, @@ -351,6 +389,8 @@ export { getEdge, indegree, isCyclic, + isUndirected, + makeUndirected, order, outdegree, parents, @@ -362,4 +402,5 @@ export { topologicalSort, transpose, vertices, + vertexPairs, }; diff --git a/test.ts b/test.ts index c3d8764..10bb1c5 100644 --- a/test.ts +++ b/test.ts @@ -12,6 +12,8 @@ import { getEdge, indegree, isCyclic, + isUndirected, + makeUndirected, order, outdegree, parents, @@ -23,6 +25,7 @@ import { topologicalSort, transpose, vertices, + vertexPairs, } from "./index"; import test from "tape"; @@ -400,6 +403,80 @@ test("isCyclic", (t) => { ); }); +test("isUndirected", (t) => { + t.plan(4); + + t.equal(isUndirected({}), true); + + t.equal(isUndirected({ a: { a: 1 } }), true); + + t.equal( + isUndirected({ + a: { a: 1, b: 1 }, + b: { a: 0, b: 0 }, + }), + false, + ); + + t.equal( + isUndirected({ + a: { a: 0, b: 1 }, + b: { a: 1, b: 0 }, + }), + true, + ); +}); + +test("makeUndirected", (t) => { + t.plan(5); + + t.deepEqual(makeUndirected({}), {}); + + t.deepEqual(makeUndirected({ a: { a: 1 } }), { a: { a: 1 } }); + + t.deepEqual( + makeUndirected({ + a: { a: 1, b: 1, c: 0 }, + b: { a: 0, b: 0, c: 1 }, + c: { a: 0, b: 0, c: 0 }, + }), + { + a: { a: 1, b: 1, c: 0 }, + b: { a: 1, b: 0, c: 1 }, + c: { a: 0, b: 1, c: 0 }, + }, + ); + + t.deepEqual( + makeUndirected({ + a: { a: 0.5, b: -1, c: 0 }, + b: { a: 0, b: 0, c: -1 }, + c: { a: 0, b: 0, c: 0 }, + }), + { + a: { a: 0.5, b: -1, c: 0 }, + b: { a: -1, b: 0, c: -1 }, + c: { a: 0, b: -1, c: 0 }, + }, + ); + + t.deepEqual( + makeUndirected( + { + a: { a: 2, b: 3, c: 0 }, + b: { a: 2, b: 0, c: 1 }, + c: { a: 0, b: 0, c: 0 }, + }, + (a, b) => a * b, + ), + { + a: { a: 2, b: 6, c: 0 }, + b: { a: 6, b: 0, c: 1 }, + c: { a: 0, b: 1, c: 0 }, + }, + ); +}); + test("order", (t) => { t.plan(3); @@ -748,3 +825,23 @@ test("vertices", (t) => { new Set(["a", "b", "c"]), ); }); + +test("vertexPairs", (t) => { + t.plan(1); + + t.deepEqual( + vertexPairs({ + a: { a: 0, b: 0, c: 0 }, + b: { a: 0, b: 0, c: 0 }, + c: { a: 0, b: 0, c: 0 }, + }), + new Set([ + ["a", "a"], + ["a", "b"], + ["a", "c"], + ["b", "b"], + ["b", "c"], + ["c", "c"], + ]), + ); +});