Skip to content

Commit f6a357e

Browse files
committed
perf(dfa): faster predecessor state lookup
Store a reversed copy of DFA state transitions for faster lookup of predecessor states. That halves the time for part 2 in the AOC benchmark: 40 seconds to 20 seconds.
1 parent 9b1f56e commit f6a357e

File tree

4 files changed

+146
-59
lines changed

4 files changed

+146
-59
lines changed

benchmark/aoc2023-day12.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ function part2() {
9191
}
9292

9393
const sol1 = part1() // best time: 992ms
94-
const sol2 = part2() // best time: 28258ms
94+
const sol2 = part2() // best time: 20095ms
9595

9696
console.log('Part 1:', sol1.totalCount, `(time: ${Math.ceil(sol1.time)}ms)`)
9797
console.log('Part 2:', sol2.totalCount, `(time: ${Math.ceil(sol2.time)}ms)`)

src/dfa.ts

Lines changed: 17 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as CharSet from "./char-set"
22
import * as RE from "./regex"
33
import { assert } from "./utils"
44
import * as Table from './table'
5+
import * as Graph from './graph'
56

67
export type DFA = Readonly<{
78
allStates: Map<number, RE.ExtRegex>
@@ -44,7 +45,7 @@ function regexToDFA(regex: RE.ExtRegex): DFA {
4445
worklist.push(targetState)
4546
// console.debug('state count: ', allStates.size)
4647
} else {
47-
Table.setWith(
48+
Table.set(
4849
sourceState.hash,
4950
knownState.hash,
5051
charSet,
@@ -70,81 +71,54 @@ function regexToDFA(regex: RE.ExtRegex): DFA {
7071
}
7172
}
7273

73-
type RipStateResult = {
74-
predecessors: [number, RE.StdRegex][]
75-
selfLoop: RE.StdRegex
76-
successors: [number, RE.StdRegex][]
77-
}
78-
79-
function ripState(state: number, transitions: Table.Table<RE.StdRegex>): RipStateResult {
80-
const selfLoop = Table.get(state, state, transitions) ?? RE.epsilon
81-
82-
const successorsMap = transitions.get(state) ?? new Map<number, RE.StdRegex>()
83-
// handle self loops separately:
84-
successorsMap.delete(state)
85-
const successors = [...successorsMap.entries()]
86-
transitions.delete(state)
87-
88-
const predecessors: [number, RE.StdRegex][] = []
89-
for (const [source, transitionsFromSource] of transitions) {
90-
// handle self loops separately:
91-
if (source !== state) {
92-
const label = transitionsFromSource.get(state)
93-
if (label !== undefined) {
94-
predecessors.push([source, label])
95-
transitionsFromSource.delete(state)
96-
}
97-
}
98-
}
99-
100-
return { selfLoop, successors, predecessors }
101-
}
102-
10374
export function dfaToRegex(dfa: DFA): RE.StdRegex {
104-
const transitionsWithRegexLabels = Table.map(dfa.transitions, RE.literal)
75+
const graph = Graph.create<RE.StdRegex>()
76+
for (const [source, target, charSet] of Table.entries(dfa.transitions)) {
77+
Graph.setEdge(source, target, RE.literal(charSet), graph)
78+
}
10579

10680
const newStartState = -1
107-
Table.set(
81+
Graph.setEdge(
10882
newStartState,
10983
dfa.startState,
11084
RE.epsilon,
111-
transitionsWithRegexLabels,
85+
graph
11286
)
11387

11488
const newFinalState = -2
11589
for (const oldFinalState of dfa.finalStates) {
116-
Table.set(
90+
Graph.setEdge(
11791
oldFinalState,
11892
newFinalState,
11993
RE.epsilon,
120-
transitionsWithRegexLabels,
94+
graph
12195
)
12296
}
12397

12498
for (const state of dfa.allStates.keys()) {
125-
const result = ripState(state, transitionsWithRegexLabels)
99+
const result = Graph.ripNode(state, graph)
126100

127101
for (const [pred, predLabel] of result.predecessors) {
128102
for (const [succ, succLabel] of result.successors) {
129103
const transitiveLabel = RE.seq([
130104
predLabel,
131-
RE.star(result.selfLoop),
105+
RE.star(result.selfLoop ?? RE.epsilon),
132106
succLabel,
133107
])
134108

135-
Table.setWith(
136-
pred,
109+
Graph.setEdge(
110+
pred,
137111
succ,
138112
transitiveLabel,
139-
transitionsWithRegexLabels,
113+
graph,
140114
RE.union,
141115
)
142116
}
143117
}
144118
}
145119

146-
assert(transitionsWithRegexLabels.size === 1)
147-
const transitionsFromNewStart = transitionsWithRegexLabels.get(newStartState)
120+
assert(graph.successors.size === 1)
121+
const transitionsFromNewStart = graph.successors.get(newStartState)
148122
assert(transitionsFromNewStart !== undefined)
149123

150124
if (transitionsFromNewStart.size === 0) {

src/graph.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import * as Table from "./table"
2+
3+
/**
4+
* Directed graph where nodes are identified by numbers
5+
* and edges labels have generic type `A`.
6+
* For efficient access to both successors and predecessors of nodes,
7+
* two copies of the edges are stored. `predecessors` should always
8+
* be the same as `successors`, only that edges are reversed.
9+
*/
10+
export type Graph<A> = {
11+
successors: Table.Table<A>
12+
predecessors: Table.Table<A>
13+
}
14+
15+
export function create<A>(): Graph<A> {
16+
return {
17+
predecessors: new Map(),
18+
successors: new Map()
19+
}
20+
}
21+
22+
/**
23+
* Number of in-going edges at `node` (not counting self-loop).
24+
*/
25+
export function inDegree<A>(node: number, graph: Graph<A>): number {
26+
const preds = graph.predecessors.get(node) ?? new Map()
27+
if (preds.has(node))
28+
return preds.size - 1
29+
else
30+
return preds.size
31+
}
32+
33+
/**
34+
* Number of out-going edges at `node` (not counting self-loop).
35+
*/
36+
export function outDegree<A>(node: number, graph: Graph<A>): number {
37+
const succs = graph.successors.get(node) ?? new Map()
38+
if (succs.has(node))
39+
return succs.size - 1
40+
else
41+
return succs.size
42+
}
43+
44+
export type RipNodeResult<A> = {
45+
predecessors: [number, A][]
46+
selfLoop: A | undefined
47+
successors: [number, A][]
48+
}
49+
50+
/**
51+
* Removes `node` from `graph` including all in-going and out-going
52+
* edges of `node`. Returns the removed edges.
53+
*/
54+
export function ripNode<A>(node: number, graph: Graph<A>): RipNodeResult<A> {
55+
const selfLoop = Table.get(node, node, graph.successors)
56+
57+
const successorsMap = graph.successors.get(node) ?? new Map<number, A>()
58+
successorsMap.delete(node) // remove self-loop
59+
const successors = [...successorsMap.entries()]
60+
61+
// remove successors from successors Map:
62+
graph.successors.delete(node)
63+
// remove successors from predecessor Map:
64+
for (const [succ,_] of successors) {
65+
Table.remove(succ, node, graph.predecessors)
66+
}
67+
68+
const predecessorMap = graph.predecessors.get(node) ?? new Map<number, A>()
69+
predecessorMap.delete(node) // remove self-loop
70+
const predecessors = [...predecessorMap.entries()]
71+
72+
// remove predecessors from predecessor Map:
73+
graph.predecessors.delete(node)
74+
// remove predecessors from successor Map:
75+
for (const [pred,_] of predecessors) {
76+
Table.remove(pred, node, graph.successors)
77+
}
78+
79+
return { selfLoop, successors, predecessors }
80+
}
81+
82+
/**
83+
* Adds an edge to `graph` from `sourceNode` to `targetNode` with `edgeLabel`.
84+
* If there is already an edge between these nodes, `combine` is called to
85+
* to combine the two edge labels.
86+
*/
87+
export function setEdge<A>(
88+
sourceNode: number,
89+
targetNode: number,
90+
edgeLabel: A,
91+
graph: Graph<A>,
92+
combine?: (oldLabel: A, newLabel: A) => A,
93+
): void {
94+
Table.set(
95+
sourceNode,
96+
targetNode,
97+
edgeLabel,
98+
graph.successors,
99+
combine,
100+
)
101+
Table.set(
102+
targetNode,
103+
sourceNode,
104+
edgeLabel,
105+
graph.predecessors,
106+
combine,
107+
)
108+
}

src/table.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,16 @@ export function remove<T>(
1414
return table.get(rowIndex)?.delete(colIndex)
1515
}
1616

17-
export function set<T>(
18-
rowIndex: number,
19-
colIndex: number,
20-
newValue: T,
21-
table: Table<T>,
22-
): void {
23-
return setWith(
24-
rowIndex,
25-
colIndex,
26-
newValue,
27-
table,
28-
() => { throw new Error('Table.set cell non-empty') }
29-
)
17+
function crashOnConflict<T>(): T {
18+
throw new Error('Table.set cell non-empty')
3019
}
3120

32-
export function setWith<T>(
21+
export function set<T>(
3322
rowIndex: number,
3423
colIndex: number,
3524
newValue: T,
3625
table: Table<T>,
37-
combine: (oldValue: T, newValue: T) => T
26+
combine: (oldValue: T, newValue: T) => T = crashOnConflict
3827
): void {
3928
let row = table.get(rowIndex)
4029
if (row === undefined) {
@@ -60,3 +49,19 @@ export function map<A,B>(table: Table<A>, fn: (_: A) => B): Table<B> {
6049
)
6150
)
6251
}
52+
53+
export function* entries<A>(table: Table<A>): Generator<[number, number, A]> {
54+
for (const [rowIndex, row] of table.entries()) {
55+
for (const [colIndex, value] of row.entries()) {
56+
yield [rowIndex, colIndex, value]
57+
}
58+
}
59+
}
60+
61+
export function fromEntries<A>(items: Iterable<[number, number, A]>): Table<A> {
62+
const table: Table<A> = new Map()
63+
for (const [rowIndex, colIndex, value] of items) {
64+
set(rowIndex, colIndex, value, table)
65+
}
66+
return table
67+
}

0 commit comments

Comments
 (0)