engine: education strategies (random vs most-connected)
Two ways to spend the same education budget: a uniform random sample (spray and pray) vs the highest-degree hubs, ties broken stably towards lower node numbers so the pick is deterministic. The origin is never educated; they post the fake. rng.Shuffle does a seeded Fisher-Yates, so the random pick is reproducible too. min() is a builtin since Go 1.21.
This commit is contained in:
parent
736cd9070d
commit
c40e483ee6
2 changed files with 120 additions and 0 deletions
52
internal/engine/educate.go
Normal file
52
internal/engine/educate.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package engine
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// Education strategies pick which students the program educates. The origin
|
||||
// is never educated (they post the fake in the first place). Returned
|
||||
// slices are sorted by node number for readable, stable output; the order
|
||||
// carries no meaning.
|
||||
|
||||
// EducateRandom educates count students drawn uniformly from everyone but
|
||||
// the origin. This is the "spray and pray" baseline.
|
||||
func EducateRandom(graph *Graph, origin, count int, rng *rand.Rand) []int {
|
||||
if count <= 0 {
|
||||
return nil
|
||||
}
|
||||
candidates := make([]int, 0, graph.NumNodes()-1)
|
||||
for student := range graph.NumNodes() {
|
||||
if student != origin {
|
||||
candidates = append(candidates, student)
|
||||
}
|
||||
}
|
||||
rng.Shuffle(len(candidates), func(i, j int) {
|
||||
candidates[i], candidates[j] = candidates[j], candidates[i]
|
||||
})
|
||||
educated := candidates[:min(count, len(candidates))]
|
||||
slices.Sort(educated)
|
||||
return educated
|
||||
}
|
||||
|
||||
// EducateMostConnected educates the count students with the highest degree:
|
||||
// the hubs whose forwarding keeps the network connected. Ties break towards
|
||||
// the lower node number (stable sort) so the choice is deterministic.
|
||||
func EducateMostConnected(graph *Graph, origin, count int) []int {
|
||||
if count <= 0 {
|
||||
return nil
|
||||
}
|
||||
candidates := make([]int, 0, graph.NumNodes()-1)
|
||||
for student := range graph.NumNodes() {
|
||||
if student != origin {
|
||||
candidates = append(candidates, student)
|
||||
}
|
||||
}
|
||||
slices.SortStableFunc(candidates, func(first, second int) int {
|
||||
return graph.Degree(second) - graph.Degree(first) // descending by degree
|
||||
})
|
||||
educated := candidates[:min(count, len(candidates))]
|
||||
slices.Sort(educated)
|
||||
return educated
|
||||
}
|
||||
68
internal/engine/educate_test.go
Normal file
68
internal/engine/educate_test.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package engine
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// starGraph builds a small graph with a known degree ranking:
|
||||
// node 0 has degree 5, node 1 degree 3, nodes 2 and 3 degree 2,
|
||||
// nodes 4 and 5 degree 1.
|
||||
func starGraph() *Graph {
|
||||
graph := NewGraph(6)
|
||||
for leaf := 1; leaf <= 5; leaf++ {
|
||||
graph.AddEdge(0, leaf)
|
||||
}
|
||||
graph.AddEdge(1, 2)
|
||||
graph.AddEdge(1, 3)
|
||||
return graph
|
||||
}
|
||||
|
||||
func TestEducateMostConnected(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
origin int
|
||||
count int
|
||||
want []int
|
||||
}{
|
||||
{name: "picks top degrees, skips origin", origin: 0, count: 2, want: []int{1, 2}},
|
||||
{name: "origin can be the biggest hub", origin: 1, count: 2, want: []int{0, 2}},
|
||||
{name: "tie breaks to lower node number", origin: 0, count: 3, want: []int{1, 2, 3}},
|
||||
{name: "count clamps to available students", origin: 0, count: 99, want: []int{1, 2, 3, 4, 5}},
|
||||
{name: "zero count educates nobody", origin: 0, count: 0, want: nil},
|
||||
}
|
||||
for _, testCase := range tests {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
got := EducateMostConnected(starGraph(), testCase.origin, testCase.count)
|
||||
if !slices.Equal(got, testCase.want) {
|
||||
t.Errorf("EducateMostConnected(origin=%d, count=%d) = %v, want %v",
|
||||
testCase.origin, testCase.count, got, testCase.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEducateRandomProperties(t *testing.T) {
|
||||
graph := starGraph()
|
||||
const origin, count = 0, 3
|
||||
|
||||
educated := EducateRandom(graph, origin, count, newRand(1))
|
||||
|
||||
if len(educated) != count {
|
||||
t.Fatalf("len(educated) = %d, want %d", len(educated), count)
|
||||
}
|
||||
if slices.Contains(educated, origin) {
|
||||
t.Errorf("educated %v contains the origin %d", educated, origin)
|
||||
}
|
||||
for index := 1; index < len(educated); index++ {
|
||||
if educated[index] == educated[index-1] {
|
||||
t.Errorf("educated %v contains a duplicate", educated)
|
||||
}
|
||||
}
|
||||
|
||||
// Same seed, same pick; that is the determinism contract.
|
||||
repeat := EducateRandom(graph, origin, count, newRand(1))
|
||||
if !slices.Equal(educated, repeat) {
|
||||
t.Errorf("same seed picked %v then %v", educated, repeat)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue