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 }