package main
import (
"flag"
"fmt"
"math/rand"
"time"
"algo/metrics"
)
// buildWeightedTrails is lesson 14's generator with a cost on every edge: the
// number of DAYS the reader waited before the next book. Same seed, same trails,
// same shape — only the weights are new.
func buildWeightedTrails(n, trails, length int, seed int64) *WGraph {
rng := rand.New(rand.NewSource(seed))
g := NewWGraph(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, int32(1+rng.Intn(60))) // 1..60 days
v = w
}
}
return g
}
func cmdRoute(args []string) error {
fs := flag.NewFlagSet("route", 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 pair where hops and cost disagree most)")
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 := buildWeightedTrails(n, *trails, *length, *seed)
dst := int32(*to)
if *to < 0 {
dst = mostDisagreeing(g, int32(*from))
}
var bc, hc, sc metrics.Counter
bfsPath, okB := BFSW(g, int32(*from), dst, &bc)
t0 := time.Now()
heapPath, heapCost, heapSettled, okH := DijkstraHeap(g, int32(*from), dst, &hc)
heapTime := time.Since(t0)
t0 = time.Now()
_, scanCost, scanSettled, okS := DijkstraScan(g, int32(*from), dst, &sc)
scanTime := time.Since(t0)
if !okB || !okH || !okS {
fmt.Printf("no route from %d to %d\n", *from, dst)
return nil
}
fmt.Printf("nodes %d, edges %d, weights = days between borrows (1..60)\n", g.Nodes(), g.Edges())
fmt.Printf("route from %d to %d\n\n", *from, dst)
fmt.Printf(" %-26s %8s %10s\n", "", "hops", "days")
fmt.Printf(" %-26s %8d %10d\n", "BFS (fewest hops)", len(bfsPath)-1, g.PathCost(bfsPath))
fmt.Printf(" %-26s %8d %10d\n", "Dijkstra (fewest days)", len(heapPath)-1, heapCost)
fmt.Printf("\n the cheapest route takes %d more hops and saves %d days\n",
(len(heapPath)-1)-(len(bfsPath)-1), g.PathCost(bfsPath)-heapCost)
fmt.Printf("\n %-26s %14s %12s %12s\n", "same answer, two frontiers", "queue work", "settled", "time")
fmt.Printf(" %-26s %14d %12d %12s\n", "binary heap (lesson 13)", hc.N(), heapSettled, heapTime.Round(time.Microsecond))
fmt.Printf(" %-26s %14d %12d %12s\n", "linear min-scan", sc.N(), scanSettled, scanTime.Round(time.Microsecond))
fmt.Printf("\n identical cost: %v (%d vs %d)\n", heapCost == scanCost, heapCost, scanCost)
fmt.Printf(" work ratio %.1fx", float64(sc.N())/float64(hc.N()))
if heapTime > 0 {
fmt.Printf(", time ratio %.1fx", float64(scanTime)/float64(heapTime))
}
fmt.Println()
return nil
}
// mostDisagreeing finds the destination where the fewest-hops route and the
// cheapest route differ most in days — the clearest illustration that a hop
// count is not a cost.
func mostDisagreeing(g *WGraph, from int32) int32 {
best, bestGap := from, int32(-1)
for t := 0; t < g.Nodes(); t++ {
if int32(t) == from {
continue
}
var a, b metrics.Counter
bp, ok1 := BFSW(g, from, int32(t), &a)
_, cost, _, ok2 := DijkstraHeap(g, from, int32(t), &b)
if !ok1 || !ok2 {
continue
}
if gap := g.PathCost(bp) - cost; gap > bestGap {
best, bestGap = int32(t), gap
}
}
return best
}
// cmdFrontier is the two-frontier comparison across every reachable
// destination, so the ratio is a claim rather than an anecdote.
func cmdFrontier(args []string) error {
fs := flag.NewFlagSet("frontier", 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")
from := fs.Int("from", 0, "start node")
rounds := fs.Int("rounds", 5, "repeat the whole sweep this many times, for the timings")
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 := buildWeightedTrails(n, *trails, *length, 1)
var hc, sc metrics.Counter
var heapTime, scanTime time.Duration
pairs, agree := 0, 0
var hopsDiffer int
for r := 0; r < *rounds; r++ {
var rhc, rsc metrics.Counter
pairs, agree, hopsDiffer = 0, 0, 0
for t := 0; t < n; t++ {
if int32(t) == int32(*from) {
continue
}
t0 := time.Now()
hp, hcost, _, ok1 := DijkstraHeap(g, int32(*from), int32(t), &rhc)
heapTime += time.Since(t0)
t0 = time.Now()
_, scost, _, ok2 := DijkstraScan(g, int32(*from), int32(t), &rsc)
scanTime += time.Since(t0)
if !ok1 || !ok2 {
continue
}
pairs++
if hcost == scost {
agree++
}
var bcc metrics.Counter
if bp, ok := BFSW(g, int32(*from), int32(t), &bcc); ok && len(bp) != len(hp) {
hopsDiffer++
}
}
hc, sc = rhc, rsc // one round's counts; they are identical every round
}
heapTime /= time.Duration(*rounds)
scanTime /= time.Duration(*rounds)
fmt.Printf("nodes %d, edges %d, %d reachable destinations from %d\n\n", g.Nodes(), g.Edges(), pairs, *from)
fmt.Printf(" %-26s %16s %12s\n", "", "queue work", "time")
fmt.Printf(" %-26s %16d %12s\n", "binary heap (lesson 13)", hc.N(), heapTime.Round(time.Millisecond))
fmt.Printf(" %-26s %16d %12s\n", "linear min-scan", sc.N(), scanTime.Round(time.Millisecond))
fmt.Printf("\n identical cost on %d of %d destinations\n", agree, pairs)
fmt.Printf(" work ratio %.1fx\n", float64(sc.N())/float64(hc.N()))
fmt.Printf(" time ratio %.1fx\n", float64(scanTime)/float64(heapTime))
fmt.Printf("\n BFS's route differed from Dijkstra's on %d of %d (%.1f%%)\n",
hopsDiffer, pairs, 100*float64(hopsDiffer)/float64(pairs))
return nil
}