joshkaposh-graph
Version:
A collection of general purpose graph data structures and traversal algorithms
644 lines (540 loc) • 19.6 kB
text/typescript
import { is_none, type Option } from "joshkaposh-option";
import { type EdgeType, type GraphIx, type DefaultIx, Directed, Direction, Outgoing, Undirected } from "./graph";
import { IndexSet } from "joshkaposh-index-map";
import { done, item, Iterator, Range } from "joshkaposh-iterator";
import { is_some } from "joshkaposh-option";
import { assert } from "joshkaposh-iterator/src/util";
import { Default } from "./default";
import { next_power_of_2 } from "./util";
import { replace, swap } from "./array-helpers";
import { VisitMap, VisitorFbs } from "./visit";
import { FixedBitSet } from "fixed-bit-set";
export class MatrixGraph<N extends Default, E extends Default, Ty extends EdgeType = Directed, Ix extends GraphIx = DefaultIx> {
#node_adjacencies: Option<E>[];
#node_capacity: number;
#nodes: IdStorage<N>;
#nb_edges: number;
#ty: Ty;
#ix: Ix;
constructor(node_adjacencies: Option<E>[] = [], node_capacity = 0, nodes: IdStorage<N> = IdStorage.with_capacity(0), nb_edges = 0, ty: Ty = Directed as Ty, ix: Ix = 16 as Ix) {
this.#node_adjacencies = node_adjacencies;
this.#node_capacity = node_capacity
this.#nodes = nodes;
this.#nb_edges = nb_edges;
this.#ty = ty;
this.#ix = ix;
}
static with_capacity<N extends Default, E extends Default, Ty extends EdgeType = Directed, Ix extends 8 | 16 = 16>(node_capacity: number, ty: Ty = Directed as Ty, ix: Ix = 16 as Ix): MatrixGraph<N, E, Ty, Ix> {
const g = new MatrixGraph<N, E, Ty, Ix>(
[],
0,
IdStorage.with_capacity(node_capacity),
0,
ty,
ix
);
if (node_capacity > 0) {
g.#extend_capacity_for_node(node_capacity - 1, true);
}
return g;
}
static directed<N extends Default, E extends Default>(): MatrixGraph<N, E, Directed> {
return new MatrixGraph();
}
static undirected<N extends Default, E extends Default>(): MatrixGraph<N, E, Undirected> {
return new MatrixGraph([], 0, new IdStorage(), 0, Undirected)
}
static from_edges<N extends Default, E extends Default, Ty extends EdgeType>(iterable: Iterable<any>, default_node_weight: () => N) {
const g = new MatrixGraph<N, E, Ty>()
g.extend_with_edges(iterable, default_node_weight);
return g;
}
clear() {
for (let i = 0; i < this.#node_adjacencies.length; i++) {
this.#node_adjacencies[i] = undefined
}
this.#nodes.clear();
this.#nb_edges = 0;
}
get node_capacity() {
return this.#node_capacity;
}
node_count(): number {
return this.#nodes.len();
}
node_bound(): number {
return this.#nodes.upper_bound();
}
edge_count(): number {
return this.#nb_edges;
}
to_node_index(ix: number) {
return ix
}
to_edge_index(ix: number) {
return ix
}
from_node_index(ix: number) {
return ix
}
from_edge_index(ix: number) {
return ix
}
is_directed(): boolean {
return this.#ty.is_directed();
}
add_node(weight: N): number {
return this.#nodes.add(weight)
}
remove_node(a: number): Option<N> {
for (const id of this.#nodes.iter_ids()) {
let position = this.#to_edge_position(a, id);
if (is_some(position)) {
this.#node_adjacencies[position] = undefined;
}
if (this.is_directed()) {
position = this.#to_edge_position(id, a)
if (is_some(position)) {
this.#node_adjacencies[position] = undefined;
}
}
}
return this.#nodes.remove(a);
}
update_edge(a: number, b: number, weight: E): Option<E> {
this.#extend_capacity_for_edge(a, b);
const p = this.#to_edge_position_unchecked(a, b);
const old_weight = replace(this.#node_adjacencies, p, weight)
if (old_weight === undefined) {
this.#nb_edges += 1;
}
return old_weight
}
add_edge(a: number, b: number, weight: E) {
const old_edge_id = this.update_edge(a, b, weight)
assert(old_edge_id === undefined)
}
remove_edge(a: number, b: number): Option<E> {
const p = this.#to_edge_position(a, b);
// TODO: check if exists
if (is_none(p)) {
return
}
const old_weight = this.#node_adjacencies[p]
this.#node_adjacencies[p] = undefined;
this.#nb_edges -= 1;
return old_weight!;
}
has_edge(a: number, b: number): boolean {
const p = this.#to_edge_position(a, b);
if (is_some(p)) {
const e = this.#node_adjacencies[p]
return is_some(e);
}
return false
}
is_adjacent(a: number, b: number): boolean {
return this.has_edge(a, b);
}
node_weight(a: number): N {
return this.#nodes.get(a)!
}
edge_weight(a: number, b: number): E {
const p = this.#to_edge_position(a, b);
if (is_none(p)) {
throw new Error('No edge found between the nodes.')
}
const w = this.#node_adjacencies[p];
if (is_none(w)) {
throw new Error('No edge found between the nodes.')
}
return w
}
visit_map(): VisitMap<N> {
return new VisitorFbs(this.node_bound()) as VisitMap<N>;
}
reset_map(map: VisitMap<N> & FixedBitSet) {
map.clear();
map.grow(this.node_bound());
}
node_identifiers(): Iterator<number> {
return new NodeIdentifiers(this.#nodes.iter_ids());
}
node_references(): Iterator<[number, N]> {
return new NodeReferences(this.#nodes, this.#ix)
}
edge_references(): Iterator<[number, number, E]> {
return new EdgeReferences(this.#node_adjacencies, this.#node_capacity, this.#ty, this.#ix)
}
neighbors(a: number): Iterator<number> {
return new Neighbors(Edges.on_columns(
a,
this.#node_adjacencies,
this.#node_capacity,
this.#ty,
this.#ix
))
}
neighbors_directed(a: number, d: Direction): Iterator<number> {
if (d.value === Outgoing.value) {
return this.neighbors(a)
} else {
return new Neighbors(Edges.on_rows(
a,
this.#node_adjacencies,
this.#node_capacity,
this.#ty,
this.#ix
))
}
}
edges(a: number): Iterator<[number, number, E]> {
return Edges.on_columns(a, this.#node_adjacencies, this.#node_capacity, this.#ty, this.#ix)
}
edges_directed(a: number, d: Direction): Iterator<[number, number, E]> {
if (d.value === Outgoing.value) {
return this.edges(a)
} else {
return Edges.on_rows(a, this.#node_adjacencies, this.#node_capacity, this.#ty, this.#ix)
}
}
extend_with_edges(iterable: Iterable<any>, default_node_weight: () => N) {
for (const [source, target, weight] of iterable) {
const nx = Math.max(source, target)
while (nx >= this.node_count()) {
this.add_node(default_node_weight());
}
this.add_edge(source, target, weight);
}
}
#extend_capacity_for_node(min_node: number, exact: boolean) {
this.#node_capacity = extend_linearized_matrix(
this.is_directed(),
this.#node_adjacencies,
this.#node_capacity,
min_node + 1,
exact
)
}
#extend_capacity_for_edge(a: number, b: number) {
const min_node = Math.max(a, b);
if (min_node >= this.#node_capacity) {
this.#extend_capacity_for_node(min_node, false);
}
}
#to_edge_position(a: number, b: number): Option<number> {
if (Math.max(a, b) > this.#node_capacity) {
return
}
return this.#to_edge_position_unchecked(a, b)
}
#to_edge_position_unchecked(a: number, b: number): number {
return to_linearized_matrix_position(this.is_directed(), a, b, this.#node_capacity);
}
}
export class IdStorage<T> {
#elements: Option<T>[];
#upper_bound: number;
#removed_ids: IndexSet<number>;
constructor(elements: Option<T>[] = [], upper_bound = 0, removed_ids: IndexSet<number> = new IndexSet()) {
this.#elements = elements;
this.#upper_bound = upper_bound;
this.#removed_ids = removed_ids;
}
static with_capacity<T>(capacity: number): IdStorage<T> {
return new IdStorage<T>(
Array.from({ length: capacity }, () => undefined),
0,
new IndexSet()
)
}
upper_bound() {
return this.#upper_bound;
}
get(index: number): Option<T> {
return this.#elements[index]
}
add(element: T): number {
let id = this.#removed_ids.pop();
if (typeof id !== 'number') {
id = this.#upper_bound;
this.#upper_bound += 1;
ensure_len(this.#elements, id + 1, () => undefined)
}
this.#elements[id] = element
return id;
}
remove(id: number): Option<T> {
const data = this.#elements[id];
assert(is_some(data))
this.#elements[id] = undefined;
if (this.#upper_bound - id === 1) {
this.#upper_bound -= 1
} else {
this.#removed_ids.insert(id)
}
return data;
}
clear() {
this.#upper_bound = 0;
this.#elements.length = 0;
this.#removed_ids.clear();
}
len(): number {
return this.#upper_bound - this.#removed_ids.len();
}
iter_ids() {
return new IdIterator(
this.#upper_bound,
this.#removed_ids,
undefined
)
}
}
class IdIterator extends Iterator<number> {
#upper_bound: number;
#removed_ids: IndexSet<number>
#current: Option<number>;
constructor(upper_bound: number, removed_ids: IndexSet<number>, current: Option<number>) {
super();
this.#upper_bound = upper_bound;
this.#removed_ids = removed_ids;
this.#current = current;
}
into_iter(): Iterator<number> {
return this;
}
next(): IteratorResult<number> {
if (is_none(this.#current)) {
this.#current = 0;
} else {
this.#current += 1;
}
while (this.#removed_ids.contains(this.#current) && this.#current < this.#upper_bound) {
this.#current += 1
}
return this.#current < this.#upper_bound ? item(this.#current) : done();
}
}
class NodeReferences<N, Ix extends GraphIx> extends Iterator<[number, N]> {
#nodes: IdStorage<N>;
#iter: IdIterator;
#ix: Ix;
constructor(nodes: IdStorage<N>, ix: Ix) {
super();
this.#nodes = nodes;
this.#iter = nodes.iter_ids();
this.#ix = ix;
}
next(): IteratorResult<[number, N], any> {
const i = this.#iter.next();
return i.done ? done() : item<[number, N]>([i.value, this.#nodes.get(i.value)!]);
}
into_iter(): Iterator<[number, N]> {
this.#iter.into_iter();
return this;
}
size_hint(): [number, Option<number>] {
return this.#iter.size_hint();
}
}
class EdgeReferences<E, Ty extends EdgeType, Ix extends GraphIx> extends Iterator<[number, number, E]> {
#row: number;
#column: number;
#node_adjacencies: any[];
#node_capacity: number;
#ty: Ty;
#ix: Ix;
constructor(node_adjacencies: any[], node_capacity: number, ty: Ty, ix: Ix) {
super()
this.#row = 0;
this.#column = 0;
this.#node_adjacencies = node_adjacencies;
this.#node_capacity = node_capacity;
this.#ty = ty;
this.#ix = ix;
}
next(): IteratorResult<[number, number, E], any> {
while (true) {
const row = this.#row, column = this.#column;
if (row >= this.#node_capacity) {
return done()
}
this.#column += 1;
const max_column_len = !this.#ty.is_directed() ? row + 1 : this.#node_capacity
if (this.#column >= max_column_len) {
this.#column = 0;
this.#row += 1;
}
const p = to_linearized_matrix_position(this.#ty.is_directed(), row, column, this.#node_capacity)
const e = this.#node_adjacencies[p];
if (is_some(e)) {
return item<[number, number, E]>([row, column, e])
}
}
}
into_iter(): Iterator<[number, number, E]> {
return this
}
}
class NodeIdentifiers extends Iterator<number> {
#iter: IdIterator
constructor(iterable: IdIterator) {
super()
this.#iter = iterable
}
next(): IteratorResult<number, any> {
return this.#iter.next()
}
into_iter(): Iterator<number> {
this.#iter.into_iter()
return this
}
}
class Neighbors extends Iterator<number> {
#iter: Iterator<[number, number, any]>
constructor(iterable: Iterator<[number, number, any]>) {
super()
this.#iter = iterable
}
into_iter(): Iterator<number> {
this.#iter.into_iter();
return this
}
next(): IteratorResult<number, any> {
const n = this.#iter.next();
return n.done ? done() : item(n.value[1])
}
size_hint(): [number, Option<number>] {
return this.#iter.size_hint()
}
}
type NeighborIterDirection = 0 | 1;
const NeighborIterDirection = {
Rows: 0,
Columns: 1
} as const;
class Edges<E> extends Iterator<[number, number, E]> {
#iter_direction: NeighborIterDirection;
#node_adjacencies: any[];
#node_capacity: number;
#row: number;
#column: number;
#ty: EdgeType;
#ix: GraphIx;
constructor(column: number, row: number, iter_direction: NeighborIterDirection, node_adjacencies: any[], node_capacity: number, ty: EdgeType, ix: GraphIx) {
super()
this.#column = column;
this.#row = row;
this.#iter_direction = iter_direction;
this.#node_adjacencies = node_adjacencies;
this.#node_capacity = node_capacity;
this.#ty = ty;
this.#ix = ix;
}
static on_columns<E>(row: number, node_adjacencies: any[], node_capacity: number, ty: EdgeType, ix: GraphIx): Edges<E> {
return new Edges<E>(
0,
row,
NeighborIterDirection.Columns,
node_adjacencies,
node_capacity,
ty,
ix
)
}
static on_rows<E>(column: number, node_adjacencies: any[], node_capacity: number, ty: EdgeType, ix: GraphIx): Edges<E> {
return new Edges<E>(
column,
0,
NeighborIterDirection.Rows,
node_adjacencies,
node_capacity,
ty,
ix
)
}
next(): IteratorResult<[number, number, E], any> {
while (true) {
const row = this.#row, column = this.#column;
// console.log('next', column, row);
if (row >= this.#node_capacity || column >= this.#node_capacity) {
return done()
}
if (this.#iter_direction === NeighborIterDirection.Rows) {
this.#row += 1
} else {
this.#column += 1
}
const p = to_linearized_matrix_position(this.#ty.is_directed(), row, column, this.#node_capacity)
const e = this.#node_adjacencies[p]
if (e !== undefined) {
const [a, b] = this.#iter_direction === NeighborIterDirection.Rows ?
[column, row] :
[row, column];
return item<[number, number, E]>([a, b, e])
}
}
}
into_iter(): Iterator<[number, number, E]> {
return this;
}
}
function to_flat_square_matrix_position(row: number, column: number, width: number) {
return row * width + column;
}
function to_lower_triangular_matrix_position(row: number, column: number): number {
const [r, c] = row > column ? [row, column] : [column, row]
return (r * (r + 1)) / 2 + c;
}
function to_linearized_matrix_position(directed: boolean, row: number, column: number, width: number): number {
return directed ? to_flat_square_matrix_position(row, column, width) :
to_lower_triangular_matrix_position(row, column);
}
function extend_linearized_matrix<T>(directed: boolean, node_adjacencies: T[], old_node_capacity: number, new_capacity: number, exact: boolean): number {
if (old_node_capacity >= new_capacity) {
return old_node_capacity
}
if (directed) {
return extend_flat_square_matrix(node_adjacencies, old_node_capacity, new_capacity, exact)
} else {
return extend_lower_triangular_matrix(node_adjacencies, new_capacity)
}
}
function extend_flat_square_matrix<T>(node_adjacencies: T[], old_node_capacity: number, new_node_capacity: number, exact: boolean) {
if (!exact) {
const MIN_CAPACITY = 4
new_node_capacity = Math.max(next_power_of_2(new_node_capacity), MIN_CAPACITY)
}
// Optimization: when resizing the matrix this way we skip the first few grows to make
// small matrices a bit faster to work with.
ensure_len(node_adjacencies, Math.pow(new_node_capacity, 2), () => undefined)
for (const c of new Range(1, old_node_capacity).rev()) {
const pos = c * old_node_capacity;
const new_pos = c * new_node_capacity;
// move the slices directly if they do not overlap with their new position
if (pos + old_node_capacity <= new_pos) {
assert(pos + old_node_capacity < node_adjacencies.length)
assert(new_pos + old_node_capacity < node_adjacencies.length)
swap(node_adjacencies, pos, new_pos)
} else {
for (const i of new Range(0, old_node_capacity).rev()) {
swap(node_adjacencies, pos + i, new_pos + i)
}
}
}
return new_node_capacity;
}
function extend_lower_triangular_matrix<T>(node_adjacencies: T[], new_capacity: number) {
const max_node = new_capacity - 1;
const max_pos = to_lower_triangular_matrix_position(max_node, max_node);
ensure_len(node_adjacencies, max_pos + 1, () => undefined)
return new_capacity;
}
function ensure_len<T>(v: T[], size: number, d: () => T) {
if (v.length < size) {
for (let i = v.length - 1; i < size; i++) {
v.push(d())
}
}
}