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

     1  package day16
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"strings"
     7  	"time"
     8  )
     9  
    10  type valve struct {
    11  	name        string
    12  	flow        int
    13  	f           int
    14  	timeToOpen  int
    15  	open        bool
    16  	connections []string
    17  	id          int64
    18  	inPath      int64
    19  	curFlow     int
    20  	hash        complex128
    21  	openedCount int
    22  }
    23  
    24  func (v *valve) calc(time int, current valve) {
    25  	v.inPath = current.inPath
    26  	v.timeToOpen = current.timeToOpen + 1
    27  	v.f = current.f + v.flow*(time-v.timeToOpen)
    28  	v.curFlow = current.curFlow + v.flow
    29  	v.openedCount = current.openedCount
    30  	if v.open {
    31  		v.inPath = v.inPath | v.id
    32  		v.openedCount = current.openedCount + 1
    33  	}
    34  	open := int64(0)
    35  	if v.open {
    36  		open = 1
    37  	}
    38  	v.hash = complex(float64(v.inPath), float64(v.id+open))
    39  }
    40  
    41  func (v valve) String() string {
    42  	return fmt.Sprintf("Valve: %s (%d), Flow: %d, Connections to: %v\n  Path: %d\n  curFlow: %v",
    43  		v.name, v.id, v.flow, v.connections, v.inPath, v.curFlow)
    44  }
    45  
    46  func parseValves(input string) map[string]valve {
    47  	valves := make(map[string]valve)
    48  	connections := make(map[string]string)
    49  	for i, l := range strings.Split(string(input), "\n") {
    50  		if l == "" {
    51  			continue
    52  		}
    53  		parts := strings.Split(l, " ")
    54  		connection := ""
    55  		for _, p := range parts[9:] {
    56  			connection += p
    57  		}
    58  		flow, _ := strconv.Atoi(parts[4][5 : len(parts[4])-1])
    59  		id := int64(1) << i
    60  		valves[parts[1]+"C"] = valve{
    61  			name: parts[1] + "C",
    62  			flow: 0,
    63  			id:   id,
    64  		}
    65  		valves[parts[1]+"O"] = valve{
    66  			name: parts[1] + "O",
    67  			flow: flow,
    68  			open: true,
    69  			id:   id,
    70  		}
    71  		connections[parts[1]+"O"] = connection
    72  		connections[parts[1]+"C"] = connection
    73  	}
    74  	for name, connection := range connections {
    75  		val := valves[name]
    76  		for _, c := range strings.Split(connection, ",") {
    77  			val.connections = append(val.connections, c+"C")
    78  		}
    79  		val.connections = append(val.connections, name[:len(name)-1]+"C")
    80  		if !valves[name].open {
    81  			val.connections = append(val.connections, name[:len(name)-1]+"O")
    82  		}
    83  		valves[name] = val
    84  	}
    85  	return valves
    86  }
    87  
    88  func findMaxPressure(valves map[string]valve, time int, startPoint string, multipleWorkers bool) (int, map[int64]valve) {
    89  	open := make([]valve, 0)
    90  	openHash := make(map[complex128]bool)
    91  	// start with closed version of first valve
    92  	current := valves[startPoint+"C"]
    93  	current.calc(time, valve{
    94  		timeToOpen: -1,
    95  	})
    96  	open = append(open, current)
    97  	bestGoal := current
    98  	bestPaths := make(map[int64]valve)
    99  	cache := make(map[int64]int)
   100  	maxFlow := 0
   101  	workingValves := 0
   102  	for _, v := range valves {
   103  		maxFlow += v.flow
   104  		if v.flow > 0 {
   105  			workingValves++
   106  		}
   107  	}
   108  	// explore paths using a* like algorithm
   109  	for len(open) > 0 {
   110  		// choose best item of open list
   111  		maxScore := 0
   112  		for i, v := range open {
   113  			if v.f > open[maxScore].f {
   114  				maxScore = i
   115  			}
   116  		}
   117  		current = open[maxScore]
   118  		open = append(open[:maxScore], open[maxScore+1:]...)
   119  		delete(openHash, current.hash)
   120  
   121  		// check if better solution is found
   122  		if current.timeToOpen == time-1 {
   123  			overallPaths++
   124  			if current.f > bestGoal.f {
   125  				bestGoal = current
   126  			}
   127  			// check paths
   128  			bestPath, ok := bestPaths[current.inPath]
   129  			balanced := true
   130  			if multipleWorkers {
   131  				percentOpen := float32(current.openedCount) / float32(workingValves)
   132  				if percentOpen < 0.3 || percentOpen > 0.7 {
   133  					balanced = false
   134  				}
   135  			}
   136  			if (!ok || current.f > bestPath.f) && balanced {
   137  				bestPaths[current.inPath] = current
   138  			}
   139  			//}
   140  		}
   141  
   142  		for _, v := range current.connections {
   143  			// check if trying to open already open valve or valve without flow
   144  			if valves[v].open && (current.inPath&valves[v].id > 0 || valves[v].flow == 0) {
   145  				continue
   146  			}
   147  			val := valves[v]
   148  			val.calc(time, current)
   149  			pressure, ok := cache[val.inPath]
   150  			if ok && pressure > val.f {
   151  				cacheHit++
   152  				continue
   153  			}
   154  			if !multipleWorkers && (maxFlow-val.curFlow)*(time-val.timeToOpen) < bestGoal.f-val.f {
   155  				possibilityCheck++
   156  				continue
   157  			}
   158  			if val.timeToOpen > time || openHash[val.hash] {
   159  				continue
   160  			} else {
   161  				cache[val.inPath] = val.f
   162  				open = append(open, val)
   163  				openHash[val.hash] = true
   164  			}
   165  		}
   166  
   167  	}
   168  	return bestGoal.f, bestPaths
   169  }
   170  
   171  func findBestDisjointPath(bestPaths map[int64]valve) (valve, valve) {
   172  	v1, v2 := valve{}, valve{}
   173  	for a, aVal := range bestPaths {
   174  	inner:
   175  		for b, bVal := range bestPaths {
   176  			if a == b {
   177  				continue
   178  			}
   179  
   180  			if aVal.inPath&bVal.inPath != 0 {
   181  				continue inner
   182  			}
   183  			if aVal.f+bVal.f > v1.f+v2.f {
   184  				v1 = aVal
   185  				v2 = bVal
   186  			}
   187  		}
   188  	}
   189  	return v1, v2
   190  }
   191  
   192  var cacheHit = 0
   193  var possibilityCheck = 0
   194  var overallPaths = 0
   195  
   196  func Solve(input string, test bool, tasks int) (string, string) {
   197  	start := time.Now()
   198  	valves := parseValves(input)
   199  	if test {
   200  		for _, v := range valves {
   201  			fmt.Println(v)
   202  		}
   203  	}
   204  	parsing := time.Now()
   205  
   206  	ret1, ret2 := "", ""
   207  	// Task 1
   208  	if tasks == 0 || tasks == 1 {
   209  		maxPressure, _ := findMaxPressure(valves, 30, "AA", false)
   210  		fmt.Println("Part 1:")
   211  		fmt.Println("Cache hits:", cacheHit)
   212  		fmt.Println("Possibility checks:", possibilityCheck)
   213  		fmt.Println("Max Pressure:", maxPressure)
   214  		ret1 = strconv.Itoa(maxPressure)
   215  	}
   216  	task1 := time.Now()
   217  
   218  	// Task 2
   219  	if tasks == 0 || tasks == 2 {
   220  		cacheHit = 0
   221  		possibilityCheck = 0
   222  		_, bestPath := findMaxPressure(valves, 26, "AA", true)
   223  		fmt.Println("\n\nPart 2:")
   224  		fmt.Println("Cache hits:", cacheHit)
   225  		fmt.Println("Possibility checks:", possibilityCheck)
   226  		fmt.Println("Possible paths: ", len(bestPath))
   227  		fmt.Println("Overall paths: ", overallPaths)
   228  		path1, path2 := findBestDisjointPath(bestPath)
   229  		if test {
   230  			fmt.Println("Opened paths:")
   231  			fmt.Println(" PATH 1: ", path1)
   232  			fmt.Println(" PATH 2: ", path2)
   233  		}
   234  		fmt.Println("Max Pressures with help: ", path1.f, path2.f)
   235  		fmt.Println("Sum: ", path1.f+path2.f)
   236  		fmt.Println(path1)
   237  		fmt.Println(path2)
   238  		ret2 = strconv.Itoa(path1.f + path2.f)
   239  	}
   240  	task2 := time.Now()
   241  	fmt.Println("Time: parsing ", parsing.Sub(start), " task1 ", task1.Sub(parsing), " task2 ", task2.Sub(task1), " total ", task2.Sub(start))
   242  
   243  	return ret1, ret2
   244  }