trips/router/internal/plan/optimize.go
Greg Pomerantz efde2cc71b maps project: design, survey, mock app, and route-aware planning backend
- DESIGN.md: full design incl. driving-trip requirements (R1-R4),
  stays model, focus mode, mobile, provenance rules
- SURVEY.md: open-source landscape
- mock/: interaction mock (Florence itinerary, focus mode, stays,
  region stops, mobile layout)
- router/: Go module (stdlib-only) with Router interface
  (Valhalla + OSRM backends), stop_cost, optimize_stops, corridor,
  routectl CLI, bench (5 real NE-corridor tasks, 26 checks passing),
  integration tests, and setup-osrm.sh for the self-hosted router
- osm/: NH+MA+CT+NY PBFs (gitignored) + setup artifacts
2026-09-06 00:05:17 -04:00

190 lines
5.1 KiB
Go

package plan
import "maps/router/internal/route"
// OptimizeResult is the best order of k stops.
type OptimizeResult struct {
Order []int // indices into the stops slice
DetourMin int
TotalMin int
Exhaustive bool // true when the optimum came from full search
Feasible bool // true when TotalMin <= budgetMin (when a budget was given)
Relaxed bool // true when we fell back to fewer than k stops
}
// OptimizeStops picks the best ordered subset of k stops minimizing
// SeqCost, subject to TotalMin <= budgetMin when budgetMin > 0.
// m must be the (n+2)-layout matrix for stops.
//
// Selection semantics (k is the user's request, "give me 2 hikes"):
// 1. best feasible exactly-k
// 2. else best feasible with fewer stops (Relaxed)
// 3. else best exactly-k ignoring the budget (Feasible=false)
//
// n <= 10: full enumeration (this is also the ground truth for evals).
// n > 10: greedy best-insertion (exact for k=1).
func OptimizeStops(m route.Matrix, stops []Stop, k, budgetMin int) OptimizeResult {
n := len(stops)
if k < 0 || k > n {
k = n
}
if k == 0 {
return OptimizeResult{Feasible: true, Exhaustive: true}
}
if n <= 10 {
return optimizeExhaustive(m, stops, k, budgetMin)
}
return optimizeGreedy(m, stops, k, budgetMin)
}
type cand struct {
order []int
total int
detour int
}
func feas(c cand, budgetMin int) bool { return budgetMin <= 0 || c.total <= budgetMin }
func lowerCost(a, b cand) bool {
if a.total != b.total {
return a.total < b.total
}
return a.detour < b.detour
}
func pickBest(cands []cand, budgetMin, wantSize int) (cand, bool) {
var best cand
found := false
for _, c := range cands {
if wantSize >= 0 && len(c.order) != wantSize {
continue
}
if !feas(c, budgetMin) {
continue
}
if !found || lowerCost(c, best) {
best, found = c, true
}
}
return best, found
}
func optimizeExhaustive(m route.Matrix, stops []Stop, k, budgetMin int) OptimizeResult {
n := len(stops)
// Enumerate ALL permutations of every subset of size k.
var all []cand
var rec func(order []int, used []bool)
rec = func(order []int, used []bool) {
if len(order) == k {
detour, total := SeqCost(m, order, stops)
if total >= 0 {
all = append(all, cand{order: append([]int(nil), order...), total: total, detour: detour})
}
return
}
for i := 0; i < n; i++ {
if !used[i] {
used[i] = true
rec(append(order, i), used)
used[i] = false
}
}
}
rec(nil, make([]bool, n))
// 1. feasible exactly-k
if c, ok := pickBest(all, budgetMin, k); ok {
return OptimizeResult{Order: c.order, DetourMin: c.detour, TotalMin: c.total, Exhaustive: true, Feasible: true}
}
// 2. feasible with fewer: re-enumerate smaller sizes
if budgetMin > 0 {
var less []cand
var rec2 func(start, size int, order []int, used []bool)
rec2 = func(start, size int, order []int, used []bool) {
if len(order) == size {
detour, total := SeqCost(m, order, stops)
if total >= 0 && feas(cand{total: total}, budgetMin) {
less = append(less, cand{order: append([]int(nil), order...), total: total, detour: detour})
}
return
}
for i := start; i < n; i++ {
if !used[i] {
used[i] = true
rec2(i+1, size, append(order, i), used)
used[i] = false
}
}
}
for size := 1; size < k; size++ {
rec2(0, size, nil, make([]bool, n))
}
// best among feasible smaller (larger size preferred on tie? no — just lowest total)
if c, ok := pickBest(less, budgetMin, -1); ok {
return OptimizeResult{Order: c.order, DetourMin: c.detour, TotalMin: c.total, Exhaustive: true, Feasible: true, Relaxed: true}
}
}
// 3. best exactly-k ignoring budget
var bestK cand
found := false
for _, c := range all {
if !found || lowerCost(c, bestK) {
bestK, found = c, true
}
}
if !found {
return OptimizeResult{DetourMin: -1, TotalMin: -1, Exhaustive: true}
}
return OptimizeResult{Order: bestK.order, DetourMin: bestK.detour, TotalMin: bestK.total, Exhaustive: true, Feasible: budgetMin <= 0}
}
func optimizeGreedy(m route.Matrix, stops []Stop, k, budgetMin int) OptimizeResult {
n := len(stops)
order := []int{}
used := make([]bool, n)
for len(order) < k {
_, curTotal := SeqCost(m, order, stops)
if curTotal < 0 {
break
}
bestTotal := int(^uint(0) >> 1)
bestStop, bestPos := -1, -1
for s := 0; s < n; s++ {
if used[s] {
continue
}
for pos := 0; pos <= len(order); pos++ {
tri := make([]int, 0, len(order)+1)
tri = append(tri, order[:pos]...)
tri = append(tri, s)
tri = append(tri, order[pos:]...)
_, total := SeqCost(m, tri, stops)
if total < 0 {
continue
}
if budgetMin > 0 && total > budgetMin {
continue
}
if total < bestTotal {
bestTotal = total
bestStop, bestPos = s, pos
}
}
}
if bestStop < 0 {
break
}
used[bestStop] = true
order = append(order[:bestPos], append([]int{bestStop}, order[bestPos:]...)...)
}
detour, total := SeqCost(m, order, stops)
res := OptimizeResult{Order: order, DetourMin: detour, TotalMin: total, Exhaustive: false}
if len(order) < k {
res.Relaxed = true
}
if total >= 0 {
res.Feasible = budgetMin <= 0 || total <= budgetMin
}
return res
}