diff --git a/lib/application/app.go b/lib/application/app.go index 67a87c9..849f469 100644 --- a/lib/application/app.go +++ b/lib/application/app.go @@ -11,7 +11,7 @@ import ( // App holds context for the application type App struct { Target uint64 - Buckets []bucket.Bucket + Buckets bucket.Buckets } // New returns an instance of the app @@ -27,7 +27,7 @@ func New(args ...string) (*App, error) { app := App{ Target: target, - Buckets: []bucket.Bucket{}, + Buckets: bucket.Buckets{}, } for _, arg := range args[1:] { @@ -44,8 +44,9 @@ func New(args ...string) (*App, error) { // Run runs the application func (a *App) Run() error { fmt.Printf("target: %d\n", a.Target) - for i, bucket := range a.Buckets { - fmt.Printf("bucket %d: %s\n", i+1, bucket.String()) + fmt.Printf("buckets: %s\n", a.Buckets.String()) + for _, solution := range a.Buckets.FindTarget((a.Target)) { + fmt.Println(solution) } return nil } diff --git a/lib/bucket/bucket.go b/lib/bucket/bucket.go index bb8c4bb..7e6f563 100644 --- a/lib/bucket/bucket.go +++ b/lib/bucket/bucket.go @@ -10,6 +10,43 @@ type Bucket struct { Volume uint64 } +// Fill returns a copy Bucket filled +func Fill(b Bucket) Bucket { + return Bucket{ + Volume: b.Capacity, + Capacity: b.Capacity, + } +} + +// Empty returns a copy Bucket set to empty +func Empty(b Bucket) Bucket { + return Bucket{ + Volume: 0, + Capacity: b.Capacity, + } +} + +// Pour fills the target bucket to the top +func Pour(src, target Bucket) (Bucket, Bucket) { + s := Bucket{ + Capacity: src.Capacity, + Volume: src.Volume, + } + t := Bucket{ + Capacity: target.Capacity, + Volume: target.Volume, + } + availableVolume := t.Capacity - t.Volume + if availableVolume > s.Volume { + t.Volume += s.Volume + s.Volume = 0 + } else { + s.Volume -= availableVolume + t.Volume += availableVolume + } + return s, t +} + // Fill sets the volume to the capacity to fill bucket func (b *Bucket) Fill() { b.Volume = b.Capacity @@ -20,6 +57,16 @@ func (b *Bucket) Empty() { b.Volume = 0 } +// IsEmpty is true no volume +func (b *Bucket) IsEmpty() bool { + return b.Volume == 0 +} + +// IsFull is true if bucket is at capacity +func (b *Bucket) IsFull() bool { + return b.Volume == b.Capacity +} + // Pour fills the target bucket to the top func (b *Bucket) Pour(target *Bucket) { availableVolume := target.Capacity - target.Volume diff --git a/lib/bucket/buckets.go b/lib/bucket/buckets.go new file mode 100644 index 0000000..b16e5c6 --- /dev/null +++ b/lib/bucket/buckets.go @@ -0,0 +1,267 @@ +package bucket + +import ( + "fmt" + "sort" + "strings" +) + +// Buckets manages bucket objects +type Buckets []Bucket + +// // action name +type actionType int + +const ( + actionTypeUnknown actionType = iota + actionTypeEmpty + actionTypeFill + actionTypePour +) + +type action struct { + kind actionType + src int + target int +} + +type actions []action + +func (a actions) String() string { + acts := []string{} + for _, act := range a { + acts = append(acts, act.String()) + } + return fmt.Sprintf("[%s]", strings.Join(acts, ",")) +} + +func (a action) String() string { + switch a.kind { + case actionTypeEmpty: + return fmt.Sprintf("empty %d ", a.target) + case actionTypeFill: + return fmt.Sprintf("fill %d ", a.target) + case actionTypePour: + return fmt.Sprintf("pour %d → %d", a.src, a.target) + case actionTypeUnknown: + } + return fmt.Sprintf("unknown (src: %d, target: %d)", a.src, a.target) +} + +type result struct { + mapping map[string][]actions + matched map[string]interface{} +} + +func (r *result) get(key string) ([]actions, bool) { + acts, ok := r.mapping[key] + return acts, ok +} + +func (r *result) keys() []string { + keys := []string{} + for key := range r.mapping { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +func (r *result) matchedKeys() []string { + keys := []string{} + for key := range r.matched { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +func (r *result) bestMatches() ([]string, bool) { + var best []string + found := false + numActions := 0 + + matched := r.matchedKeys() + for _, key := range matched { + acts, ok := r.get(key) + if found == false && ok && len(acts) > 0 { + found = true + numActions = len(acts[0]) + best = []string{key} + continue + } + + if numActions > len(acts[0]) { + numActions = len(acts[0]) + best = []string{key} + } else if numActions == len(acts[0]) { + best = append(best, key) + } + } + + if !found { + return nil, false + } + return best, true +} + +func (r *result) addMatch(key string) { + r.matched[key] = struct{}{} +} + +func (r *result) addActions(key string, acts actions) bool { + currentActions, ok := r.mapping[key] + if !ok || len(currentActions) == 0 { + currentActions = []actions{acts} + r.mapping[key] = currentActions + return true + } + + if len(acts) > len(currentActions[0]) { + return false + } + + if len(acts) == len(currentActions[0]) { + r.mapping[key] = append(currentActions, acts) + return true + } + + r.mapping[key] = []actions{acts} + return true +} + +func (b Buckets) empty(index int) { + buk := &b[index] + buk.Empty() + b[index] = *buk +} + +func (b Buckets) fill(index int) { + buk := &b[index] + buk.Fill() + b[index] = *buk +} + +func (b Buckets) pour(i, j int) { + src := &b[i] + dest := &b[j] + src.Pour(dest) + b[i] = *src + b[j] = *dest +} + +// HasTarget returns true +func (b Buckets) HasTarget(target uint64) bool { + for _, bucket := range b { + if bucket.Volume == target { + return true + } + } + return false +} + +func (b Buckets) String() string { + bucketStrs := []string{} + for _, bucket := range b { + bucketStrs = append(bucketStrs, bucket.String()) + } + return fmt.Sprintf("[%s]", strings.Join(bucketStrs, ",")) +} + +// FindTarget returns actions to find target +func (b Buckets) FindTarget(target uint64) []string { + r := &result{ + mapping: map[string][]actions{}, + matched: map[string]interface{}{}, + } + b.findTarget(target, r, actions{}) + + keys, found := r.bestMatches() + if !found { + return []string{"No solution"} + } + fmt.Printf("%d solutions\n", len(keys)) + for i, key := range keys { + allacts, ok := r.get(key) + if !ok { + continue + } + newState := make(Buckets, len(b)) + copy(newState, b) + fmt.Printf("solution %d\n", i+1) + for _, acts := range allacts { + if len(acts) == 0 { + fmt.Println("no actions required") + continue + } + for _, act := range acts { + newState = newState.performAction(act) + fmt.Printf("%s\t%s\n", act.String(), newState.String()) + } + } + } + ret := []string{} + return ret +} + +func (b Buckets) performAction(act action) Buckets { + newState := make(Buckets, len(b)) + copy(newState, b) + + switch act.kind { + case actionTypeEmpty: + newState.empty(act.target) + case actionTypeFill: + newState.fill(act.target) + case actionTypePour: + newState.pour(act.src, act.target) + } + + return newState +} + +func (b Buckets) findTarget( + target uint64, + states *result, + prevActions actions, +) { + key := b.String() + + added := states.addActions(key, prevActions) + + if b.HasTarget(target) { + states.addMatch(key) + return + } + + if !added { + return + } + + possibleActions := b.possibleActions() + for _, act := range possibleActions { + newActions := append(prevActions, act) + newState := b.performAction(act) + newState.findTarget(target, states, newActions) + } +} + +func (b Buckets) possibleActions() actions { + ret := actions{} + for i, bucket := range b { + if !bucket.IsFull() { + ret = append(ret, action{kind: actionTypeFill, target: i}) + } + if !bucket.IsEmpty() { + // ret = append(ret, action{kind: actionTypeEmpty, fn: b.empty(i), target: i}) + ret = append(ret, action{kind: actionTypeEmpty, target: i}) + for j, target := range b { + if !target.IsFull() && i != j { + // ret = append(ret, action{kind: actionTypePour, fn: b.pour(i, j), src: i, target: j}) + ret = append(ret, action{kind: actionTypePour, src: i, target: j}) + } + } + } + } + return ret +}