package main
import (
"flag"
"fmt"
"math/rand"
"time"
"algo/metrics"
)
// buildTrails makes the "readers also borrowed" graph, and it is shaped
// deliberately: TRAILS, not random pairs.
//
// A reader borrows a run of books one after another, so each trail is a chain.
// Trails cross where two readers happen to pick the same book, and those
// crossings are the only junctions. That is the same shape as a transit network:
// long chains, few junctions — which is why the comparison with `transit` in step
// 8 is a comparison and not an analogy.
func buildTrails(n, trails, length int, seed int64) *Graph {
rng := rand.New(rand.NewSource(seed))
g := NewGraph(n)
for t := 0; t < trails; t++ {
v := int32(rng.Intn(n))
for i := 0; i < length; i++ {
w := int32(rng.Intn(n))
g.AddEdge(v, w)
v = w
}
}
return g
}
func cmdNet(args []string) error {
fs := flag.NewFlagSet("net", flag.ContinueOnError)
in := fs.String("in", "library.jsonl", "library file")
trails := fs.Int("trails", 0, "how many borrowing trails (default n/16)")
length := fs.Int("length", 12, "books per trail")
seed := fs.Int64("seed", 1, "trail seed")
from := fs.Int("from", 0, "start node")
to := fs.Int("to", -1, "end node (default: the farthest node BFS can reach)")
if err := fs.Parse(args); err != nil {
return err
}
items, err := load(*in)
if err != nil {
return err
}
n := len(items)
if *trails == 0 {
*trails = n / 16
}
g := buildTrails(n, *trails, *length, *seed)
mean, median, max := g.Degrees()
var cc metrics.Counter
comps, largest := g.Components(&cc)
fmt.Printf("nodes %d, edges %d\n", g.Nodes(), g.Edges())
fmt.Printf(" degree: mean %.2f, median %d, max %d\n", mean, median, max)
fmt.Printf(" components %d, largest %d (%.1f%% of the library)\n\n",
comps, largest, 100*float64(largest)/float64(n))
dst := int32(*to)
if *to < 0 {
dst = worstPair(g, int32(*from))
}
var bc, dc, rc metrics.Counter
t0 := time.Now()
bfsPath, okB := BFS(g, int32(*from), dst, &bc)
bfsTime := time.Since(t0)
t0 = time.Now()
dfsPath, okD := DFS(g, int32(*from), dst, &dc)
dfsTime := time.Since(t0)
recPath, okR := DFSRecursive(g, int32(*from), dst, &rc)
if !okB || !okD || !okR {
fmt.Printf(" no path from %d to %d\n", *from, dst)
return nil
}
fmt.Printf(" path from %d to %d\n\n", *from, dst)
fmt.Printf(" %-22s %8s %14s %12s\n", "", "hops", "edges looked at", "time")
fmt.Printf(" %-22s %8d %14d %12s\n", "BFS (queue)", len(bfsPath)-1, bc.N(), bfsTime.Round(time.Microsecond))
fmt.Printf(" %-22s %8d %14d %12s\n", "DFS (stack)", len(dfsPath)-1, dc.N(), dfsTime.Round(time.Microsecond))
fmt.Printf(" %-22s %8d %14d %12s\n", "DFS (recursive)", len(recPath)-1, rc.N(), "-")
fmt.Printf("\n DFS path is %.1fx longer than the shortest\n",
float64(len(dfsPath)-1)/float64(len(bfsPath)-1))
fmt.Printf(" the two DFS versions agree: %v\n", samePath(dfsPath, recPath))
// One pair is an anecdote. Do every reachable destination.
var equal, longer, worstRatio, sumRatio, pairs = 0, 0, 0.0, 0.0, 0
worstB, worstD := 0, 0
for t := 0; t < n; t++ {
if int32(t) == int32(*from) {
continue
}
var b, d metrics.Counter
bp, ok1 := BFS(g, int32(*from), int32(t), &b)
dp, ok2 := DFS(g, int32(*from), int32(t), &d)
if !ok1 || !ok2 {
continue
}
pairs++
bh, dh := len(bp)-1, len(dp)-1
if dh == bh {
equal++
} else {
longer++
}
r := float64(dh) / float64(bh)
sumRatio += r
if r > worstRatio {
worstRatio, worstB, worstD = r, bh, dh
}
}
fmt.Printf("\n every reachable destination from %d (%d of them):\n", *from, pairs)
fmt.Printf(" DFS found the shortest path %d (%.1f%%)\n", equal, 100*float64(equal)/float64(pairs))
fmt.Printf(" DFS found a longer path %d (%.1f%%)\n", longer, 100*float64(longer)/float64(pairs))
fmt.Printf(" mean DFS/BFS hop ratio %.2fx\n", sumRatio/float64(pairs))
fmt.Printf(" worst case %d hops vs %d (%.1fx)\n", worstD, worstB, worstRatio)
return nil
}
func samePath(a, b []int32) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// worstPair returns the destination where DFS's path is longest relative to
// BFS's — the clearest single illustration, and the sweep below the table says
// how typical it is.
func worstPair(g *Graph, from int32) int32 {
best, bestRatio := from, 0.0
for t := 0; t < g.Nodes(); t++ {
if int32(t) == from {
continue
}
var b, d metrics.Counter
bp, ok1 := BFS(g, from, int32(t), &b)
dp, ok2 := DFS(g, from, int32(t), &d)
if !ok1 || !ok2 {
continue
}
if r := float64(len(dp)-1) / float64(len(bp)-1); r > bestRatio {
best, bestRatio = int32(t), r
}
}
return best
}
// cmdDense measures the two representations against each other.
func cmdDense(args []string) error {
fs := flag.NewFlagSet("dense", flag.ContinueOnError)
in := fs.String("in", "library.jsonl", "library file")
trails := fs.Int("trails", 0, "how many trails (default n/16)")
length := fs.Int("length", 12, "books per trail")
if err := fs.Parse(args); err != nil {
return err
}
items, err := load(*in)
if err != nil {
return err
}
n := len(items)
if *trails == 0 {
*trails = n / 16
}
g := buildTrails(n, *trails, *length, 1)
// adjacency list: one 24-byte slice header per node, plus 4 bytes per
// directed edge (each undirected edge is stored twice).
const header = 24
listBytes := n*header + 2*g.Edges()*4
m := NewAdjMatrix(n)
for v := 0; v < n; v++ {
for _, w := range g.adj[int32(v)] {
m.Set(int32(v), w)
}
}
fmt.Printf("nodes %d, edges %d\n\n", n, g.Edges())
fmt.Printf(" %-22s %14s %16s %14s\n", "", "bytes", "cells", "used")
fmt.Printf(" %-22s %14d %16s %14s\n", "adjacency list", listBytes, "-", "-")
fmt.Printf(" %-22s %14d %16d %13.4f%%\n", "adjacency matrix", m.Bytes(), m.Cells(),
100*float64(2*g.Edges())/float64(m.Cells()))
fmt.Printf("\n the matrix is %.1fx larger\n", float64(m.Bytes())/float64(listBytes))
// And what each costs to answer the two questions a search asks.
var lc, mc metrics.Counter
t0 := time.Now()
for v := 0; v < n; v++ {
for range g.adj[int32(v)] {
lc.Hit()
}
}
listScan := time.Since(t0)
t0 = time.Now()
for v := 0; v < n; v++ {
for w := 0; w < n; w++ {
mc.Hit()
_ = m.Has(int32(v), int32(w))
}
}
matrixScan := time.Since(t0)
fmt.Printf("\n visiting every neighbour of every node:\n")
fmt.Printf(" %-22s %14d %12s\n", "adjacency list", lc.N(), listScan.Round(time.Microsecond))
fmt.Printf(" %-22s %14d %12s\n", "adjacency matrix", mc.N(), matrixScan.Round(time.Microsecond))
return nil
}