More graphs and primes!
This commit is contained in:
6 files changed
+301
-157
No files matched your search
+1
-17
@@ -1,19 +1,3 @@
|
||||
import { Graph } from '../tools/graph.ts'
|
||||
import { g1, g2 } from '../tools/graph/_graph-samples.ts'
|
||||
|
||||
// TODO: Add real tests!
|
||||
|
||||
const G = new Graph<string, string>()
|
||||
|
||||
const a = G.createVertex('A')
|
||||
const b = G.createVertex('B')
|
||||
const c = G.createVertex('C')
|
||||
|
||||
G.createEdge(a, b, 'a-b')
|
||||
G.createEdge(b, c, 'b-c')
|
||||
G.createEdge(c, a, 'c-a')
|
||||
|
||||
// console.log(G)
|
||||
console.log(JSON.stringify(G))
|
||||
console.log(Graph.fromJSON(
|
||||
JSON.parse(JSON.stringify(G, null, 4))
|
||||
))
|
||||
@@ -1,120 +0,0 @@
|
||||
import { Graph, VertexType, EdgeType } from "./graph.ts";
|
||||
|
||||
type WeightFn<Graph> = (v: VertexType<Graph>, e: EdgeType<Graph>) => number
|
||||
type SearchFn<Graph> = (v: VertexType<Graph>, e: EdgeType<Graph> | null, pathmap: Map<typeof v, typeof e>) => boolean | void
|
||||
|
||||
export class GraphSolver<vData, eData> {
|
||||
G: Graph<vData, eData>
|
||||
|
||||
constructor(G: Graph<vData, eData>) {
|
||||
this.G = G
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs DFS on a graph.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-31
|
||||
*/
|
||||
#DFS(root: VertexType<typeof this.G>, search: (v: typeof root) => typeof root | void) {
|
||||
const stack = [root]
|
||||
const discovered = new WeakSet<typeof root>()
|
||||
discovered.add(root)
|
||||
|
||||
while (stack.length !== 0) {
|
||||
const v = stack.pop()!
|
||||
const value = search(v)
|
||||
if (value) return value
|
||||
for (const E of v.adjacentEdges) {
|
||||
const w = E.v === v ? E.u : E.v
|
||||
if (!discovered.has(w)) {
|
||||
discovered.add(w)
|
||||
stack.push(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a breadth-first search on a graph. Returns the path from root to the found node.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-31
|
||||
*/
|
||||
BFS(root: VertexType<typeof this.G>, search: SearchFn<typeof this.G>) {
|
||||
const queue = [root]
|
||||
const backEdges = new Map<VertexType<typeof this.G>, EdgeType<typeof this.G>>()
|
||||
const discovered = new WeakSet<typeof root>()
|
||||
discovered.add(root)
|
||||
|
||||
let v: VertexType<typeof this.G> | null = root
|
||||
|
||||
while (queue.length !== 0) {
|
||||
v = queue.shift()!
|
||||
const value = search(v, backEdges.get(v) ?? null, backEdges)
|
||||
|
||||
if (value) break
|
||||
for (const E of v.adjacentEdges) {
|
||||
if (this.G.directed && v !== E.u) continue
|
||||
const w = E.not(v)
|
||||
if (!discovered.has(w)) {
|
||||
discovered.add(w)
|
||||
backEdges.set(w, E)
|
||||
queue.push(w)
|
||||
}
|
||||
}
|
||||
v = null
|
||||
}
|
||||
|
||||
if (v === null) return null
|
||||
|
||||
const reversed: EdgeType<typeof this.G>[] = []
|
||||
while (v != root) {
|
||||
const back = backEdges.get(v)
|
||||
if (!back) break
|
||||
v = back.not(v)
|
||||
reversed.push(back)
|
||||
}
|
||||
const path = this.G.createPath(root)
|
||||
reversed.reverse().forEach(e => path.addEdge(e))
|
||||
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 0
|
||||
// / \
|
||||
// 1 2
|
||||
// / \ / \
|
||||
// 3 4 5 6
|
||||
|
||||
if (import.meta.main) {
|
||||
const G = new Graph<number, null>({ directed: true })
|
||||
//#region Graph definition
|
||||
const vertices = [
|
||||
G.createVertex(0),
|
||||
G.createVertex(1),
|
||||
G.createVertex(2),
|
||||
G.createVertex(3),
|
||||
G.createVertex(4),
|
||||
G.createVertex(5),
|
||||
G.createVertex(6)
|
||||
]
|
||||
|
||||
G.createEdge(vertices[0], vertices[1], null)
|
||||
G.createEdge(vertices[0], vertices[2], null)
|
||||
G.createEdge(vertices[1], vertices[3], null)
|
||||
G.createEdge(vertices[1], vertices[4], null)
|
||||
G.createEdge(vertices[2], vertices[5], null)
|
||||
G.createEdge(vertices[2], vertices[6], null)
|
||||
//#endregion
|
||||
|
||||
const solver = new GraphSolver(G)
|
||||
const solution = solver.BFS(vertices[1], v => v === vertices[6])
|
||||
|
||||
console.log(
|
||||
G.directed ? 'directed' : 'undirected',
|
||||
solution?.edges.map(e => e),
|
||||
solution?.vertices.map(v => v.data)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Graph } from './graph.ts'
|
||||
|
||||
/**
|
||||
* Creates a undirected cyclic graph.
|
||||
*
|
||||
* ```plaintext
|
||||
* A
|
||||
* / \
|
||||
* C B
|
||||
* \ / \
|
||||
* \ E D
|
||||
* \|
|
||||
* F
|
||||
* ```
|
||||
*/
|
||||
export function g1() {
|
||||
const G = new Graph<null, null>({ directed: false })
|
||||
|
||||
const vertices = [
|
||||
G.createVertex(null, 'A'),
|
||||
G.createVertex(null, 'B'),
|
||||
G.createVertex(null, 'C'),
|
||||
G.createVertex(null, 'D'),
|
||||
G.createVertex(null, 'E'),
|
||||
G.createVertex(null, 'F')
|
||||
]
|
||||
|
||||
G.createEdge(vertices[0], vertices[1], null, { id: 'AB' })
|
||||
G.createEdge(vertices[0], vertices[2], null, { id: 'AC' })
|
||||
G.createEdge(vertices[1], vertices[3], null, { id: 'BD' })
|
||||
G.createEdge(vertices[1], vertices[4], null, { id: 'BE' })
|
||||
G.createEdge(vertices[2], vertices[5], null, { id: 'CF' })
|
||||
G.createEdge(vertices[3], vertices[5], null, { id: 'DF' })
|
||||
G.createEdge(vertices[4], vertices[5], null, { id: 'EF' })
|
||||
|
||||
return G
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an *undirected acyclic graph* for testing. Resembles a tree.
|
||||
*
|
||||
*
|
||||
* ```plaintext
|
||||
* 0
|
||||
* / \
|
||||
* 1 2
|
||||
* / \ / \
|
||||
* 3 4 5 6
|
||||
* ```
|
||||
*/
|
||||
export function g2() {
|
||||
const G = new Graph<null, null>({ directed: false })
|
||||
|
||||
const vertices = [
|
||||
G.createVertex(null, 'A'),
|
||||
G.createVertex(null, 'B'),
|
||||
G.createVertex(null, 'C'),
|
||||
G.createVertex(null, 'D'),
|
||||
G.createVertex(null, 'E'),
|
||||
G.createVertex(null, 'F'),
|
||||
G.createVertex(null, 'G')
|
||||
]
|
||||
|
||||
G.createEdge(vertices[0], vertices[1], null)
|
||||
G.createEdge(vertices[0], vertices[2], null)
|
||||
G.createEdge(vertices[1], vertices[3], null)
|
||||
G.createEdge(vertices[1], vertices[4], null)
|
||||
G.createEdge(vertices[2], vertices[5], null)
|
||||
G.createEdge(vertices[2], vertices[6], null)
|
||||
|
||||
return G
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Graph, VertexType, EdgeType, Path, Vertex, PathType } from "./graph.ts";
|
||||
|
||||
/**
|
||||
* Describes a search function for DFS and BFS.
|
||||
*/
|
||||
export type SearchFn<Graph> = (v: VertexType<Graph>, e: EdgeType<Graph> | null, pathmap: Map<typeof v, PathType<Graph>>) => boolean | void
|
||||
|
||||
/**
|
||||
* A graph solver, which can perform various algorithms on a graph.
|
||||
*/
|
||||
export class GraphSolver<vData, eData> {
|
||||
G: Graph<vData, eData>
|
||||
|
||||
constructor(G: Graph<vData, eData>) {
|
||||
this.G = G
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a breadth-first search on a graph. Returns the path from the root to the found node.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-09-28
|
||||
*/
|
||||
BFS(root: Vertex<vData, eData>, search: SearchFn<typeof this.G>): Path<vData, eData> | null {
|
||||
const queue = [root]
|
||||
const backPaths = new Map<Vertex<vData, eData>, Path<vData, eData>>()
|
||||
const discovered = new WeakSet<typeof root>()
|
||||
discovered.add(root)
|
||||
|
||||
let v: VertexType<typeof this.G> | null = root
|
||||
backPaths.set(root, this.G.createPath(root))
|
||||
|
||||
while (queue.length !== 0) {
|
||||
v = queue.shift()!
|
||||
const found = search(v, backPaths.get(v)?.edges.at(-1) ?? null, backPaths)
|
||||
|
||||
if (found) break
|
||||
for (const E of v.adjacentEdges) {
|
||||
if (E.directed && v !== E.u) continue
|
||||
const w = E.not(v)
|
||||
|
||||
if (!discovered.has(w)) {
|
||||
discovered.add(w)
|
||||
|
||||
const path = backPaths.get(v)!.copy()
|
||||
path.addEdge(E)
|
||||
backPaths.set(w, path)
|
||||
|
||||
queue.push(w)
|
||||
}
|
||||
}
|
||||
// If we iterate through all vertices without finding one that fulfills `search`,
|
||||
// then the found vertex is null and we should return null
|
||||
v = null
|
||||
}
|
||||
|
||||
if (v === null) return null
|
||||
|
||||
return backPaths.get(v) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a depth-first search on a graph. Returns the path from the root to the found node.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-09-28
|
||||
*/
|
||||
DFS(root: Vertex<vData, eData>, search: SearchFn<typeof this.G>): Path<vData, eData> | null {
|
||||
const stack = [root]
|
||||
const backPaths = new Map<VertexType<typeof this.G>, Path<vData, eData>>()
|
||||
|
||||
// const discovered = new WeakSet<typeof root>()
|
||||
// discovered.add(root)
|
||||
|
||||
let v: VertexType<typeof this.G> | null = root
|
||||
backPaths.set(root, this.G.createPath(root))
|
||||
|
||||
while (stack.length !== 0) {
|
||||
v = stack.pop()!
|
||||
const found = search(v, backPaths.get(v)?.edges.at(-1) ?? null, backPaths)
|
||||
|
||||
if (found) break
|
||||
for (const E of v.adjacentEdges) {
|
||||
if (E.directed && v !== E.u) continue
|
||||
const w = E.not(v)
|
||||
// discovered.add(w)
|
||||
stack.unshift(w)
|
||||
|
||||
// Update the path!
|
||||
const path = backPaths.get(v)!.copy()
|
||||
path.addEdge(E)
|
||||
backPaths.set(w, path)
|
||||
|
||||
}
|
||||
v = null
|
||||
}
|
||||
|
||||
if (v === null) return null
|
||||
|
||||
return backPaths.get(v) ?? null
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,22 @@
|
||||
import * as Colors from 'https://deno.land/std/fmt/colors.ts'
|
||||
import * as Colors from "https://deno.land/std@0.157.0/fmt/colors.ts"
|
||||
|
||||
type UUID = ReturnType<typeof crypto.randomUUID>
|
||||
|
||||
export type VertexType<G> = G extends Graph<infer T, infer V> ? Vertex<T, V> : never
|
||||
export type EdgeType<G> = G extends Graph<infer T, infer V> ? Edge<T, V> : never
|
||||
export type WeightFn<Graph> = (v: VertexType<Graph>, e: EdgeType<Graph>) => number
|
||||
export type PathType<G> = G extends Graph<infer T, infer V> ? Path<T, V> : never
|
||||
export type WeightFn<Graph> = (this: Graph, e: EdgeType<Graph>) => number
|
||||
|
||||
interface GraphOpts<G> {
|
||||
directed: boolean
|
||||
weights: WeightFn<G>
|
||||
}
|
||||
|
||||
interface EdgeOpts {
|
||||
directed: boolean
|
||||
id: UUID
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a graph, consisting of vertices and edges.
|
||||
* @author MindfulMinun
|
||||
@@ -46,6 +52,19 @@ export class Graph<vData, eData> {
|
||||
this.vertices.delete(V.id)
|
||||
}
|
||||
|
||||
createEdge(u: Vertex<vData, eData>, v: Vertex<vData, eData>, data: eData, opts: Partial<EdgeOpts> = {}) {
|
||||
const options: EdgeOpts = {
|
||||
directed: this.directed,
|
||||
id: crypto.randomUUID(),
|
||||
...opts
|
||||
}
|
||||
const E = new Edge(this, options.id, u, v, options.directed, data)
|
||||
this.edges.set(options.id, E)
|
||||
v.adjacentEdges.add(E)
|
||||
u.adjacentEdges.add(E)
|
||||
return E
|
||||
}
|
||||
|
||||
deleteEdge(E: Edge<vData, eData>) {
|
||||
// Remove me from the caches of my connecting nodes, then delete me
|
||||
E.u.adjacentEdges.delete(E)
|
||||
@@ -53,18 +72,6 @@ export class Graph<vData, eData> {
|
||||
this.edges.delete(E.id)
|
||||
}
|
||||
|
||||
createEdge(
|
||||
u: Vertex<vData, eData>,
|
||||
v: Vertex<vData, eData>,
|
||||
data: eData,
|
||||
id: UUID = crypto.randomUUID()
|
||||
): Edge<vData, eData> {
|
||||
const E = new Edge(this, id, u, v, data)
|
||||
this.edges.set(id, E)
|
||||
v.adjacentEdges.add(E)
|
||||
u.adjacentEdges.add(E)
|
||||
return E
|
||||
}
|
||||
|
||||
createPath(start: Vertex<vData, eData>) {
|
||||
return new Path<vData, eData>(this, start)
|
||||
@@ -98,7 +105,7 @@ export class Graph<vData, eData> {
|
||||
|
||||
for (const E of obj.e) {
|
||||
if (!Array.isArray(E)) throw Error("Failed to parse: One of the edges isn't a tuple!")
|
||||
const [uuid, u, v, data] = E as [UUID, UUID, UUID, eData]
|
||||
const [uuid, u, v, isDirected, data] = E as [UUID, UUID, UUID, boolean, eData]
|
||||
if (typeof uuid != 'string') throw Error("Failed to parse: UUID wasn't a string!")
|
||||
if (typeof u != 'string') throw Error("Failed to parse: U wasn't a string!")
|
||||
if (typeof v != 'string') throw Error("Failed to parse: V wasn't a string!")
|
||||
@@ -106,9 +113,13 @@ export class Graph<vData, eData> {
|
||||
const U = G.vertices.get(u)
|
||||
const V = G.vertices.get(v)
|
||||
|
||||
if (!U || !V) throw Error("Failed to parse: A vertex is never defined!")
|
||||
if (!U) throw Error(`Vertex ${U} doesn't exist!`)
|
||||
if (!V) throw Error(`Vertex ${V} doesn't exist!`)
|
||||
|
||||
G.createEdge(U, V, data, uuid)
|
||||
G.createEdge(U, V, data, {
|
||||
id: uuid,
|
||||
directed: isDirected
|
||||
})
|
||||
}
|
||||
|
||||
return G
|
||||
@@ -162,11 +173,20 @@ export class Edge<vData, eData> {
|
||||
id: UUID
|
||||
u: Vertex<vData, eData>
|
||||
v: Vertex<vData, eData>
|
||||
directed: boolean
|
||||
|
||||
constructor(parentGraph: Graph<vData, eData>, id: UUID, u: Vertex<vData, eData>, v: Vertex<vData, eData>, data: eData) {
|
||||
constructor(
|
||||
parentGraph: Graph<vData, eData>,
|
||||
id: UUID,
|
||||
u: Vertex<vData, eData>,
|
||||
v: Vertex<vData, eData>,
|
||||
directed: boolean,
|
||||
data: eData
|
||||
) {
|
||||
this.graph = parentGraph
|
||||
this.data = data
|
||||
this.id = id
|
||||
this.directed = directed
|
||||
|
||||
if (u.graph !== v.graph || u.graph !== parentGraph) throw Error("Both vertices and the new edge must all belong to the same graph.")
|
||||
|
||||
@@ -182,16 +202,18 @@ export class Edge<vData, eData> {
|
||||
delete() { this.graph.deleteEdge(this) }
|
||||
|
||||
representAsJSON() {
|
||||
// tuple!
|
||||
return [
|
||||
this.id,
|
||||
this.u.id,
|
||||
this.v.id,
|
||||
this.directed,
|
||||
this.data
|
||||
]
|
||||
}
|
||||
|
||||
toString() {
|
||||
return Colors.magenta(`<${this.id.slice(0, 8)}>: ${this.u} ${!this.graph.directed ? '<' : ''}--> ${this.v}`)
|
||||
return Colors.magenta(`<${this.id.slice(0, 8)}>: ${this.u} ${!this.directed ? '<' : ''}--> ${this.v}`)
|
||||
}
|
||||
|
||||
[Symbol.for("Deno.customInspect")]() {
|
||||
@@ -199,7 +221,6 @@ export class Edge<vData, eData> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class Path<vData, eData> {
|
||||
graph: Graph<vData, eData>
|
||||
start: Vertex<vData, eData> | null
|
||||
@@ -231,4 +252,10 @@ export class Path<vData, eData> {
|
||||
|
||||
return vertices
|
||||
}
|
||||
|
||||
copy() {
|
||||
const path = new Path<vData, eData>(this.graph, this.start ?? undefined)
|
||||
path.edges = this.edges.slice()
|
||||
return path
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* A cache of prime numbers.
|
||||
*
|
||||
* This cache is unfortunately bounded by the memory constraints JavaScript
|
||||
* imposes on arrays. This means the highest prime number that can be generated
|
||||
* is NOT anywhere close to `Number.MAX_SAFE_INTEGER` (2^53 - 1), but instead
|
||||
* much lower, at around the (2^32 - 2)th prime.
|
||||
* @private
|
||||
*/
|
||||
const knownPrimesArr: number[] = [2]
|
||||
|
||||
/**
|
||||
* Yields prime numbers in ascending order
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This function has a heavy memory footprint, is unfortunately bounded
|
||||
* by the memory constraints JavaScript imposes on arrays. This means
|
||||
* the highest prime number that can be generated is NOT `Number.MAX_SAFE_INTEGER`, but
|
||||
* instead the (2^32 - 2)th prime.
|
||||
*
|
||||
* However, since primality only needs to be verified for numbers up to sqrt(n), this
|
||||
* function is sufficient for telling if any number between 2 and `Number.MAX_SAFE_INTEGER` is prime
|
||||
*
|
||||
* @example
|
||||
* for (const prime of primes()) {
|
||||
* if (prime > 100) break
|
||||
* console.log(prime)
|
||||
* }
|
||||
*
|
||||
* @author MindfulMinun
|
||||
* @since 2022-10-13
|
||||
*/
|
||||
export function* primes(): Generator<number, never, undefined> {
|
||||
// First we yield all the known primes
|
||||
// yield* knownPrimes.values()
|
||||
yield* knownPrimesArr
|
||||
|
||||
// Then we generate new primes using the sieve of Eratosthenes
|
||||
while (true) {
|
||||
const length = knownPrimesArr.length
|
||||
let n = knownPrimesArr[length - 1] + 1
|
||||
|
||||
while (!_isPrime(n)) n++
|
||||
|
||||
if (2 ** 32 - 3 < length) throw Error("Too many primes!")
|
||||
|
||||
knownPrimesArr.push(n)
|
||||
yield n
|
||||
}
|
||||
}
|
||||
|
||||
function _isPrime(n: number) {
|
||||
if (n < 2) return false
|
||||
|
||||
if (Number.MAX_SAFE_INTEGER < n) throw Error("Number is too large to verify X(")
|
||||
|
||||
const sqrt = Math.ceil(Math.sqrt(n))
|
||||
|
||||
for (const prime of knownPrimesArr) {
|
||||
if (n % prime === 0) return false
|
||||
if (sqrt < prime) break
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if a given number is prime.
|
||||
*/
|
||||
export function isPrime(n: number): boolean {
|
||||
// To verify if a number is prime, we only need to check if it is divisible by
|
||||
// any of the primes less than or equal to its square root.
|
||||
|
||||
const sqrt = Math.ceil(Math.sqrt(n))
|
||||
|
||||
for (const prime of primes()) {
|
||||
if (n % prime === 0) return false
|
||||
if (sqrt < prime) break
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in new issue
Block a user