- 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
190 lines
5.1 KiB
Go
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
|
|
}
|