Last active
December 17, 2023 02:53
-
-
Save carlynorama/a8be78ce4562d74490925f64187d1c04 to your computer and use it in GitHub Desktop.
Day 12 a la gregtitus. I made a super crazy switch statement in my original code, but when I went back to do the whole memoize dance... someone else had shown you didn't have to! This felt much nicer.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//https://adventofcode.com/2023/day/12 | |
//Based on https://forums.swift.org/t/advent-of-code-2023/68749/61 | |
//Intro video Lydia | |
//https://www.youtube.com/watch?v=PK3wL7DXuuw | |
//Compterphile: Computers Without Memory | |
//https://www.youtube.com/watch?v=vhiiia1_hC4 | |
public struct Day12:AdventDay { | |
public let dayNum:Int = 12 | |
public let slug:String = "start-over" | |
public init() {} | |
struct SpringRecord { | |
let knownLayout:String | |
let requiredGroups:[Int] | |
init(_ fullString:some StringProtocol) { | |
let splitInfo = fullString.split(separator: " ", maxSplits: 1, omittingEmptySubsequences: false) | |
self.knownLayout = String(splitInfo[0]) | |
self.requiredGroups = splitInfo[1].split(separator:",").compactMap { Int($0)} | |
} | |
init(unfolded:some StringProtocol, count:Int) { | |
let splitInfo = unfolded.split(separator: " ", maxSplits: 1, omittingEmptySubsequences: false) | |
self.knownLayout = Array(repeating: String(splitInfo[0]), count: count).joined(separator: "?") | |
self.requiredGroups = Array(repeating: splitInfo[1].split(separator:",").compactMap { Int($0)}, count: count).flatMap({ $0 }) | |
} | |
//Will create an array of precisely the needed count. No padding. | |
func makeTransitionStates() -> [GroupStates] { | |
var minimumViableSprings:[GroupStates] = [] | |
for group in requiredGroups { | |
minimumViableSprings.append(.startState) | |
for _ in 0..<(group-1) { //...(group-2) will go negative when group size is 1 | |
minimumViableSprings.append(.groupStartedAndHasRoom) | |
} | |
minimumViableSprings.append(.expectedEndOfGroup) | |
} | |
minimumViableSprings.append(.allDamageForRecordFound) | |
//the amount per group, the spacers, and the all clear. | |
assert(minimumViableSprings.count == (requiredGroups.reduce(0,+)+requiredGroups.count+1)) | |
return minimumViableSprings | |
} | |
func generatePossibilities() -> Int { | |
//print("------------------------------------------------------") | |
let template = self.makeTransitionStates() | |
//print(record.requiredGroups) | |
//printStatesArray(template) | |
var possibilities = Array(repeating: 0, count: template.count) | |
possibilities[0] = 1 //get it started. | |
for springEntry in self.knownLayout { | |
//print("---------------------------") | |
//print("The current character is \(springEntry)") | |
//Starting from the end, how does adding this character that impact the number of options. | |
//Does it MOVE us closer th that state? | |
for (idx,possibleState) in template.enumerated().reversed() { | |
//What are the number of possibilities at that level count currently recorded? | |
let possibilityCountAtIndex = possibilities[idx] | |
//print(idx,possibleState,possibilityCountAtIndex) | |
//Don't bother if there aren't any possibility left at this state. | |
if possibilityCountAtIndex > 0 { | |
//Clean the slate for the decisions you're about to make. | |
possibilities[idx] = 0 | |
//print("looking at idx \(idx) with currently \(possibilityCountAtIndex) possibility") | |
//If spring's condition is given to us by the record, use it. Otherwise, do both. | |
if let springState = Alphabet_𝚺(springEntry) { | |
//print("chosen:\(springState)") | |
//the possibilities will be moved ahead (offset ==1) down the path, kept the same (offset == 0) | |
//or zoted out of existence (offset == nil) | |
if let offset = transitionImpacts(state:possibleState, inputSymbol:springState) { | |
//All these possibilities get added to that, too. | |
possibilities[idx+offset] += possibilityCountAtIndex | |
} | |
//else { | |
//print("nil response from \(springState)") | |
//} | |
} else { | |
//print("double the options.") | |
//For one path all the possibilities will be moved ahead, and for other they will stay the same. | |
//(provided the path is valid at all i.e. not nil.) | |
if let offsetA = transitionImpacts(state:possibleState, inputSymbol:.damaged) { | |
possibilities[idx+offsetA] += possibilityCountAtIndex | |
} | |
if let offsetB = transitionImpacts(state:possibleState, inputSymbol:.working) { | |
possibilities[idx+offsetB] += possibilityCountAtIndex | |
} | |
} | |
//print(possibilities) | |
} | |
} | |
} | |
return possibilities.suffix(2).reduce(0,+) | |
//print(possibilities) | |
} | |
enum GroupStates:String { | |
case startState = "start" //No damage yet but available to start. | |
case groupStartedAndHasRoom = "mid" | |
case expectedEndOfGroup = "expectEnd" | |
case allDamageForRecordFound = "Finished" | |
} | |
enum Alphabet_𝚺:String,CaseIterable { | |
case damaged = "#" | |
case working = "." | |
init?(_ c:String.Element) { | |
if c == "#" { self = .damaged } | |
else if c == "." { self = .working } | |
else { return nil } | |
} | |
} | |
//optionCountImpact will return nil for invalid options so | |
//the possibility counts will not increment. | |
//It returns 1 if the the combination should move a group's State forward | |
//It returns 0 is there is no impact to a group's State but is a valid option. | |
func transitionImpacts(state:GroupStates, inputSymbol:Alphabet_𝚺) -> Int? { | |
switch(inputSymbol) { | |
case .damaged: | |
switch state { | |
case .startState: | |
return 1 //begins a group. | |
case .groupStartedAndHasRoom: | |
return 1 //continues a group | |
case .expectedEndOfGroup: | |
return nil // BREAK needed to be a working to end the group. | |
case .allDamageForRecordFound: | |
return nil // BREAK have already placed all the damage. | |
} | |
case .working: | |
switch state { | |
case .startState: | |
return 0 //In between groups. | |
case .groupStartedAndHasRoom: | |
return nil //BREAK no good springs in the middle of a group. | |
case .expectedEndOfGroup: | |
return 1 // Actively needed to successfully end group. | |
case .allDamageForRecordFound: | |
return 0 //Done with groups at this point. | |
} | |
} | |
} | |
} | |
func getSplitString(isTest: Bool, isPart2: Bool) -> [String.SubSequence] { | |
guard let dataURL = dataURL(isTest: isTest, isPart2: false) else { | |
fatalError("no file") | |
} | |
if let input = try? String(contentsOf: dataURL).split(separator: "\n") { | |
return input | |
} else { | |
fatalError("Did you forget to add the input file again?") | |
} | |
} | |
public func part01(isTest: Bool) { | |
let records = getSplitString(isTest: isTest, isPart2: false).map({ SpringRecord($0)}) | |
var total:Int = 0 | |
for record in records { | |
total += record.generatePossibilities() | |
} | |
print(total) | |
} | |
public func part02(isTest: Bool) { | |
let records = getSplitString(isTest: isTest, isPart2: true).map({ SpringRecord(unfolded:$0, count: 5)}) | |
var total:Int = 0 | |
for record in records { | |
total += record.generatePossibilities() | |
} | |
print(total) | |
} | |
// func printStatesArray(_ array:[GroupStates]) { | |
// print(array.map({$0.rawValue})) | |
// } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment