UNPKG

max-priority-queue-typed

Version:
1,082 lines (965 loc) • 34.5 kB
/** * data-structure-typed * * @author Pablo Zeng * @copyright Copyright (c) 2022 Pablo Zeng <zrwusa@gmail.com> * @license MIT License */ import type { DijkstraResult, EntryCallback, GraphOptions, VertexKey } from '../../types'; import { uuidV4 } from '../../utils'; import { IterableEntryBase } from '../base'; import { IGraph } from '../../interfaces'; import { Heap } from '../heap'; import { Queue } from '../queue'; export abstract class AbstractVertex<V = any> { key: VertexKey; value: V | undefined; protected constructor(key: VertexKey, value?: V) { this.key = key; this.value = value; } } export abstract class AbstractEdge<E = any> { value: E | undefined; weight: number; protected constructor(weight?: number, value?: E) { this.weight = weight !== undefined ? weight : 1; this.value = value; this._hashCode = uuidV4(); } protected _hashCode: string; get hashCode(): string { return this._hashCode; } } /** * Abstract graph over vertices and edges. * @template V - Vertex value type. * @template E - Edge value type. * @template VO - Concrete vertex subclass (extends AbstractVertex<V>). * @template EO - Concrete edge subclass (extends AbstractEdge<E>). * @remarks Time O(1), Space O(1) * @example examples will be generated by unit test */ export abstract class AbstractGraph< V = any, E = any, VO extends AbstractVertex<V> = AbstractVertex<V>, EO extends AbstractEdge<E> = AbstractEdge<E> > extends IterableEntryBase<VertexKey, V | undefined> implements IGraph<V, E, VO, EO> { /** * Construct a graph with runtime defaults. * @param options - `GraphOptions<V>` in `options.graph` (e.g. `vertexValueInitializer`, `defaultEdgeWeight`). * @remarks Time O(1), Space O(1) */ constructor(options?: Partial<Record<string, unknown>>) { super(); const graph = (options as any)?.graph as GraphOptions<V> | undefined; this._options = { defaultEdgeWeight: 1, ...(graph ?? {}) }; } protected _options: GraphOptions<V> = { defaultEdgeWeight: 1 }; get options(): Readonly<GraphOptions<V>> { return this._options; } protected _vertexMap: Map<VertexKey, VO> = new Map<VertexKey, VO>(); get vertexMap(): Map<VertexKey, VO> { return this._vertexMap; } set vertexMap(v: Map<VertexKey, VO>) { this._vertexMap = v; } get size(): number { return this._vertexMap.size; } /** * Create a new vertex instance (implementation specific). * @param key - Vertex identifier. * @param value - Optional payload. * @returns Concrete vertex instance. * @remarks Time O(1), Space O(1) */ abstract createVertex(key: VertexKey, value?: V): VO; /** * Create a new edge instance (implementation specific). * @param srcOrV1 - Source/endpoint A key. * @param destOrV2 - Destination/endpoint B key. * @param weight - Edge weight (defaults may apply). * @param value - Edge payload. * @returns Concrete edge instance. * @remarks Time O(1), Space O(1) */ abstract createEdge(srcOrV1: VertexKey, destOrV2: VertexKey, weight?: number, value?: E): EO; /** * Delete an edge by instance. * @param edge - Edge instance. * @returns Removed edge or `undefined`. * @remarks Time O(1) avg, Space O(1) */ abstract deleteEdge(edge: EO): EO | undefined; /** * Get an edge between two vertices if present. * @param srcOrKey - Source/endpoint A vertex or key. * @param destOrKey - Destination/endpoint B vertex or key. * @returns Edge instance or `undefined`. * @remarks Time O(1) avg, Space O(1) */ abstract getEdge(srcOrKey: VO | VertexKey, destOrKey: VO | VertexKey): EO | undefined; /** * Degree of a vertex in this graph model. * @param vertexOrKey - Vertex or key. * @returns Non-negative integer degree. * @remarks Time O(1) avg, Space O(1) */ abstract degreeOf(vertexOrKey: VO | VertexKey): number; /** * All edges in the graph (unique, order not guaranteed). * @returns Array of edges. * @remarks Time O(E), Space O(E) */ abstract edgeSet(): EO[]; /** * Incident edges of a vertex. * @param vertexOrKey - Vertex or key. * @returns Array of incident edges. * @remarks Time O(deg), Space O(deg) */ abstract edgesOf(vertexOrKey: VO | VertexKey): EO[]; /** * One-step neighbors of a vertex. * @param vertexOrKey - Vertex or key. * @returns Array of neighbor vertices. * @remarks Time O(deg), Space O(deg) */ abstract getNeighbors(vertexOrKey: VO | VertexKey): VO[]; /** * Resolve endpoints of an edge to vertex instances. * @param edge - Edge instance. * @returns `[v1, v2]` or `undefined` if missing. * @remarks Time O(1), Space O(1) */ abstract getEndsOfEdge(edge: EO): [VO, VO] | undefined; /** * Get vertex instance by key. * @param vertexKey - Vertex key. * @returns Vertex instance or `undefined`. * @remarks Time O(1), Space O(1) */ getVertex(vertexKey: VertexKey): VO | undefined { return this._vertexMap.get(vertexKey) || undefined; } /** * Whether a vertex exists. * @param vertexOrKey - Vertex or key. * @returns `true` if present, otherwise `false`. * @remarks Time O(1) avg, Space O(1) */ hasVertex(vertexOrKey: VO | VertexKey): boolean { return this._vertexMap.has(this._getVertexKey(vertexOrKey)); } addVertex(vertex: VO): boolean; addVertex(key: VertexKey, value?: V): boolean; /** * Add a vertex by key/value or by pre-built vertex. * @param keyOrVertex - Vertex key or existing vertex instance. * @param value - Optional payload. * @returns `true` if inserted; `false` when key already exists. * @remarks Time O(1) avg, Space O(1) */ addVertex(keyOrVertex: VertexKey | VO, value?: V): boolean { if (keyOrVertex instanceof AbstractVertex) { return this._addVertex(keyOrVertex); } else { const newVertex = this.createVertex(keyOrVertex, value); return this._addVertex(newVertex); } } /** * Type guard: check if a value is a valid vertex key. * @param potentialKey - Value to test. * @returns `true` if string/number; else `false`. * @remarks Time O(1), Space O(1) */ isVertexKey(potentialKey: any): potentialKey is VertexKey { const potentialKeyType = typeof potentialKey; return potentialKeyType === 'string' || potentialKeyType === 'number'; } /** * Delete a vertex and its incident edges. * @param vertexOrKey - Vertex or key. * @returns `true` if removed; otherwise `false`. * @remarks Time O(deg), Space O(1) */ abstract deleteVertex(vertexOrKey: VO | VertexKey): boolean; /** * Delete multiple vertices. * @param vertexMap - Array of vertices or keys. * @returns `true` if any vertex was removed. * @remarks Time O(sum(deg)), Space O(1) */ removeManyVertices(vertexMap: VO[] | VertexKey[]): boolean { const removed: boolean[] = []; for (const v of vertexMap) { removed.push(this.deleteVertex(v)); } return removed.length > 0; } /** * Whether an edge exists between two vertices. * @param v1 - Endpoint A vertex or key. * @param v2 - Endpoint B vertex or key. * @returns `true` if present; otherwise `false`. * @remarks Time O(1) avg, Space O(1) */ hasEdge(v1: VertexKey | VO, v2: VertexKey | VO): boolean { const edge = this.getEdge(v1, v2); return !!edge; } addEdge(edge: EO): boolean; addEdge(src: VO | VertexKey, dest: VO | VertexKey, weight?: number, value?: E): boolean; /** * Add an edge by instance or by `(src, dest, weight?, value?)`. * @param srcOrEdge - Edge instance or source vertex/key. * @param dest - Destination vertex/key (when adding by pair). * @param weight - Edge weight. * @param value - Edge payload. * @returns `true` if inserted; otherwise `false`. * @remarks Time O(1) avg, Space O(1) */ addEdge(srcOrEdge: VO | VertexKey | EO, dest?: VO | VertexKey, weight?: number, value?: E): boolean { if (srcOrEdge instanceof AbstractEdge) { return this._addEdge(srcOrEdge); } else { if (dest instanceof AbstractVertex || typeof dest === 'string' || typeof dest === 'number') { if (!(this.hasVertex(srcOrEdge) && this.hasVertex(dest))) return false; if (srcOrEdge instanceof AbstractVertex) srcOrEdge = srcOrEdge.key; if (dest instanceof AbstractVertex) dest = dest.key; const newEdge = this.createEdge(srcOrEdge, dest, weight, value); return this._addEdge(newEdge); } else { throw new Error('dest must be a Vertex or vertex key while srcOrEdge is an Edge'); } } } /** * Set the weight of an existing edge. * @param srcOrKey - Source vertex or key. * @param destOrKey - Destination vertex or key. * @param weight - New weight. * @returns `true` if updated; otherwise `false`. * @remarks Time O(1) avg, Space O(1) */ setEdgeWeight(srcOrKey: VertexKey | VO, destOrKey: VertexKey | VO, weight: number): boolean { const edge = this.getEdge(srcOrKey, destOrKey); if (edge) { edge.weight = weight; return true; } else { return false; } } /** * Enumerate simple paths up to a limit. * @param v1 - Source vertex or key. * @param v2 - Destination vertex or key. * @param limit - Maximum number of paths to collect. * @returns Array of paths (each path is an array of vertices). * @remarks Time O(paths) worst-case exponential, Space O(V + paths) */ getAllPathsBetween(v1: VO | VertexKey, v2: VO | VertexKey, limit = 1000): VO[][] { const paths: VO[][] = []; const vertex1 = this._getVertex(v1); const vertex2 = this._getVertex(v2); if (!(vertex1 && vertex2)) { return []; } const stack: { vertex: VO; path: VO[] }[] = []; stack.push({ vertex: vertex1, path: [vertex1] }); while (stack.length > 0) { const { vertex, path } = stack.pop()!; if (vertex === vertex2) { paths.push(path); if (paths.length >= limit) return paths; } const neighbors = this.getNeighbors(vertex); for (const neighbor of neighbors) { if (!path.includes(neighbor)) { const newPath = [...path, neighbor]; stack.push({ vertex: neighbor, path: newPath }); } } } return paths; } /** * Sum the weights along a vertex path. * @param path - Sequence of vertices. * @returns Path weight sum (0 if empty or edge missing). * @remarks Time O(L), Space O(1) where L is path length */ getPathSumWeight(path: VO[]): number { let sum = 0; for (let i = 0; i < path.length; i++) { sum += this.getEdge(path[i], path[i + 1])?.weight || 0; } return sum; } /** * Minimum hops/weight between two vertices. * @param v1 - Source vertex or key. * @param v2 - Destination vertex or key. * @param isWeight - If `true`, compare by path weight; otherwise by hop count. * @returns Minimum cost or `undefined` if missing/unreachable. * @remarks Time O((V + E) log V) weighted / O(V + E) unweighted, Space O(V + E) */ getMinCostBetween(v1: VO | VertexKey, v2: VO | VertexKey, isWeight?: boolean): number | undefined { if (isWeight === undefined) isWeight = false; if (isWeight) { const allPaths = this.getAllPathsBetween(v1, v2); let min = Number.MAX_SAFE_INTEGER; for (const path of allPaths) { min = Math.min(this.getPathSumWeight(path), min); } return min; } else { const vertex2 = this._getVertex(v2); const vertex1 = this._getVertex(v1); if (!(vertex1 && vertex2)) { return undefined; } const visited: Map<VO, boolean> = new Map(); const queue = new Queue<VO>([vertex1]); visited.set(vertex1, true); let cost = 0; while (queue.length > 0) { for (let i = 0, layerSize = queue.length; i < layerSize; i++) { const cur = queue.shift(); if (cur === vertex2) { return cost; } if (cur !== undefined) { const neighbors = this.getNeighbors(cur); for (const neighbor of neighbors) { if (!visited.has(neighbor)) { visited.set(neighbor, true); queue.push(neighbor); } } } } cost++; } return undefined; } } /** * Minimum path (as vertex sequence) between two vertices. * @param v1 - Source vertex or key. * @param v2 - Destination vertex or key. * @param isWeight - If `true`, compare by path weight; otherwise by hop count. * @param isDFS - For weighted mode only: if `true`, brute-force all paths; if `false`, use Dijkstra. * @returns Vertex sequence, or `undefined`/empty when unreachable depending on branch. * @remarks Time O((V + E) log V) weighted / O(V + E) unweighted, Space O(V + E) */ getMinPathBetween(v1: VO | VertexKey, v2: VO | VertexKey, isWeight?: boolean, isDFS = false): VO[] | undefined { if (isWeight === undefined) isWeight = false; if (isWeight) { if (isDFS) { const allPaths = this.getAllPathsBetween(v1, v2, 10000); let min = Number.MAX_SAFE_INTEGER; let minIndex = -1; let index = 0; for (const path of allPaths) { const pathSumWeight = this.getPathSumWeight(path); if (pathSumWeight < min) { min = pathSumWeight; minIndex = index; } index++; } return allPaths[minIndex] || undefined; } else { /** * Dijkstra (binary-heap) shortest paths for non-negative weights. * @param src - Source vertex or key. * @param dest - Optional destination for early stop. * @param getMinDist - If `true`, compute global minimum distance. * @param genPaths - If `true`, also generate path arrays. * @returns Result bag or `undefined` if source missing. * @remarks Time O((V + E) log V), Space O(V + E) */ return this.dijkstra(v1, v2, true, true)?.minPath ?? []; } } else { let minPath: VO[] = []; const vertex1 = this._getVertex(v1); const vertex2 = this._getVertex(v2); if (!(vertex1 && vertex2)) return []; const dfs = (cur: VO, dest: VO, visiting: Set<VO>, path: VO[]) => { visiting.add(cur); if (cur === dest) { minPath = [vertex1, ...path]; return; } const neighbors = this.getNeighbors(cur); for (const neighbor of neighbors) { if (!visiting.has(neighbor)) { path.push(neighbor); dfs(neighbor, dest, visiting, path); path.pop(); } } visiting.delete(cur); }; dfs(vertex1, vertex2, new Set<VO>(), []); return minPath; } } /** * Dijkstra without heap (array-based selection). * @param src - Source vertex or key. * @param dest - Optional destination for early stop. * @param getMinDist - If `true`, compute global minimum distance. * @param genPaths - If `true`, also generate path arrays. * @returns Result bag or `undefined` if source missing. * @remarks Time O(V^2 + E), Space O(V + E) */ dijkstraWithoutHeap( src: VO | VertexKey, dest: VO | VertexKey | undefined = undefined, getMinDist: boolean = false, genPaths: boolean = false ): DijkstraResult<VO> | undefined { let minDist = Number.MAX_SAFE_INTEGER; let minDest: VO | undefined = undefined; let minPath: VO[] = []; const paths: VO[][] = []; const vertexMap = this._vertexMap; const distMap: Map<VO, number> = new Map(); const seen: Set<VO> = new Set(); const preMap: Map<VO, VO | undefined> = new Map(); const srcVertex = this._getVertex(src); const destVertex = dest ? this._getVertex(dest) : undefined; if (!srcVertex) { return undefined; } for (const vertex of vertexMap) { const vertexOrKey = vertex[1]; if (vertexOrKey instanceof AbstractVertex) distMap.set(vertexOrKey, Number.MAX_SAFE_INTEGER); } distMap.set(srcVertex, 0); preMap.set(srcVertex, undefined); const getMinOfNoSeen = () => { let min = Number.MAX_SAFE_INTEGER; let minV: VO | undefined = undefined; for (const [key, value] of distMap) { if (!seen.has(key)) { if (value < min) { min = value; minV = key; } } } return minV; }; const getPaths = (minV: VO | undefined) => { for (const vertex of vertexMap) { const vertexOrKey = vertex[1]; if (vertexOrKey instanceof AbstractVertex) { const path: VO[] = [vertexOrKey]; let parent = preMap.get(vertexOrKey); while (parent) { path.push(parent); parent = preMap.get(parent); } const reversed = path.reverse(); if (vertex[1] === minV) minPath = reversed; paths.push(reversed); } } }; for (let i = 1; i < vertexMap.size; i++) { const cur = getMinOfNoSeen(); if (cur) { seen.add(cur); if (destVertex && destVertex === cur) { if (getMinDist) { minDist = distMap.get(destVertex) || Number.MAX_SAFE_INTEGER; } if (genPaths) { getPaths(destVertex); } return { distMap, preMap, seen, paths, minDist, minPath }; } const neighbors = this.getNeighbors(cur); for (const neighbor of neighbors) { if (!seen.has(neighbor)) { const edge = this.getEdge(cur, neighbor); if (edge) { const curFromMap = distMap.get(cur); const neighborFromMap = distMap.get(neighbor); if (curFromMap !== undefined && neighborFromMap !== undefined) { if (edge.weight + curFromMap < neighborFromMap) { distMap.set(neighbor, edge.weight + curFromMap); preMap.set(neighbor, cur); } } } } } } } if (getMinDist) distMap.forEach((d, v) => { if (v !== srcVertex) { if (d < minDist) { minDist = d; if (genPaths) minDest = v; } } }); if (genPaths) getPaths(minDest); return { distMap, preMap, seen, paths, minDist, minPath }; } dijkstra( src: VO | VertexKey, dest: VO | VertexKey | undefined = undefined, getMinDist: boolean = false, genPaths: boolean = false ): DijkstraResult<VO> | undefined { let minDist = Number.MAX_SAFE_INTEGER; let minDest: VO | undefined = undefined; let minPath: VO[] = []; const paths: VO[][] = []; const vertexMap = this._vertexMap; const distMap: Map<VO, number> = new Map(); const seen: Set<VO> = new Set(); const preMap: Map<VO, VO | undefined> = new Map(); const srcVertex = this._getVertex(src); const destVertex = dest ? this._getVertex(dest) : undefined; if (!srcVertex) return undefined; for (const vertex of vertexMap) { const vertexOrKey = vertex[1]; if (vertexOrKey instanceof AbstractVertex) distMap.set(vertexOrKey, Number.MAX_SAFE_INTEGER); } const heap = new Heap<{ key: number; value: VO }>([], { comparator: (a, b) => a.key - b.key }); heap.add({ key: 0, value: srcVertex }); distMap.set(srcVertex, 0); preMap.set(srcVertex, undefined); const getPaths = (minV: VO | undefined) => { for (const vertex of vertexMap) { const vertexOrKey = vertex[1]; if (vertexOrKey instanceof AbstractVertex) { const path: VO[] = [vertexOrKey]; let parent = preMap.get(vertexOrKey); while (parent) { path.push(parent); parent = preMap.get(parent); } const reversed = path.reverse(); if (vertex[1] === minV) minPath = reversed; paths.push(reversed); } } }; while (heap.size > 0) { const curHeapNode = heap.poll(); const dist = curHeapNode?.key; const cur = curHeapNode?.value; if (dist !== undefined) { if (cur) { seen.add(cur); if (destVertex && destVertex === cur) { if (getMinDist) { minDist = distMap.get(destVertex) || Number.MAX_SAFE_INTEGER; } if (genPaths) { getPaths(destVertex); } return { distMap, preMap, seen, paths, minDist, minPath }; } const neighbors = this.getNeighbors(cur); for (const neighbor of neighbors) { if (!seen.has(neighbor)) { const weight = this.getEdge(cur, neighbor)?.weight; if (typeof weight === 'number') { const distSrcToNeighbor = distMap.get(neighbor); if (distSrcToNeighbor !== undefined) { if (dist + weight < distSrcToNeighbor) { heap.add({ key: dist + weight, value: neighbor }); preMap.set(neighbor, cur); distMap.set(neighbor, dist + weight); } } } } } } } } if (getMinDist) { distMap.forEach((d, v) => { if (v !== srcVertex) { if (d < minDist) { minDist = d; if (genPaths) minDest = v; } } }); } if (genPaths) { getPaths(minDest); } return { distMap, preMap, seen, paths, minDist, minPath }; } /** * Bellman-Ford single-source shortest paths with option to scan negative cycles. * @param src - Source vertex or key. * @param scanNegativeCycle - If `true`, also detect negative cycles. * @param getMin - If `true`, compute global minimum distance. * @param genPath - If `true`, generate path arrays via predecessor map. * @returns Result bag including distances, predecessors, and optional cycle flag. * @remarks Time O(V * E), Space O(V + E) */ bellmanFord(src: VO | VertexKey, scanNegativeCycle?: boolean, getMin?: boolean, genPath?: boolean) { if (getMin === undefined) getMin = false; if (genPath === undefined) genPath = false; const srcVertex = this._getVertex(src); const paths: VO[][] = []; const distMap: Map<VO, number> = new Map(); const preMap: Map<VO, VO> = new Map(); let min = Number.MAX_SAFE_INTEGER; let minPath: VO[] = []; let hasNegativeCycle: boolean | undefined; if (scanNegativeCycle) hasNegativeCycle = false; if (!srcVertex) return { hasNegativeCycle, distMap, preMap, paths, min, minPath }; const vertexMap = this._vertexMap; const numOfVertices = vertexMap.size; const edgeMap = this.edgeSet(); const numOfEdges = edgeMap.length; this._vertexMap.forEach(vertex => { distMap.set(vertex, Number.MAX_SAFE_INTEGER); }); distMap.set(srcVertex, 0); for (let i = 1; i < numOfVertices; ++i) { for (let j = 0; j < numOfEdges; ++j) { const ends = this.getEndsOfEdge(edgeMap[j]); if (ends) { const [s, d] = ends; const weight = edgeMap[j].weight; const sWeight = distMap.get(s); const dWeight = distMap.get(d); if (sWeight !== undefined && dWeight !== undefined) { if (distMap.get(s) !== Number.MAX_SAFE_INTEGER && sWeight + weight < dWeight) { distMap.set(d, sWeight + weight); if (genPath) preMap.set(d, s); } } } } } let minDest: VO | undefined = undefined; if (getMin) { distMap.forEach((d, v) => { if (v !== srcVertex) { if (d < min) { min = d; if (genPath) minDest = v; } } }); } if (genPath) { for (const vertex of vertexMap) { const vertexOrKey = vertex[1]; if (vertexOrKey instanceof AbstractVertex) { const path: VO[] = [vertexOrKey]; let parent = preMap.get(vertexOrKey); while (parent !== undefined) { path.push(parent); parent = preMap.get(parent); } const reversed = path.reverse(); if (vertex[1] === minDest) minPath = reversed; paths.push(reversed); } } } for (let j = 0; j < numOfEdges; ++j) { const ends = this.getEndsOfEdge(edgeMap[j]); if (ends) { const [s] = ends; const weight = edgeMap[j].weight; const sWeight = distMap.get(s); if (sWeight) { if (sWeight !== Number.MAX_SAFE_INTEGER && sWeight + weight < sWeight) hasNegativeCycle = true; } } } return { hasNegativeCycle, distMap, preMap, paths, min, minPath }; } /** * Floyd–Warshall all-pairs shortest paths. * @returns `{ costs, predecessor }` matrices. * @remarks Time O(V^3), Space O(V^2) */ floydWarshall(): { costs: number[][]; predecessor: (VO | undefined)[][] } { const idAndVertices = [...this._vertexMap]; const n = idAndVertices.length; const costs: number[][] = []; const predecessor: (VO | undefined)[][] = []; for (let i = 0; i < n; i++) { costs[i] = []; predecessor[i] = []; for (let j = 0; j < n; j++) { predecessor[i][j] = undefined; } } for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { costs[i][j] = this.getEdge(idAndVertices[i][1], idAndVertices[j][1])?.weight || Number.MAX_SAFE_INTEGER; } } for (let k = 0; k < n; k++) { for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { if (costs[i][j] > costs[i][k] + costs[k][j]) { costs[i][j] = costs[i][k] + costs[k][j]; predecessor[i][j] = idAndVertices[k][1]; } } } } return { costs, predecessor }; } /** * Enumerate simple cycles (may be expensive). * @param isInclude2Cycle - If `true`, include 2-cycles when graph semantics allow. * @returns Array of cycles (each as array of vertex keys). * @remarks Time exponential in worst-case, Space O(V + E) */ getCycles(isInclude2Cycle: boolean = false): VertexKey[][] { const cycles: VertexKey[][] = []; const visited: Set<VO> = new Set(); const dfs = (vertex: VO, currentPath: VertexKey[], visited: Set<VO>) => { if (visited.has(vertex)) { if ( ((!isInclude2Cycle && currentPath.length > 2) || (isInclude2Cycle && currentPath.length >= 2)) && currentPath[0] === vertex.key ) { cycles.push([...currentPath]); } return; } visited.add(vertex); currentPath.push(vertex.key); for (const neighbor of this.getNeighbors(vertex)) { if (neighbor) dfs(neighbor, currentPath, visited); } visited.delete(vertex); currentPath.pop(); }; for (const vertex of this.vertexMap.values()) { dfs(vertex, [], visited); } const uniqueCycles = new Map<string, VertexKey[]>(); for (const cycle of cycles) { const sorted = [...cycle].sort().toString(); if (uniqueCycles.has(sorted)) continue; else { uniqueCycles.set(sorted, cycle); } } /** * Map entries to an array via callback. * @param callback - `(key, value, index, self) => T`. * @param thisArg - Optional `this` for callback. * @returns Mapped results. * @remarks Time O(V), Space O(V) */ return [...uniqueCycles].map(cycleString => cycleString[1]); } /** * Induced-subgraph filter: keep vertices where `predicate(key, value)` is true, * and only keep edges whose endpoints both survive. * @param predicate - `(key, value, index, self) => boolean`. * @param thisArg - Optional `this` for callback. * @returns A new graph of the same concrete class (`this` type). * @remarks Time O(V + E), Space O(V + E) */ filter(predicate: EntryCallback<VertexKey, V | undefined, boolean>, thisArg?: any): this { const filtered: [VertexKey, V | undefined][] = []; let index = 0; for (const [key, value] of this) { if (predicate.call(thisArg, key, value, index, this)) { filtered.push([key, value]); } index++; } return this._createLike(filtered, this._snapshotOptions()); } /** * Preserve the old behavior: return filtered entries as an array. * @remarks Time O(V), Space O(V) */ filterEntries( predicate: EntryCallback<VertexKey, V | undefined, boolean>, thisArg?: any ): [VertexKey, V | undefined][] { const filtered: [VertexKey, V | undefined][] = []; let index = 0; for (const [key, value] of this) { if (predicate.call(thisArg, key, value, index, this)) { filtered.push([key, value]); } index++; } return filtered; } map<T>(callback: EntryCallback<VertexKey, V | undefined, T>, thisArg?: any): T[] { const mapped: T[] = []; let index = 0; for (const [key, value] of this) { mapped.push(callback.call(thisArg, key, value, index, this)); index++; } return mapped; } /** * Create a deep clone of the graph with the same species. * @remarks Time O(V + E), Space O(V + E) */ /** * Create a deep clone of the graph with the same species. * @returns A new graph of the same concrete class (`this` type). * @remarks Time O(V + E), Space O(V + E) */ clone(): this { return this._createLike(undefined, this._snapshotOptions()); } // ===== Same-species factory & cloning helpers ===== /** * Internal iterator over `[key, value]` entries in insertion order. * @returns Iterator of `[VertexKey, V | undefined]`. * @remarks Time O(V), Space O(1) */ protected *_getIterator(): IterableIterator<[VertexKey, V | undefined]> { for (const vertex of this._vertexMap.values()) { yield [vertex.key, vertex.value]; } } /** * Capture configuration needed to reproduce the current graph. * Currently the graph has no runtime options, so we return an empty object. */ /** * Capture configuration needed to reproduce the current graph. * @returns Options bag (opaque to callers). * @remarks Time O(1), Space O(1) */ protected _snapshotOptions(): Record<string, unknown> { return { graph: { ...this._options } }; } /** * Create an empty graph instance of the same concrete species (Directed/Undirected/etc). * @remarks Time O(1), Space O(1) */ /** * Create an empty graph instance of the same concrete species. * @param _options - Snapshot options from `_snapshotOptions()`. * @returns A new empty graph instance of `this` type. * @remarks Time O(1), Space O(1) */ protected _createInstance(_options?: Partial<Record<string, unknown>>): this { const Ctor: any = (this as any).constructor; const instance: this = new Ctor(); const graph = (_options as any)?.graph as GraphOptions<V> | undefined; if (graph) (instance as any)._options = { ...(instance as any)._options, ...graph }; else (instance as any)._options = { ...(instance as any)._options, ...(this as any)._options }; return instance; } /** * Create a same-species graph populated with the given entries. * Also preserves edges between kept vertices from the source graph. * @remarks Time O(V + E), Space O(V + E) */ /** * Create a same-species graph populated with entries; preserves edges among kept vertices. * @param iter - Optional entries to seed the new graph. * @param options - Snapshot options. * @returns A new graph of `this` type. * @remarks Time O(V + E), Space O(V + E) */ protected _createLike(iter?: Iterable<[VertexKey, V | undefined]>, options?: Partial<Record<string, unknown>>): this { const g = this._createInstance(options); // 1) Add vertices if (iter) { for (const [k, v] of iter) { (g as any).addVertex(k as VertexKey, v as V | undefined); } } else { for (const [k, v] of this) { (g as any).addVertex(k as VertexKey, v as V | undefined); } } // 2) Add edges whose endpoints exist in the new graph const edges = this.edgeSet(); for (const e of edges as any[]) { const ends = this.getEndsOfEdge(e as any) as unknown as [any, any] | undefined; if (!ends) continue; const [va, vb] = ends; const ka = (va as any).key as VertexKey; const kb = (vb as any).key as VertexKey; const hasA = (g as any).hasVertex ? (g as any).hasVertex(ka) : false; const hasB = (g as any).hasVertex ? (g as any).hasVertex(kb) : false; if (hasA && hasB) { const w = (e as any).weight; const val = (e as any).value; const newEdge = (g as any).createEdge(ka, kb, w, val); (g as any)._addEdge(newEdge); } } return g; } /** * Internal hook to attach an edge into adjacency structures. * @param edge - Edge instance. * @returns `true` if inserted; otherwise `false`. * @remarks Time O(1) avg, Space O(1) */ protected abstract _addEdge(edge: EO): boolean; /** * Insert a pre-built vertex into the graph. * @param newVertex - Concrete vertex instance. * @returns `true` if inserted; `false` if key already exists. * @remarks Time O(1) avg, Space O(1) */ protected _addVertex(newVertex: VO): boolean { if (this.hasVertex(newVertex)) { return false; } this._vertexMap.set(newVertex.key, newVertex); return true; } /** * Resolve a vertex key or instance to the concrete vertex instance. * @param vertexOrKey - Vertex key or existing vertex. * @returns Vertex instance or `undefined`. * @remarks Time O(1), Space O(1) */ protected _getVertex(vertexOrKey: VertexKey | VO): VO | undefined { const vertexKey = this._getVertexKey(vertexOrKey); return this._vertexMap.get(vertexKey) || undefined; } /** * Resolve a vertex key from a key or vertex instance. * @param vertexOrKey - Vertex key or existing vertex. * @returns The vertex key. * @remarks Time O(1), Space O(1) */ protected _getVertexKey(vertexOrKey: VO | VertexKey): VertexKey { return vertexOrKey instanceof AbstractVertex ? vertexOrKey.key : vertexOrKey; } }