פיתרון Advent Of Code יום 19 בסקאלה
אני כבר לא זוכר כמה זמן עבר מאז חידת ה Advent Of Code הקודמת שפתרתי כאן בסקאלה, אבל יצא שתוכניות השתנו היום וסוף סוף היה לי זמן להמשיך לפרק הבא. למי שעוקב אנחנו בחידה 19 מתוך 25 מה שאומר שיש סיכוי לא רע שעד סוף השנה אצליח לסיים את כל ה 25 חידות של שנה שעברה.
1. האתגר
היום סקאלה עבדה נגדי או שאולי פשוט השתמשתי בה לא נכון. הקלט שלנו נראה כך:
px{a<2006:qkq,m>2090:A,rfg}
pv{a>1716:R,A}
lnx{m>1548:A,A}
rfg{s<537:gd,x>2440:R,A}
qs{s>3448:A,lnx}
qkq{x<1416:A,crn}
crn{x>2662:A,R}
in{s<1351:px,qqz}
qqz{s>2770:qs,m<1801:hdj,R}
gd{a>3333:R,R}
hdj{m>838:A,pv}
{x=787,m=2655,a=1222,s=2876}
{x=1679,m=44,a=2067,s=496}
{x=2036,m=264,a=79,s=2244}
{x=2461,m=1339,a=466,s=291}
{x=2127,m=1623,a=2188,s=1013}
והוא מחולק לשני בלוקים. בבלוק התחתון יש רשימה של פריטים, לכל פריט יש 4 מאפיינים המסומנים באותיות x, m, a, s. הבלוק השני הוא "תוכנית" שמתחילה בשורה שמתחילה ב in וכוללת רשימת תנאים ותוצאות. כשיש לך פריט שמתאים לתנאי ממשיכים לתוצאה שלידו. התוצאה A אומרת שהפריט "התקבל" והתוצאה R אומרת שהפריט לא התקבל. אלה כמה דוגמאות לזרימה של פריטים בתוך תוכניות:
{x=787,m=2655,a=1222,s=2876}: in -> qqz -> qs -> lnx -> A
{x=1679,m=44,a=2067,s=496}: in -> px -> rfg -> gd -> R
{x=2036,m=264,a=79,s=2244}: in -> qqz -> hdj -> pv -> A
{x=2461,m=1339,a=466,s=291}: in -> px -> qkq -> crn -> R
{x=2127,m=1623,a=2188,s=1013}: in -> px -> rfg -> A
אז אנחנו רואים לדוגמה בפריט הראשון שהוא מתחיל ב in, ה s שלו גדול מ 1351 ולכן לא ממשיך ל px אלא ל qqz. שם ה s שלו גדול מ 2770 ולכן ממשיך ל lnx ובגלל שה m שלו גדול מ 1548 הוא מגיע ל A ופה מסיימים.
השאלה הראשונה היא לגלות איזה פריטים מגיעים ל A ולחבר את כל ה a, x, m ו s-ים שלהם.
בשאלה השנייה עלינו לדמיין שכל מאפיין יכול לקבל ערכים בין 1 ל 4000, ולגלות כמה פריטים תיאורטית סך הכל (בלי קשר לרשימה שהם נתנו) יכולים להגיע ל A.
2. פיתרון חלק ראשון
הגדרתי המון טיפוסים בסקאלה כדי שיעזרו לי להתמודד עם כל הטקסט של החידה:
import scala.io.Source
import scala.util.matching.Regex
object aoc2023day19 {
val MAX_VALUE = 4000
case class Item(x: Long, m: Long, a: Long, s: Long)
case class CompressedConditions(minX: Long, maxX: Long,
minM: Long, maxM: Long,
minA: Long, maxA: Long,
minS: Long, maxS: Long)
case class Condition(first: String, comparator: String, second: Long)
case class Rule(condition: Condition, destination: String)
type Workflow = List[Rule]
type Workflows = Map[String, Workflow]
val demoWorkflowLine: String = "ex{x>10:one,m<20:two,a>30:R,A}"
val demoWorkflow: String = """px{a<2006:qkq,m>2090:A,rfg}
|pv{a>1716:R,A}
|lnx{m>1548:A,A}
|rfg{s<537:gd,x>2440:R,A}
|qs{s>3448:A,lnx}
|qkq{x<1416:A,crn}
|crn{x>2662:A,R}
|in{s<1351:px,qqz}
|qqz{s>2770:qs,m<1801:hdj,R}
|gd{a>3333:R,R}
|hdj{m>838:A,pv}
|
|{x=787,m=2655,a=1222,s=2876}
|{x=1679,m=44,a=2067,s=496}
|{x=2036,m=264,a=79,s=2244}
|{x=2461,m=1339,a=466,s=291}
|{x=2127,m=1623,a=2188,s=1013}""".stripMargin
ואז המשכתי לכתוב את כל הפונקציות שמפענחות את הקלט:
// condition - x>10
def parseCondition(condition: String): Condition =
val pattern: Regex = "([xmas])([<>])(\\d+)".r
condition match
case "" => Condition("x", "<=", MAX_VALUE)
case pattern(a, b, c) => Condition(a, b, c.toLong)
// rule - x>10:one
def parseRule(rule: String): Rule =
rule.split(":") match
case Array(destination) => Rule(parseCondition(""), destination)
case Array(condition, destination) => Rule(parseCondition(condition), destination)
// workflow - ex{x>10:one,m<20:two,a>30:R,A}
def parseWorkflow(line: String): (String, Workflow) =
val List(name, workflow) = """(\w+)\{(.*)}""".r.findFirstMatchIn(line).get.subgroups
val rules = workflow.split(",").map(parseRule).toList
(name, rules)
def parseItem(line: String): Item =
val pattern = """\{x=(\d+),m=(\d+),a=(\d+),s=(\d+)}""".r
val List(x, m, a, s) = pattern.findFirstMatchIn(line).get.subgroups
Item(x.toLong,
m.toLong,
a.toLong,
s.toLong)
def parse(source: Source): (Workflows, List[Item]) =
val lines = source.getLines().toList
val workflows = lines
.takeWhile(_.nonEmpty)
.map(parseWorkflow)
.foldLeft(Map[String, Workflow]()) {(workflow, map) =>
val (name, data) = map
workflow ++ Map(name -> data)
}
val items = lines
.dropWhile(_.nonEmpty)
.drop(1)
.map(parseItem)
(workflows, items)
הפונקציה המעניינת הבאה מקבלת פריט ותנאי ובודקת אם הפריט מתאים לתנאי:
def checkCondition(condition: Condition, item: Item): Boolean =
val op1 = condition.first match
case "x" => item.x
case "m" => item.m
case "a" => item.a
case "s" => item.s
condition.comparator match
case "<" => op1 < condition.second
case "<=" => op1 <= condition.second
case ">" => op1 > condition.second
case ">=" => op1 >= condition.second
ואחריה אפשר היה לכתוב פונקציית המשך רקורביסית שבודקת אם הפריט מסתיים ב A או ב R:
def process(workflows: Workflows, current: Workflow, item: Item): Boolean =
current.find(r => checkCondition(r.condition, item)) match
case Some(rule) =>
rule.destination match
case "A" => true
case "R" => false
case next => process(workflows, workflows(next), item)
זה מספיק בשביל החלק הראשון:
def day19part1(): Unit =
val (workflows, items) = parse(Source.fromResource("day19.txt"))
val start = workflows("in")
println(items.filter {
item => process(workflows, start, item)
}.map { item =>
item.x + item.m + item.a + item.s
}.sum)
הרעיון המרכזי של החלק השני הוא הפונקציה הבאה:
def count(workflows: Workflows,
name: String,
ruleNumber: Int = 0,
constraints: List[Condition] = List()): Long =
if (name == "R") {
0L
} else if (name == "A") {
sizeOf(compress(constraints))
} else {
val rules = workflows(name)
val rule = rules(ruleNumber)
if (rule.condition.second < MAX_VALUE) {
count(workflows, rule.destination, 0, constraints :+ rule.condition) +
count(workflows, name, ruleNumber + 1, constraints :+ inverseCondition(rule.condition))
} else {
count(workflows, rule.destination, 0, constraints :+ rule.condition)
}
}
זו פונקציה רקורסיבית שעוברת על כל המסלולים שמגיעים ל A ו"אוספת" את כל התנאים שמגיעים לשם - לדוגמה אם היה תנאי x<50
אז נאסוף אותו כדי לדעת שבמסלול מסוים אנחנו יכולים להתקדם רק אם התנאי הזה מתקיים. כל "כלל" בתוכנית מייצר שתי אפשרויות, אפשרות אחת אם הכלל הזה אמיתי ואפשרות שניה אם הוא שקרי. אז לדוגמה אם יש לנו את השורה:
crn{x>2662:A,R}
אז אני יודע לחלק אותו לשני מסלולים - במסלול אחד x באמת קטן מ 2662 ואז נגיע ל A, ובמסלול שני x גדול או שווה ל 2662 ואז מגיעים ל R. זה הסיפור של החיבור הרקורסיבי שמופיע בפונקציה.
וכן בשביל שהפונקציה תעבוד היא צריכה את המימושים של פונקציות העזר:
def sizeOf(condition: CompressedConditions): Long =
(condition.maxX - condition.minX + 1) *
(condition.maxS - condition.minS + 1) *
(condition.maxA - condition.minA + 1) *
(condition.maxM - condition.minM + 1)
def compress(conditions: List[Condition]): CompressedConditions =
conditions.foldLeft(CompressedConditions(1, 4000, 1, 4000, 1, 4000, 1, 4000)) { (compressed, cond) =>
cond.first match
case "x" =>
cond.comparator match
case "<" if cond.second < compressed.maxX => compressed.copy(maxX=cond.second - 1)
case "<=" if cond.second < compressed.maxX => compressed.copy(maxX=cond.second)
case ">" if cond.second > compressed.minX => compressed.copy(minX=cond.second + 1)
case ">=" if cond.second > compressed.minX => compressed.copy(minX=cond.second)
case _ => compressed
case "m" =>
cond.comparator match
case "<" if cond.second < compressed.maxM => compressed.copy(maxM=cond.second - 1)
case "<=" if cond.second < compressed.maxM => compressed.copy(maxM=cond.second)
case ">" if cond.second > compressed.minM => compressed.copy(minM=cond.second + 1)
case ">=" if cond.second > compressed.minM => compressed.copy(minM=cond.second)
case _ => compressed
case "a" =>
cond.comparator match
case "<" if cond.second < compressed.maxA => compressed.copy(maxA=cond.second - 1)
case "<=" if cond.second < compressed.maxA => compressed.copy(maxA=cond.second)
case ">" if cond.second > compressed.minA => compressed.copy(minA=cond.second + 1)
case ">=" if cond.second > compressed.minA => compressed.copy(minA=cond.second)
case _ => compressed
case "s" =>
cond.comparator match
case "<" if cond.second < compressed.maxS => compressed.copy(maxS = cond.second - 1)
case "<=" if cond.second < compressed.maxS => compressed.copy(maxS = cond.second)
case ">" if cond.second > compressed.minS => compressed.copy(minS = cond.second + 1)
case ">=" if cond.second > compressed.minS => compressed.copy(minS = cond.second)
case _ => compressed
}
def inverseCondition(condition: Condition): Condition =
condition.comparator match
case "<" => condition.copy(comparator=">=")
case "<=" => condition.copy(comparator=">")
case ">" => condition.copy(comparator="<=")
case ">=" => condition.copy(comparator = "<")
וכל התנאים האלה הם מה שהתכוונתי כשכתבתי שסקאלה עבדה לרעתי.
סך הכל קוד הפיתרון המלא בסקאלה הוא:
import scala.io.Source
import scala.util.matching.Regex
object aoc2023day19 {
val MAX_VALUE = 4000
case class Item(x: Long, m: Long, a: Long, s: Long)
case class CompressedConditions(minX: Long, maxX: Long,
minM: Long, maxM: Long,
minA: Long, maxA: Long,
minS: Long, maxS: Long)
case class Condition(first: String, comparator: String, second: Long)
case class Rule(condition: Condition, destination: String)
type Workflow = List[Rule]
type Workflows = Map[String, Workflow]
val demoWorkflowLine: String = "ex{x>10:one,m<20:two,a>30:R,A}"
val demoWorkflow: String = """px{a<2006:qkq,m>2090:A,rfg}
|pv{a>1716:R,A}
|lnx{m>1548:A,A}
|rfg{s<537:gd,x>2440:R,A}
|qs{s>3448:A,lnx}
|qkq{x<1416:A,crn}
|crn{x>2662:A,R}
|in{s<1351:px,qqz}
|qqz{s>2770:qs,m<1801:hdj,R}
|gd{a>3333:R,R}
|hdj{m>838:A,pv}
|
|{x=787,m=2655,a=1222,s=2876}
|{x=1679,m=44,a=2067,s=496}
|{x=2036,m=264,a=79,s=2244}
|{x=2461,m=1339,a=466,s=291}
|{x=2127,m=1623,a=2188,s=1013}""".stripMargin
// condition - x>10
def parseCondition(condition: String): Condition =
val pattern: Regex = "([xmas])([<>])(\\d+)".r
condition match
case "" => Condition("x", "<=", MAX_VALUE)
case pattern(a, b, c) => Condition(a, b, c.toLong)
// rule - x>10:one
def parseRule(rule: String): Rule =
rule.split(":") match
case Array(destination) => Rule(parseCondition(""), destination)
case Array(condition, destination) => Rule(parseCondition(condition), destination)
// workflow - ex{x>10:one,m<20:two,a>30:R,A}
def parseWorkflow(line: String): (String, Workflow) =
val List(name, workflow) = """(\w+)\{(.*)}""".r.findFirstMatchIn(line).get.subgroups
val rules = workflow.split(",").map(parseRule).toList
(name, rules)
def parseItem(line: String): Item =
val pattern = """\{x=(\d+),m=(\d+),a=(\d+),s=(\d+)}""".r
val List(x, m, a, s) = pattern.findFirstMatchIn(line).get.subgroups
Item(x.toLong,
m.toLong,
a.toLong,
s.toLong)
def parse(source: Source): (Workflows, List[Item]) =
val lines = source.getLines().toList
val workflows = lines
.takeWhile(_.nonEmpty)
.map(parseWorkflow)
.foldLeft(Map[String, Workflow]()) {(workflow, map) =>
val (name, data) = map
workflow ++ Map(name -> data)
}
val items = lines
.dropWhile(_.nonEmpty)
.drop(1)
.map(parseItem)
(workflows, items)
def checkCondition(condition: Condition, item: Item): Boolean =
val op1 = condition.first match
case "x" => item.x
case "m" => item.m
case "a" => item.a
case "s" => item.s
condition.comparator match
case "<" => op1 < condition.second
case "<=" => op1 <= condition.second
case ">" => op1 > condition.second
case ">=" => op1 >= condition.second
def inverseCondition(condition: Condition): Condition =
condition.comparator match
case "<" => condition.copy(comparator=">=")
case "<=" => condition.copy(comparator=">")
case ">" => condition.copy(comparator="<=")
case ">=" => condition.copy(comparator = "<")
def process(workflows: Workflows, current: Workflow, item: Item): Boolean =
current.find(r => checkCondition(r.condition, item)) match
case Some(rule) =>
rule.destination match
case "A" => true
case "R" => false
case next => process(workflows, workflows(next), item)
def sizeOf(condition: CompressedConditions): Long =
(condition.maxX - condition.minX + 1) *
(condition.maxS - condition.minS + 1) *
(condition.maxA - condition.minA + 1) *
(condition.maxM - condition.minM + 1)
def compress(conditions: List[Condition]): CompressedConditions =
conditions.foldLeft(CompressedConditions(1, 4000, 1, 4000, 1, 4000, 1, 4000)) { (compressed, cond) =>
cond.first match
case "x" =>
cond.comparator match
case "<" if cond.second < compressed.maxX => compressed.copy(maxX=cond.second - 1)
case "<=" if cond.second < compressed.maxX => compressed.copy(maxX=cond.second)
case ">" if cond.second > compressed.minX => compressed.copy(minX=cond.second + 1)
case ">=" if cond.second > compressed.minX => compressed.copy(minX=cond.second)
case _ => compressed
case "m" =>
cond.comparator match
case "<" if cond.second < compressed.maxM => compressed.copy(maxM=cond.second - 1)
case "<=" if cond.second < compressed.maxM => compressed.copy(maxM=cond.second)
case ">" if cond.second > compressed.minM => compressed.copy(minM=cond.second + 1)
case ">=" if cond.second > compressed.minM => compressed.copy(minM=cond.second)
case _ => compressed
case "a" =>
cond.comparator match
case "<" if cond.second < compressed.maxA => compressed.copy(maxA=cond.second - 1)
case "<=" if cond.second < compressed.maxA => compressed.copy(maxA=cond.second)
case ">" if cond.second > compressed.minA => compressed.copy(minA=cond.second + 1)
case ">=" if cond.second > compressed.minA => compressed.copy(minA=cond.second)
case _ => compressed
case "s" =>
cond.comparator match
case "<" if cond.second < compressed.maxS => compressed.copy(maxS = cond.second - 1)
case "<=" if cond.second < compressed.maxS => compressed.copy(maxS = cond.second)
case ">" if cond.second > compressed.minS => compressed.copy(minS = cond.second + 1)
case ">=" if cond.second > compressed.minS => compressed.copy(minS = cond.second)
case _ => compressed
}
@main
def day19part1(): Unit =
val (workflows, items) = parse(Source.fromString(demoWorkflow))
val start = workflows("in")
println(items.filter {
item => process(workflows, start, item)
}.map { item =>
item.x + item.m + item.a + item.s
}.sum)
def count(workflows: Workflows,
name: String,
ruleNumber: Int = 0,
constraints: List[Condition] = List()): Long =
if (name == "R") {
0L
} else if (name == "A") {
sizeOf(compress(constraints))
} else {
val rules = workflows(name)
val rule = rules(ruleNumber)
if (rule.condition.second < MAX_VALUE) {
count(workflows, rule.destination, 0, constraints :+ rule.condition) +
count(workflows, name, ruleNumber + 1, constraints :+ inverseCondition(rule.condition))
} else {
count(workflows, rule.destination, 0, constraints :+ rule.condition)
}
}
@main
def day19part2(): Unit =
val (workflows, items) = parse(Source.fromString(demoWorkflow))
println(count(workflows, "in"))
}
אם יש לכם רעיונות איך לכתוב את זה בסקאלה עם פחות חזרתיות אל תתביישו לשתף בתגובות או בטלגרם.