github.com/mnlphlp/aoc22@v0.0.0-20230330151331-c1dc4bff1b9b/day19/day19.go (about)

     1  package day19
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  	"sync"
     7  
     8  	"github.com/mnlphlp/aoc22/util"
     9  )
    10  
    11  type ressources struct {
    12  	clay     int
    13  	ore      int
    14  	obsidian int
    15  }
    16  
    17  type blueprint struct {
    18  	id              int
    19  	oreRobot        ressources
    20  	clayRobot       ressources
    21  	obsidianRobot   ressources
    22  	geodeRobot      ressources
    23  	maxOreCost      int
    24  	maxClayCost     int
    25  	maxObsidianCost int
    26  }
    27  
    28  type state struct {
    29  	clay, ore, obsidian                        int
    30  	clayBots, oreBots, obsidianBots, geodeBots int
    31  	geodes                                     int
    32  	timePassed                                 int
    33  }
    34  
    35  func parseInput(input string) []blueprint {
    36  	input = strings.Replace(input, "\n", "", -1)
    37  	blueprints := []blueprint{}
    38  	for _, bpStr := range strings.Split(input, "Blueprint") {
    39  		if bpStr == "" {
    40  			continue
    41  		}
    42  		bp := blueprint{}
    43  		fmt.Sscanf(bpStr,
    44  			" %d: Each ore robot costs %d ore. Each clay robot costs %d ore. Each obsidian robot costs %d ore and %d clay. Each geode robot costs %d ore and %d obsidian.",
    45  			&bp.id, &bp.oreRobot.ore, &bp.clayRobot.ore, &bp.obsidianRobot.ore, &bp.obsidianRobot.clay, &bp.geodeRobot.ore, &bp.geodeRobot.obsidian,
    46  		)
    47  		bp.maxClayCost = util.Max(bp.clayRobot.clay, bp.oreRobot.clay, bp.obsidianRobot.clay, bp.geodeRobot.clay)
    48  		bp.maxOreCost = util.Max(bp.clayRobot.ore, bp.oreRobot.ore, bp.obsidianRobot.ore, bp.geodeRobot.ore)
    49  		bp.maxObsidianCost = util.Max(bp.clayRobot.obsidian, bp.oreRobot.obsidian, bp.obsidianRobot.obsidian, bp.geodeRobot.obsidian)
    50  		blueprints = append(blueprints, bp)
    51  	}
    52  	return blueprints
    53  }
    54  
    55  func getQualityNumbers(blueprints []blueprint, timeSteps int, parallel bool) []int {
    56  	qualityNumbers := make([]int, len(blueprints))
    57  	wg := sync.WaitGroup{}
    58  	for i, bp := range blueprints {
    59  		if parallel {
    60  			wg.Add(1)
    61  			go func(i int, bp blueprint) {
    62  				defer wg.Done()
    63  				qualityNumbers[i] = getMaxGeode(bp, timeSteps) * bp.id
    64  			}(i, bp)
    65  		} else {
    66  			qualityNumbers[i] = getMaxGeode(bp, timeSteps) * bp.id
    67  		}
    68  	}
    69  	wg.Wait()
    70  	return qualityNumbers
    71  }
    72  
    73  func hashState(s state) int {
    74  	hash := s.clay
    75  	hash |= s.ore << 8
    76  	hash |= s.obsidian << 16
    77  	hash |= s.timePassed << 24
    78  	return hash
    79  }
    80  
    81  func canBuild(s state, r ressources) bool {
    82  	return s.clay >= r.clay && s.obsidian >= r.obsidian && s.ore >= r.ore
    83  }
    84  
    85  func consumeRessources(s state, r ressources) state {
    86  	s.clay -= r.clay
    87  	s.ore -= r.ore
    88  	s.obsidian -= r.obsidian
    89  	return s
    90  }
    91  
    92  func getNeighbors(s state, bp blueprint, time int) []state {
    93  	neighbors := make([]state, 0)
    94  	if s.timePassed == time-1 {
    95  		return neighbors
    96  	}
    97  	// create doNothing state
    98  	doNothing := s
    99  	doNothing.timePassed++
   100  	doNothing.clay += doNothing.clayBots
   101  	doNothing.ore += doNothing.oreBots
   102  	doNothing.obsidian += doNothing.obsidianBots
   103  	// geodes are computed directly
   104  	// if geode bot is possible only build this
   105  	if canBuild(s, bp.geodeRobot) {
   106  		new := consumeRessources(doNothing, bp.geodeRobot)
   107  		new.geodeBots++
   108  		new.geodes += time - new.timePassed
   109  		// if max count for all other robots is reached skip next steps
   110  		if s.clayBots == bp.maxClayCost && s.oreBots == bp.maxOreCost && s.obsidianBots == bp.maxObsidianCost {
   111  			// one geode bot can be build per time step
   112  			remTime := time - new.timePassed
   113  			new.geodeBots += remTime
   114  			new.geodes += int(float64(remTime) * float64(remTime) / 2)
   115  			new.timePassed = time
   116  		}
   117  		neighbors = append(neighbors, new)
   118  		return neighbors
   119  	}
   120  	// append doNothing
   121  	neighbors = append(neighbors, doNothing)
   122  	// append other possible bots
   123  	if s.clayBots < bp.maxClayCost && canBuild(s, bp.clayRobot) {
   124  		new := consumeRessources(doNothing, bp.clayRobot)
   125  		new.clayBots++
   126  		neighbors = append(neighbors, new)
   127  	}
   128  	if s.oreBots < bp.maxOreCost && canBuild(s, bp.oreRobot) {
   129  		new := consumeRessources(doNothing, bp.oreRobot)
   130  		new.oreBots++
   131  		neighbors = append(neighbors, new)
   132  	}
   133  	if s.obsidianBots < bp.maxObsidianCost && canBuild(s, bp.obsidianRobot) {
   134  		new := consumeRessources(doNothing, bp.obsidianRobot)
   135  		new.obsidianBots++
   136  		neighbors = append(neighbors, new)
   137  	}
   138  	return neighbors
   139  }
   140  
   141  func getMaxGeode(bp blueprint, timeSteps int) int {
   142  	// calculate max number of geodes cracked
   143  	current := state{oreBots: 1}
   144  	open := []state{current}
   145  	best := current
   146  	closed := map[int]struct{}{}
   147  	for len(open) > 0 {
   148  		// get next state
   149  		current = open[len(open)-1]
   150  		open = open[:len(open)-1]
   151  		// check if new max can be reached
   152  		remTime := timeSteps - current.timePassed
   153  		if current.geodes+(remTime*(remTime/2)) <= best.geodes {
   154  			continue
   155  		}
   156  		closed[hashState(current)] = struct{}{}
   157  		if current.geodes > best.geodes {
   158  			best = current
   159  		}
   160  		for _, n := range getNeighbors(current, bp, timeSteps) {
   161  			if _, ok := closed[hashState(n)]; ok {
   162  				continue
   163  			}
   164  			open = append(open, n)
   165  		}
   166  	}
   167  	if debug {
   168  		fmt.Printf("Result blueprint %d: can open %d geodes\n", bp.id, best.geodes)
   169  	}
   170  	return best.geodes
   171  }
   172  
   173  func getMaxGeodes(blueprints []blueprint, timeSteps int, parallel bool) []int {
   174  	maxGeodes := make([]int, len(blueprints))
   175  	wg := sync.WaitGroup{}
   176  	for i, bp := range blueprints {
   177  		if parallel {
   178  			wg.Add(1)
   179  			go func(i int, bp blueprint) {
   180  				defer wg.Done()
   181  				maxGeodes[i] = getMaxGeode(bp, timeSteps)
   182  			}(i, bp)
   183  		} else {
   184  			maxGeodes[i] = getMaxGeode(bp, timeSteps)
   185  		}
   186  	}
   187  	wg.Wait()
   188  	return maxGeodes
   189  }
   190  
   191  var debug = false
   192  
   193  func Solve(input string, debugFlag bool, task int) (string, string) {
   194  	res1, res2 := 0, 0
   195  	debug = debugFlag
   196  	blueprints := parseInput(input)
   197  	if debug {
   198  		fmt.Println(blueprints)
   199  
   200  	}
   201  	if task != 2 {
   202  		qualities := getQualityNumbers(blueprints, 24, !debug)
   203  		fmt.Printf("Quality numbers: %v\n", qualities)
   204  		res1 = util.Sum(qualities...)
   205  	}
   206  	if task != 1 {
   207  		maxGeodes := getMaxGeodes(blueprints[:util.Min(3, len(blueprints))], 32, !debug)
   208  		fmt.Printf("Max geodes: %v\n", maxGeodes)
   209  		res2 = util.Mul(maxGeodes)
   210  	}
   211  	return fmt.Sprint(res1), fmt.Sprint(res2)
   212  }