Skip to content

Instantly share code, notes, and snippets.

@carlynorama
Last active December 17, 2023 02:53
Show Gist options
  • Save carlynorama/a8be78ce4562d74490925f64187d1c04 to your computer and use it in GitHub Desktop.
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.
//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