Advent of Code

Gute Links und Tutorials könnt ihr hier posten.
Manul
User
Beiträge: 53
Registriert: Samstag 13. Februar 2021, 16:00

kbr hat geschrieben: Sonntag 3. Dezember 2023, 18:13 Das die Lösung noch schneller geht, soll dich in keiner Weise demotivieren. Im Gegenteil! Gewiss können es andere noch schneller, als es bei meiner Variante der Fall ist. Also bleib am Ball und viel Spaß mit den noch kommenden Aufgaben!
Keine Sorge, das hatte ich nicht anders verstanden - trotzdem vielen Dank. daß Du das noch mal explizit so schreibst. Ich sehe das hier tatsächlich nicht als Wettbewerb, sondern als Spiel und als Gelegenheit zum Lernen. Auch Dir und allen weiteren Mitspielenden viel Spaß!

Mein Tag 3 sieht so aus:

Code: Alles auswählen

#!/usr/bin/python3

import fileinput
import re

from functools import reduce
from operator import mul

NUMBER = re.compile("\d+")
SYMBOL = re.compile("[^\d\.]")

class Number():
  def __init__(self,x,y,value):
    self.value = int(value)
    self.x = x
    self.y = y
    self.length = len(value)

  def __repr__(self):
    return "Number {} at {} spanning {} spaces".format(self.value, (self.x,self.y), self.length)

  def __contains__(self, position):
    return self.x-1 <= position.x <= self.x+self.length and self.y-1 <= position.y <= self.y+1

class Part():
  def __init__(self,x,y,symbol):
    self.x = x
    self.y = y
    self.symbol = symbol

  def __repr__(self):
    return "Part '{}' at {}".format(self.symbol, (self.x,self.y))

def read_input():
  parts = []
  numbers = []

  for y, line in enumerate(fileinput.input()):
    line = line.strip()
    for part in SYMBOL.finditer(line):
      x = part.start()
      parts.append(Part(x,y,part.group(0)))
    for number in NUMBER.finditer(line):
      x = number.start()
      numbers.append(Number(x,y,number.group(0)))

  return parts, numbers

def main():
  parts, numbers = read_input()

  # part 1
  part_numbers = [ number for number in numbers if any(part in number for part in parts) ]
  print(sum(number.value for number in part_numbers))

  # part 2
  stars = [ part for part in parts if part.symbol == "*" ]
  stars_with_neighbors = [ (star,[number for number in part_numbers if star in number]) for star in stars ]
  gears = [ (star,neighbors) for star,neighbors in stars_with_neighbors if len(neighbors) == 2 ]
  gear_ratios = [ reduce(mul,[n.value for n in neighbors]) for _, neighbors in gears ]

  print(sum(gear_ratios))

if __name__ == "__main__":
  main()
Tag 4 habe ich so gelöst:

Code: Alles auswählen

#!/usr/bin/python3

import fileinput

class Card:
  def __init__(self, line):
    card, numbers = line.strip().split(":")
    self.winning, self.guesses = (part.strip().split() for part in numbers.split("|"))
    assert len(set(self.guesses)) == len(self.guesses)
    self.wins = set(self.winning) & set(self.guesses)
    self.value = 2 ** (len(self.wins) - 1) if self.wins else 0

  def __radd__(self, other):
    return other + self.value

def main():
  cards = [ Card(line) for line in fileinput.input() ]

  # part 1
  print(sum(cards))

  # part 2
  card_numbers = [ 1 ] * len(cards)

  for number, card in enumerate(cards):
    for i in range(len(card.wins)):
      card_numbers[number + i + 1] += card_numbers[number]

  print(sum(card_numbers))

if __name__ == "__main__":
  main()
Bis jetzt meine kürzeste Lösung.
__blackjack__ hat geschrieben: Montag 4. Dezember 2023, 12:25 Heute ist ja wieder eine einfache Aufgabe. Frage mich ob die auch in Betracht ziehen was Wochentag und was Wochenende ist, bei der Aufgabenstellung.
Den Gedanken hatte ich heute morgen auch kurz. Wenn mich meine Erinnerung an die letzten Jahre nicht trügt, glaube ich das aber eher nicht - ich meine mich viel mehr an eine mehr oder weniger stetig anwachsende Komplexität der Aufgaben zu erinnern.
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

Mit sauberer Rekursion geht Tag 4 Teil 2. Hat ein paar Versuche gebraucht, bis ich das unfallfrei hinbekommen habe, aber dann lief es in 1.2 Sekunden durch. Immerhin.
Benutzeravatar
snafu
User
Beiträge: 6742
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Hab nun auch die flotte Lösung hingekriegt. Man sollte bei Part 2 halt nicht wirklich Millionen von Elementen in die Liste stecken, sondern den Vorgang durch Berechnung quasi simulieren. Der naive Weg war dennoch erstmal gut, um die korrekte Lösung zu finden und sich mit der Herangehensweise vertraut zu machen. Hier jedenfalls die performante Lösung:

Code: Alles auswählen

from collections import namedtuple


class Card(
    namedtuple("Card", "id winning_numbers player_numbers")
):
    def get_matching_numbers(self):
        return set(self.winning_numbers) & set(self.player_numbers)

    def get_points(self):
        amount = len(self.get_matching_numbers())
        return 2 ** (amount - 1) if amount else 0

    @classmethod
    def from_line(cls, line):
        metadata, contents = line.split(": ")
        winning_numbers, player_numbers = contents.split(" | ")
        return cls(
            int(metadata.split()[1]),
            list(map(int, winning_numbers.split())),
            list(map(int, player_numbers.split()))
        )


def get_copy_results(cards):
    matching_amounts = [len(card.get_matching_numbers()) for card in cards]
    results = [1 for _ in range(len(cards))]
    for card in cards:
        amount = matching_amounts[card.id - 1]
        for i in range(card.id, card.id + amount):
            results[i] += results[card.id - 1]
    return results


def main():
    with open("input4.txt") as stream:
        game = list(map(Card.from_line, stream))
    print("Part 1:", sum(card.get_points() for card in game))
    print("Part 2:", sum(get_copy_results(game)))


if __name__ == "__main__":
    main()
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

@__blackjack__ ja du hast Recht, das war Mist. Ist mir im "Wahn" gar nicht mehr aufgefallen, habs angepasst.

Teil 2 habe ich jetzt auch, musste mich aber schon mit Papier und Stift hinsetzen. Einfach lesen, denken und schreiben, hat hier nicht hingehauen.

Code: Alles auswählen

#!/usr/bin/env python3
from pathlib import Path
import re


INPUT = Path("/home/dennis/AoC/2023/Day4/input.txt")

EXAMPLE_LINES = """\
Card 1: 41 48 83 86 17 | 83 86  6 31 17  9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3:  1 21 53 59 44 | 69 82 63 72 16 21 14  1
Card 4: 41 92 73 84 69 | 59 84 76 51 58  5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11""".splitlines()


def parse_cards(input_lines):
    cards = {}
    for card in input_lines:
        card_number, numbers = card.split(":")
        my_numbers, winning_numbers = numbers.strip().split("|")
        my_numbers = set(map(int, re.findall(r"\d+", my_numbers)))
        winning_numbers = set(map(int, re.findall(r"\d+", winning_numbers)))
        cards[int(card_number.replace("Card ", ""))] = my_numbers & winning_numbers
    return cards


def calculate_points(winnings):
    return 2 ** (winnings - 1) if winnings >= 1 else 0


def update_number_of_cards(card_to_wins, wins, card_id):
    for extra_card in range(1, wins + 1):
        card_to_wins[card_id + extra_card] += card_to_wins[card_id]
    return card_to_wins


def main():
    cards = parse_cards(INPUT.read_text(encoding="UTF-8").splitlines())
    print(sum(calculate_points(len(winning)) for winning in cards.values()))
    cards = {int(card_id): len(win) for card_id, win in cards.items()}
    card_to_wins = {card_id: 1 for card_id, _ in enumerate(cards, 1)}
    for card_id, wins in cards.items():
        card_to_wins = update_number_of_cards(card_to_wins, wins, card_id)
    print(sum(card_to_wins.values()))


if __name__ == "__main__":
    main()
Wenn ich @Manul Lösung ansehen, finde ich zwar ein paar Ähnlichkeiten und merke, dass das wesentlich einfach gegangen wäre. Naja immerhin noch 16 Minuten vor dem nächsten Rätsel zur Lösung gekommen.

Gute Nacht und hoffentlich bis morgen
Dennis

Danke nochmals, das ihr hier eure Lösungen teil!
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

snafu hat geschrieben: Montag 4. Dezember 2023, 23:42 Hab nun auch die flotte Lösung hingekriegt.
Ja, nach der einfachen Lösung hat es mir für heute erst einmal gereicht. Vielleicht ist mein Rechner auch zu schnell.
Benutzeravatar
snafu
User
Beiträge: 6742
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ich finde es immer kurios, wenn man eine gute Lösung gefunden hat und die vielleicht noch ein paar Mal durchdenkt, dann fragt man sich, warum man nicht früher drauf gekommen ist. Part 1 war keine große Sache und der "Trick" mit der Zweierpotenz sollte so ziemlich jedem einleuchten, der mal Informatik in der Schule oder spätestens im Studium hatte. Bei Part 2 ging die "dumme" Lösung noch recht schnell: Einfach die Karten in eine 2-dimensionale Liste stecken, sodass man mit einer Liste von einelementigen Unterlisten startet und dann nach und nach die Karten anhängt. So hatte ich es zumindest gemacht und war erschrocken, wie lange der am rödeln war. Ich hatte allerdings auch nicht erwartet, dass diese Menge an Karten-Kopien zusammenkommt. Beim berechneten Ansatz hatte ich dann anfangs Schwierigkeiten mit dem Umdenken und erst nicht so tollen Code gehabt. Aber am Ende hatte es dann doch Klick im Kopf gemacht. :)
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

Gestern war es wohl schon zu spät, aber diese Nacht fiel mir die Lösung für die schnelle Variante ein (Tag 4, Teil 2), ohne dass ich darüber aktiv nachgedacht hätte. Im Nachhinein wundert man sich dann, warum man da nicht früher drauf gekommen ist. Aber das ist ja öfter so ;)

Code: Alles auswählen

fname = "input.txt"


class Card:

    def __init__(self, deck, card_number, winning_numbers, own_numbers):
        self.deck = deck
        self.card_number = card_number
        self.winning_numbers = set(winning_numbers)
        self.own_numbers = set(own_numbers)
        self._collected_cards = 0

    def __repr__(self):
        return f"Card({self.card_number}, {self._collected_cards})"

    @property
    def matches(self):
        return len(self.winning_numbers & self.own_numbers)

    @property
    def collected_cards(self):
        if not self._collected_cards:
            for i in range(self.matches):
                card = self.deck.cards[self.card_number + i + 1]
                self._collected_cards += card.collected_cards
        return self._collected_cards + 1  # add itself

    @classmethod
    def from_numbers(cls, deck, card_number, numbers):
        winning_numbers, own_numbers = numbers.split("|")
        return Card(
            deck,
            card_number,
            map(int, winning_numbers.split()),
            map(int, own_numbers.split())
        )


class Deck:

    def __init__(self):
        self.cards = []

    def parse(self, fname):
        with open(fname) as fobj:
            for card_number, line in enumerate(fobj):
                card, numbers = line.split(":")
                self.cards.append(Card.from_numbers(self, card_number, numbers))

    @property
    def collected_cards(self):
        return sum(card.collected_cards for card in reversed(self.cards))


deck = Deck()
deck.parse(fname)
print(deck.collected_cards)
  
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Tag 4 in BASIC. War eigentlich grundsätzlich gestern spät abends schon fertig, aber da war noch ein Fehler drin. Und bei einer Laufzeit von 14 Minuten und 40 Sekunden ziehen sich Tests immer so ein bisschen. 🤣

Code: Alles auswählen

   10 TI$="000000":R1=0:R2=0
   20 DIM C%(200),W%(99),N(200),NN(200)
  100 OPEN 1,8,2,"INPUT04,S"
  110 PRINT"READING & PART 1"
  120 CI=0
  130 IF ST<>0 THEN 500
  140 CI=CI+1
  150 PRINT CI:PRINT"{UP}";
  160 FOR I=1 TO 99:W%(I)=0:NEXT
  170 FOR I=1 TO LEN("CARD ###:"):GET#1,C$:NEXT
  180 FOR I=1 TO 10
  190 GOSUB 1000:W%(N)=1
  200 NEXT
  210 FOR I=1 TO 2:GET#1,C$:NEXT
  220 C=0
  230 FOR I=1 TO 25
  240 GOSUB 1000:C=C+W%(N)
  250 NEXT:C%(CI)=C
  260 R1=R1+INT(2↑(C-1))
  270 GET#1,C$:REM CR
  280 GOTO 130
  500 CLOSE 1
  510 PRINT:PRINT R1
  520 PRINT "PART 2"
  530 FOR I=1 TO CI:N(I)=1:NEXT
  540 PRINT R2:PRINT"{UP}";
  550 N=0
  560 FOR I=1 TO CI:N=N+N(I):NEXT
  570 IF N=0 THEN 950
  580 R2=R2+N
  590 FOR I=1 TO CI:NN(I)=0:NEXT
  600 FOR I=1 TO CI
  610 W=C%(I):IF W=0 THEN 660
  620 C=N(I)
  630 FOR J=1 TO W
  640 K=I+J:NN(K)=NN(K)+C
  650 NEXT
  660 NEXT
  670 FOR I=1 TO CI:N(I)=NN(I):NEXT
  680 GOTO 540
  950 PRINT:PRINT TI$
  960 END
 1000 N=0:FOR J=1 TO 3
 1010 GET#1,C$
 1020 N=N*10+VAL(C$)
 1030 NEXT:RETURN
Und hier die Python-Lösung für Tag 4:

Code: Alles auswählen

#!/usr/bin/env python3
import sys
from collections import Counter

from attr import attrib, attrs
from parse import parse

EXAMPLE_LINES = """\
Card 1: 41 48 83 86 17 | 83 86  6 31 17  9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3:  1 21 53 59 44 | 69 82 63 72 16 21 14  1
Card 4: 41 92 73 84 69 | 59 84 76 51 58  5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11
""".splitlines()


def parse_numbers(text):
    return set(map(int, text.split()))


@attrs(frozen=True)
class Card:
    number = attrib()
    winning_numbers = attrib(cmp=False)
    numbers = attrib(cmp=False)

    @property
    def matching_numbers_count(self):
        return len(self.numbers & self.winning_numbers)

    @property
    def points(self):
        return int(2 ** (self.matching_numbers_count - 1))

    @classmethod
    def parse(cls, line):
        card_number, winning_text, numbers_text = parse(
            "Card {:>d}: {} | {}", line.rstrip()
        )
        return cls(
            card_number,
            parse_numbers(winning_text),
            parse_numbers(numbers_text),
        )


def parse_lines(lines):
    return map(Card.parse, lines)


def sum_points(cards):
    """
    >>> sum_points(parse_lines(EXAMPLE_LINES))
    13
    """
    return sum(card.points for card in cards)


def count_cards(cards):
    """
    >>> count_cards(parse_lines(EXAMPLE_LINES))
    30
    """
    number_to_card = {card.number: card for card in cards}
    total_count = 0
    card_to_count = Counter(number_to_card.values())
    while card_to_count:
        total_count += sum(card_to_count.values())

        new_card_to_count = Counter()
        for card, count in card_to_count.items():
            for offset in range(card.matching_numbers_count):
                new_card_to_count[
                    number_to_card.get(card.number + 1 + offset)
                ] += count

        card_to_count = new_card_to_count

    return total_count


def main():
    cards = list(parse_lines(sys.stdin))
    print(sum_points(cards))
    print(count_cards(cards))


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Manul
User
Beiträge: 53
Registriert: Samstag 13. Februar 2021, 16:00

Heute war (zumindest für mich) Fleißarbeit und Konzentration angesagt: Die Lösung für den ersten Teil konnte ich nach längerem Nachdenken noch mehr oder weniger direkt runterschreiben, war aber auch schon deutlich länger als meine bisher längste Lösung dieses Jahr.

Für Teil zwei hat meine Lösung für die Beispieldaten zwar auch funktioniert, hätte aber für den echten Input ein Ergebnis vermutlich nicht mehr innerhalb meiner Lebenszeit geliefert - ich musste also noch mal umstricken, und auch, wenn ich einiges aus der ersten Lösung wiederverwenden konnte, hat das noch mal sehr viel Konzentration erfordert. Aber jetzt klappt's. :)
nezzcarth
User
Beiträge: 1636
Registriert: Samstag 16. April 2011, 12:47

Der naive Ansatz (wozu man halt abends noch so in der Lage ist …) kann mit dem eigentlichen Input ohne Weiteres selbst Rechner mit großzügiger RAM-Ausstattung in Kürze zu Fall bringen. Leute, die den offensichtlichen Ansatz versuchen in's Messer laufen zu lassen, ist das Eine – solche Aufgaben gibt es ja jedes Jahr – aber das fand ich heute schon fast böswillig … :cry: (Die naive Lösung ließ sich zum Glück trotzdem leicht umstricken auf eine, die auch mit den tatsächlichen Daten klar kommt)
Benutzeravatar
ThomasL
User
Beiträge: 1366
Registriert: Montag 14. Mai 2018, 14:44
Wohnort: Kreis Unna NRW

Moin Moin, bin spät dran und konnte heute abend nur Tag 4 nach holen.

Code: Alles auswählen

import re

points = 0
cards = []
for t1, t2 in [line.strip().split('| ') for line in open('input.txt')]:
    win = list(map(int, re.findall(r'\d+', t1)))
    id, win = win[0], set(win[1:])
    have = set(map(int, re.findall(r'\d+', t2)))
    same = win & have
    # Part 1
    if same:
        points += 2**(len(same) - 1)
    # Part 2
    cards.append([id-1, len(same)])

print(f'Part 1: {points}')

amount = {id:1 for id in range(len(cards))}
for id, num in cards:
    for i in range(id+1, id+1+num):
        amount[i] += amount[id]

print(f'Part 2: {sum(amount.values())}')
Ich bin Pazifist und greife niemanden an, auch nicht mit Worten.
Für alle meine Code Beispiele gilt: "There is always a better way."
https://projecteuler.net/profile/Brotherluii.png
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Programmlänge ist ja nicht nur das was am Ende da ist, sondern auch was man zwischendurch durch was anderes ersetzt hat. Teil 1 heute war noch relativ einfach. Dann ist erst einmal Zeit dabei drauf gegangen den vorhandenen Code von Teil 1 so umzuschreiben, dass er immer noch für Teil 1 funktioniert. Denn Teil 1 ist ja im Grund ein Sonderfall von Teil 2 — Zahlenbereiche die eine Länge von 1 haben. Nach dem dann der ganze Code von einzelnen Zahlen auf Bereiche umgeschrieben war und mit den Werten aus Teil 1 funktionierte, lief Teil 2 beim ersten Versuch korrekt durch. 🤯

Was sich auch noch beim umschreiben änderte, war die Darstellung von Bereichen und der Abbildung weg von den Start- + Länge-Werten aus den Eingabedaten hin zu Start und Ende, weil sich damit IMHO einfacher arbeiten liess.

Dieses mal sind deutlich mehr Doctests drin, also auch welche die nicht direkt aus dem Aufgabentext stammen. Zum einen zum Testen und zum anderen damit man den Code leichter nachvollziehen kann, wenn man mal Teile davon in Aktion sieht.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Manul
User
Beiträge: 53
Registriert: Samstag 13. Februar 2021, 16:00

nezzcarth hat geschrieben: Dienstag 5. Dezember 2023, 21:20 Leute, die den offensichtlichen Ansatz versuchen in's Messer laufen zu lassen, ist das Eine – solche Aufgaben gibt es ja jedes Jahr – aber das fand ich heute schon fast böswillig … :cry:
Habe ich ähnlich empfunden, zumal ich die naive Lösung, u.a. wegen der mehrstufigen Transformation, auch schon relativ aufwendig empfunden habe.
__blackjack__ hat geschrieben: Dienstag 5. Dezember 2023, 22:36 [...] Teil 1 ist ja im Grund ein Sonderfall von Teil 2 — Zahlenbereiche die eine Länge von 1 haben. Nach dem dann der ganze Code von einzelnen Zahlen auf Bereiche umgeschrieben war und mit den Werten aus Teil 1 funktionierte, lief Teil 2 beim ersten Versuch korrekt durch. 🤯

Was sich auch noch beim umschreiben änderte, war die Darstellung von Bereichen und der Abbildung weg von den Start- + Länge-Werten aus den Eingabedaten hin zu Start und Ende, weil sich damit IMHO einfacher arbeiten liess.
Witzigerweise ging mir beides genauso. ;)
__blackjack__ hat geschrieben: Dienstag 5. Dezember 2023, 22:36 Dieses mal sind deutlich mehr Doctests drin, also auch welche die nicht direkt aus dem Aufgabentext stammen. Zum einen zum Testen und zum anderen damit man den Code leichter nachvollziehen kann, wenn man mal Teile davon in Aktion sieht.
Da wäre ich noch mal interessiert, Deinen Code zu sehen: Mir fehlt da die Übung und ich mache die Tests zwischendurch meistens interaktiv von Hand, was doch eine Menge Zeit kostet.
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Tag 6 liess sich ja einfach „brute forcen“. Teil 1 problemlos und bei Teil 2 rechnet mein nicht mehr so aktueller Büro-Rechner ein paar Sekunden. Aber das wird sicher nicht auf dem VIC-20 funktionieren, also muss ich da wohl noch mal ein bisschen Nachdenken was man da machen kann.

Folgendes kleines BASIC-Programm druckt was aus (auf Epson kompatiblen 9-Nadel-Druckern (IBM-Proprinter sollte auch gehen)), was beim Nachdenken helfen kann:

Code: Alles auswählen

   10 OPEN 1,4:E$=CHR$(27)
   20 PRINT#1,E$;"W1AOC 2023, DAY 6 - WAIT FOR IT";E$;"W0"
   30 FOR I=1 TO 3
   40 READ T,RD
   50 PRINT T;RD
   60 PRINT#1
   70 PRINT#1,"RACE TIME:";T;"  RECORD DISTANCE:";RD
   80 FOR N=0 TO T
   90 T$="   "+STR$(N)
  100 PRINT#1,MID$(T$,LEN(T$)-3);" ";
  110 D=N*(T-N)
  120 PRINT N;"/";T;"=";D
  130 IF D=0 THEN 200
  140 B$=CHR$(255)
  150 PRINT#1,E$;"K";CHR$(D);CHR$(0);
  160 FOR J=1 TO D
  170 IF J>=RD THEN B$=CHR$(85)
  180 PRINT#1,B$;
  190 NEXT
  200 PRINT#1,D
  210 NEXT
  220 NEXT
  230 CLOSE 1
  900 DATA 7,9
  910 DATA 15,40
  920 DATA 30,200
Bild
@Manul Hier ist Tag 5 in Python mit Doctests:

Code: Alles auswählen

#!/usr/bin/env python3
import sys

from attr import attrib, attrs
from more_itertools import chunked, split_at
from parse import parse

EXAMPLE_LINES = """\
seeds: 79 14 55 13

seed-to-soil map:
50 98 2
52 50 48

soil-to-fertilizer map:
0 15 37
37 52 2
39 0 15

fertilizer-to-water map:
49 53 8
0 11 42
42 0 7
57 7 4

water-to-light map:
88 18 7
18 25 70

light-to-temperature map:
45 77 23
81 45 19
68 64 13

temperature-to-humidity map:
0 69 1
1 0 69

humidity-to-location map:
60 56 37
56 93 4
""".splitlines()


@attrs(frozen=True)
class Range:
    """
    >>> Range(2)
    Range(start=2, end=2)
    >>> r = Range(23, 42)
    >>> r
    Range(start=23, end=42)
    >>> len(r)
    20

    The end points are included (unlike the built-in `range()`):

    >>> 3 in r
    False
    >>> 23 in r
    True
    >>> 42 in r
    True
    >>> 43 in r
    False
    """

    start = attrib()
    end = attrib()

    @end.default
    def _end_factory(self):
        return self.start

    def __len__(self):
        return self.end - self.start + 1

    def __contains__(self, number):
        return self.start <= number <= self.end

    @classmethod
    def from_start_and_length(cls, start, length):
        """
        >>> Range.from_start_and_length(79, 14)
        Range(start=79, end=92)
        """
        return cls(start, start + length - 1)

 2023
@attrs(frozen=True)
class RangeMap:
    """
    >>> map_ = RangeMap.from_starts_and_length(3, 7, 11)
    >>> map_
    RangeMap(source=Range(start=3, end=13), destination=Range(start=7, end=17))

    Check if an input number can be mapped by this `RangeMap`:

    >>> 0 in map_, 3 in map_, 13 in map_, 14 in map_
    (False, True, True, False)

    Numbers outside the source range are mapped to `None`:

    >>> map_[0], map_[3], map_[13], map_[14]
    (None, 7, 17, None)
    """

    source = attrib()
    destination = attrib()

    def __attrs_post_init__(self):
        if len(self.source) != len(self.destination):
            raise ValueError(
                f"{self.source!r} and {self.destination!r}"
                f" must have the same length"
                f" ({len(self.source)} != {len(self.destination)})"
            )

    def __contains__(self, number):
        return number in self.source

    def __getitem__(self, number):
        if number in self:
            return number - self.source.start + self.destination.start

        return None

    def map(self, range_):
        """
        Map a given `Range` to a tuple of mapped ranges and remaining unmapped
        parts of the given `Range`.

        >>> map_ = RangeMap.from_starts_and_length(5, 105, 11)
        >>> map_
        RangeMap(source=Range(start=5, end=15), destination=Range(start=105, end=115))

        Mapping a range that is outside of this `RangeMap` returns no mapped
        ranges and the original range:

        >>> map_.map(Range(0, 4))
        ([], [Range(start=0, end=4)])

        Mapping ranges that are complete within this `RangeMap` leave no
        remaining ranges:

        >>> map_.map(Range(7, 13))
        ([Range(start=107, end=113)], [])
        >>> map_.map(Range(5, 15))
        ([Range(start=105, end=115)], [])

        Mapping a range that overlaps the start *or* beginning return the mapped
        overlapping part and the unmapped remaining part:

        >>> map_.map(Range(3, 7))
        ([Range(start=105, end=107)], [Range(start=3, end=4)])
        >>> map_.map(Range(10, 20))
        ([Range(start=110, end=115)], [Range(start=16, end=20)])

        Finally mapping a range that encloses the `RangeMap` returns the mapped
        middle part, and the remaining unmapped parts before and after:

        >>> map_.map(Range(0, 20))
        ([Range(start=105, end=115)], [Range(start=0, end=4), Range(start=16, end=20)])
        """
        start = self[range_.start]
        end = self[range_.end]
        if start is None and end is None:
            if (
                range_.start < self.source.start
                and range_.end > self.source.end
            ):
                return (
                    [self.destination],
                    [
                        Range(range_.start, self.source.start - 1),
                        Range(self.source.end + 1, range_.end),
                    ],
                )
            else:
                return ([], [range_])
        elif start is None:
            return (
                [Range(self.destination.start, end)],
                [Range(range_.start, self.source.start - 1)],
            )
        elif end is None:
            return (
                [Range(start, self.destination.end)],
                [Range(self.source.end + 1, range_.end)],
            )
        else:
            return ([Range(start, end)], [])

    @classmethod
    def from_starts_and_length(cls, source_start, destination_start, length):
        return cls(
            Range.from_start_and_length(source_start, length),
            Range.from_start_and_length(destination_start, length),
        )

    @classmethod
    def parse(cls, text):
        """
        >>> RangeMap.parse("10 20 7")
        RangeMap(source=Range(start=20, end=26), destination=Range(start=10, end=16))
        """
        destination_start, source_start, length = map(int, text.split())
        return cls.from_starts_and_length(
            source_start, destination_start, length
        )


@attrs(frozen=True)
class Map:
    """
    >>> map_ = Map("seed",
    ...            "soil",
    ...            [RangeMap.from_starts_and_length(98, 50, 2),
    ...             RangeMap.from_starts_and_length(50, 52, 48)])
    >>> ranges = list(map(Range, [79, 14, 55, 13]))
    >>> [[range_.start for range_ in map_[source_range]]
    ...  for source_range in ranges]
    [[81], [14], [57], [13]]

    >>> map_[Range(79)]
    [Range(start=81, end=81)]
    >>> map_[Range.from_start_and_length(79, 14)]
    [Range(start=81, end=94)]
    """

    #
    # The two names are not really needed but make debugging easier and adding a
    # rountrip back to source at least possible.
    #
    source_name = attrib()
    destination_name = attrib()
    range_maps = attrib()

    def __getitem__(self, range_):
        result = []
        sources = [range_]
        for range_map in self.range_maps:
            next_sources = []
            for source_range in sources:
                mapped_ranges, remaining_ranges = range_map.map(source_range)
                result.extend(mapped_ranges)
                next_sources.extend(remaining_ranges)
            sources = next_sources
            if not sources:
                break

        result.extend(sources)
        return result

    @classmethod
    def parse_lines(cls, lines):
        lines = iter(lines)
        source_name, destination_name = parse("{}-to-{} map:", next(lines))
        return cls(
            source_name, destination_name, list(map(RangeMap.parse, lines))
        )


@attrs(frozen=True)
class Almanach:
    seed_ranges = attrib()
    maps = attrib()

    def get_revised(self):
        """
        >>> almanach = Almanach.parse_lines(EXAMPLE_LINES).get_revised()
        >>> almanach.seed_ranges
        [Range(start=79, end=92), Range(start=55, end=67)]
        >>> almanach.get_min_location()
        46
        """
        if not all(len(range_) == 1 for range_ in self.seed_ranges):
            raise ValueError("can't be revised")

        return Almanach(
            [
                Range.from_start_and_length(start.start, end.start)
                for start, end in chunked(self.seed_ranges, 2, strict=True)
            ],
            self.maps,
        )

    def get_location_ranges(self, seed_range):
        """
        >>> almanach = Almanach.parse_lines(EXAMPLE_LINES)

        >>> almanach.get_location_ranges(Range(79))
        [Range(start=82, end=82)]

        >>> almanach.get_location_ranges(Range(79, 92))
        [Range(start=60, end=60), Range(start=46, end=55), Range(start=82, end=84)]
        """
        ranges = [seed_range]
        for map_ in self.maps:
            next_ranges = []
            for range_ in ranges:
                next_ranges.extend(map_[range_])
            ranges = next_ranges

        return ranges

    def iter_location_ranges(self):
        """
        >>> almanach = Almanach.parse_lines(EXAMPLE_LINES)

        >>> ranges = list(almanach.iter_location_ranges())
        >>> all(len(range_) == 1 for range_ in ranges)
        True
        >>> [range_.start for range_ in ranges]
        [82, 43, 86, 35]

        >>> from attr import astuple
        >>> list(map(astuple, almanach.get_revised().iter_location_ranges()))
        [(60, 60), (46, 55), (82, 84), (86, 89), (94, 96), (56, 59), (97, 98)]
        """
        for ranges in map(self.get_location_ranges, self.seed_ranges):
            yield from ranges

    def get_min_location(self):
        """
        >>> almanach = Almanach.parse_lines(EXAMPLE_LINES)
        >>> almanach.get_min_location()
        35
        >>> almanach.get_revised().get_min_location()
        46
        """
        return min(self.iter_location_ranges()).start

    @classmethod
    def parse_lines(cls, lines):
        lines = (line.rstrip() for line in lines)
        line_blocks = split_at(lines, lambda line: line == "")
        seed_ranges = [
            Range(int(text))
            for text in parse("seeds: {}", next(line_blocks)[0])[0].split()
        ]
        return cls(seed_ranges, list(map(Map.parse_lines, line_blocks)))


def main():
    almanach = Almanach.parse_lines(sys.stdin)
    print(almanach.get_min_location())
    print(almanach.get_revised().get_min_location())


if __name__ == "__main__":
    main()
Zum Grossteil einfach die Werte aus der Beispielbeschreibung überprüft. Kann man entweder mit dem `doctest`-Modul aus der Standardbibliothek überprüfen, oder aber ``pytest`` mit der entsprechenden Option aufrufen: Ersteres ist im Erfolgsfall sehr unspektakulär — es gibt einfach nix aus. Bei ``pytest`` gibt es mehr Ausgaben:

Code: Alles auswählen

$ python3 -m doctest day06.py
$ python3 -m pytest --doctest-modules -v
============================= test session starts ==============================
platform linux -- Python 3.8.0, pytest-7.2.0, pluggy-1.0.0 -- /usr/bin/python3.8
cachedir: .pytest_cache
rootdir: /home/bj/work/advent_of_code_2023/05
collected 12 items                                                             

day05.py::day05.Almanach.get_location_ranges PASSED                      [  8%]
day05.py::day05.Almanach.get_min_location PASSED                         [ 16%]
day05.py::day05.Almanach.get_revised PASSED                              [ 25%]
day05.py::day05.Almanach.iter_location_ranges PASSED                     [ 33%]
day05.py::day05.Map PASSED                                               [ 41%]
day05.py::day05.Range PASSED                                             [ 50%]
day05.py::day05.Range.from_start_and_length PASSED                       [ 58%]
day05.py::day05.RangeMap PASSED                                          [ 66%]
day05.py::day05.RangeMap.map PASSED                                      [ 75%]
day05.py::day05.RangeMap.parse PASSED                                    [ 83%]
day05_old.py::day05_old.Almanach.iter_location_numbers PASSED            [ 91%]
day05_old.py::day05_old.Map PASSED                                       [100%]

============================== 12 passed in 0.05s ==============================
Im Terminal ist die ``pytest``-Ausgabe auch schön bunt, damit man in diesem Fall schön viele grüne PASSED sieht, und im Fehlerfall die roten FAILED schnell findet.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

So, nach dem ich Tag 5 gelesen hatte, dachte ich mir das ich jetzt keinen Urlaubsantrag wegen eines Rätsels stellen kann.
Heute hatte ich etwas Zeit und habe mich noch mal mit Tag 3 beschäftigt. Nach dem ich eure Lösungen ja schon gesehen habe, habe ich bewusst an meinem Ansatz festgehalten. Ich sah nicht viel Sinn eure Klassen nach zu programmieren (Abschreiben wie in der Schule, ist für ein Hobby recht doof), ich muss mir diese Ansätze eher merken um beim nächsten mal von selbst gleich auf die Idee zu kommen.

Naja zumindest den ersten Teil konnte ich lösen. Wie ich mit dieser Struktur Teil 2 löse, hat bei mir so viel Knoten im Kopf verursacht, das ich entschieden habe, das so zu lassen. Falls es jetzt so sein sollte, das alle weiteren Rätsel zu schwer und/oder aufwendig für mich werden, dann kann ich den Code immer noch in eine Struktur bringen um Teil 2 zu lösen. Oder ich finde noch die Änderung die ich machen muss um mit der vorhanden Struktur den Teil 2 zu lösen.

Code: Alles auswählen

import contextlib
import re
from pathlib import Path


INPUT = Path("/home/dennis/AoC/2023/Day3/input.txt")

EXAMPLE_LINES = """\
467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598..""".splitlines()


def parse_input(lines):
    line_to_parts = {number: [] for number in range(len(lines))}
    line_to_symbols = {number: [] for number in range(len(lines))}
    for line_number, line in enumerate(lines):
        for match in re.finditer(r"\d+", line):
            line_to_parts[line_number].append(
                {
                    "start": int(match.start()),
                    "end": int(match.end()),
                    "number": int(match.group(0)),
                }
            )
        for match in re.finditer(r"[^\d.\n]", line):
            line_to_symbols[line_number].append(int(match.start()))
    return line_to_parts, line_to_symbols


def looking_for_symbol(line_to_parts, line_to_symbols):
    found_parts = []
    for line, positions in line_to_parts.items():
        for position_details in positions:
            for x in range(position_details["start"] - 1, position_details["end"] + 1):
                for delta in [-1, 0, 1]:
                    with contextlib.suppress(KeyError):
                        if x in line_to_symbols[line + delta]:
                            found_parts.append(position_details["number"])
    return found_parts


# TODO Part 2 is missing
def main():
    engine_parts = list(INPUT.read_text(encoding="UTF-8").splitlines())
    # engine_parts = EXAMPLE_LINES
    line_to_parts, line_to_symbols = parse_input(engine_parts)
    print(sum(looking_for_symbol(line_to_parts, line_to_symbols)))


if __name__ == "__main__":
    main()
Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
nezzcarth
User
Beiträge: 1636
Registriert: Samstag 16. April 2011, 12:47

__blackjack__ hat geschrieben: Mittwoch 6. Dezember 2023, 13:58 Tag 6 liess sich ja einfach „brute forcen“. Teil 1 problemlos und bei Teil 2 rechnet mein nicht mehr so aktueller Büro-Rechner ein paar Sekunden.
Per Bruteforce dauerte es bei mir für beide Teile um die 11.5 Sekunden. Mit etwas Rückbesinnung auf den Matheunterricht (ca. 9/10. Klasse) ließ sich die Laufzeit des Bruteforce-Ansatzes auf ~ 4.5 Sekunden senken.
Manul
User
Beiträge: 53
Registriert: Samstag 13. Februar 2021, 16:00

__blackjack__ hat geschrieben: Mittwoch 6. Dezember 2023, 13:58 @Manul Hier ist Tag 5 in Python mit Doctests:
[...]
Zum Grossteil einfach die Werte aus der Beispielbeschreibung überprüft. Kann man entweder mit dem `doctest`-Modul aus der Standardbibliothek überprüfen, oder aber ``pytest`` mit der entsprechenden Option aufrufen: Ersteres ist im Erfolgsfall sehr unspektakulär — es gibt einfach nix aus. Bei ``pytest`` gibt es mehr Ausgaben:
[...]
Super, vielen Dank dafür! Das muss ich mir mal in Ruhe anschauen, sieht auf jeden Fall interessant aus und hatte ich so noch nicht auf dem Schirm.
nezzcarth hat geschrieben: Mittwoch 6. Dezember 2023, 22:45
__blackjack__ hat geschrieben: Mittwoch 6. Dezember 2023, 13:58 Tag 6 liess sich ja einfach „brute forcen“. Teil 1 problemlos und bei Teil 2 rechnet mein nicht mehr so aktueller Büro-Rechner ein paar Sekunden.
Per Bruteforce dauerte bei mir knapp an die 12 Sekunden. Mit etwas Rückbesinnung auf den Matheunterricht (ca. 9/10. Klasse) ließ es sich noch mal deutlich senken.
Aus welchen Gründen auch immer hatte ich die Erinnerung an die Schulmathematik schon bei Teil 1 und habe den brute force-Ansatz gar nicht erst ausprobiert.

Tag 5 habe ich so gelöst:

Code: Alles auswählen

import fileinput
import re

from more_itertools import chunked
from collections import deque

MAP_HEADER = re.compile("(\w+)-to-(\w+) map:")

class Collection(deque):
  """
  represents a collection of items

  contents are tuples, each tuple presents an interval of item numbers by its first and last elements

  Attributes
  ----------
  kind : category of items this represents

  Methods
  -------
  normalize : sort and consolidate any overlapping intervals
  """

  def __init__(self, kind, *args, from_integers = None, from_intervals = None, **kwargs):
    super().__init__(*args, **kwargs)
    self.kind = kind
    if from_integers:
      self.extend((i, i) for i in from_integers)
    elif from_intervals:
      self.extend((start, start + length - 1) for start, length in chunked(from_intervals, 2))
    self.normalize()

  def __repr__(self):
    return "Collection of {}s, {} intervals: {}".format(self.kind, len(self), tuple(self))

  def normalize(self):
    original = deque(sorted(self))
    self.clear()

    while original:
      start, end = original.popleft()
      while original and end >= original[0][0]:
        end = max(end, original.popleft()[1])
      self.append((start, end))


class MappingRange:
  def __init__(self, line):
    (
      target,
      self.start,
      length
    ) = map(int, line.split())
    self.end = self.start + length - 1
    self.offset = target - self.start

  def __repr__(self):
    return "MappingRange: {}-{} -> {}-{}".format(
      self.start,
      self.end,
      self.start + self.offset,
      self.end + self.offset
    )

  def __contains__(self, other):
    return self.start <= other[1] and self.end >= other[0]

  def __getitem__(self, collection):
    unhandled = Collection(collection.kind)
    processed = []

    for interval in collection:
      if interval in self:
        start, end = interval
        if start < self.start:
          unhandled.append((start, self.start - 1))
          start = self.start
        if end > self.end:
          unhandled.append((self.end + 1, end))
          end = self.end
        processed.append((start + self.offset, end + self.offset))
      else:
        unhandled.append(interval)

    return unhandled, processed

class Mapping:
  def __init__(self, source, target):
    self.source = source
    self.target = target
    self.ranges = []

  def __add__(self, other):
    if isinstance(other, MappingRange):
      self.ranges.append(other)
      return self
    raise ValueError

  def __getitem__(self, collection):
    result = Collection(self.target)
    unhandled = collection

    for r in self.ranges:
      unhandled, processed = r[unhandled]
      result.extend(processed)

    result.extend(unhandled)
    result.normalize()

    return result


class Ruleset:
  def __init__(self):
    self.mappings = []

  def __getitem__(self, key):
    for mapping in self.mappings:
      if mapping.source == key:
        return mapping
    raise IndexError

  def __contains__(self, other):
    return any(mapping.source == other for mapping in self.mappings)

  def __add__(self, other):
    if isinstance(other, Mapping):
      self.mappings.append(other)
      return self
    raise ValueError

  def process(self, collection):
    assert collection.kind in self

    return self[collection.kind][collection]


class State:
  def __init__(self, seeds, ruleset):
    self.ruleset = ruleset
    self.collections = [ seeds ]

  def evolve(self):
    while self.collections[-1].kind in self.ruleset:
      self.collections.append(self.ruleset.process(self.collections[-1]))


def read_state():
  ruleset = Ruleset()

  seeds, *mappings = "".join(fileinput.input()).strip().split("\n\n")

  seeds = list(map(int, seeds.split(": ")[1].split()))

  for m in mappings:
    header, *ranges = m.split("\n")

    source, target = MAP_HEADER.match(header).groups()

    mapping = Mapping(source, target)

    for r in ranges:
      mapping += MappingRange(r)

    ruleset += mapping

  return seeds, ruleset


def main():
  seeds, ruleset = read_state()

  # part 1
  state = State(Collection("seed", from_integers = seeds), ruleset)
  state.evolve()

  print(min(state.collections[-1])[0])

  # part 2
  state = State(Collection("seed", from_intervals = seeds), ruleset)
  state.evolve()

  print(min(state.collections[-1])[0])

if __name__ == "__main__":
  main()
Etwas unsicher bin ich mir, ob die __init__-Methode der Klasse Collection mit den zwei optionalen Keyword-Argumenten eine gute Idee ist. Das Überladen von __add__ in Mapping und Ruleset halte ich im Nachhinein eher für eine Schnapsidee, lasse es jetzt aber mal so stehen. Das Verarbeiten per __getitem__ in MappingRange, Mapping und Ruleset hat dagegen keine Seiteneffekte, das finde ich nach wie vor ganz charmant.

Mein Tag 6 sieht so aus:

Code: Alles auswählen

import fileinput
import math

from functools import reduce
from operator import mul

def number_of_ways(T, r):
  """
  t : time button is pressed
  T : length of race
  r : current record

  traveled distance is

  d = (T - t) * t

  solving d = r for t yields

  t**2 -Tt + r = 0

  hence

  t = T/2 +/- sqrt(T**2/4 -r)
  """
  root_term = math.sqrt(T**2 / 4 - r)

  # we need to beat the record, not just achieve it, hence the +/- 1
  min_time_to_beat = math.floor(T / 2 - root_term) + 1
  max_time_to_beat = math.ceil(T / 2 + root_term) - 1

  return max_time_to_beat - min_time_to_beat + 1

def main():

  times, records = (list(map(int, line.split()[1:])) for line in fileinput.input())

  # part 1
  possibilities = [ number_of_ways(T, r) for T, r in zip(times, records) ]

  print(reduce(mul, possibilities))

  # part 2
  T = int("".join(str(t) for t in times))
  r = int("".join(str(d) for d in records))

  print(number_of_ways(T, r))

if __name__ == "__main__":
  main()
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Manul: Die `__repr__()`-Implementierungen sind falsch. Also ”soft”-falsch, weil sie nicht den Konventionen entsprechen. Die sind entweder eine Zeichenkette die als Ausdruck ausgewertet wieder einen gleichen Wert ergeben, oder eine zur Fehlersuche nützliche Darstellung die in ”spitze Klammern” eingefasst ist, also "<…>", wobei in der Regel als erstes der Typname da drin steht und irgend etwas was dieses Objekt eindeutig identifizierbar macht. Also minimal das was man bekommt wenn man `__repr__()` nicht überschreibt, sondern einfach das von `object` erbt:

Code: Alles auswählen

In [271]: object()
Out[271]: <object at 0x7f6abbb854f0>

In [272]: class A: pass

In [273]: A()
Out[273]: <__main__.A at 0x7f6abe78b250>
Das was Du da bei `__repr__()` machst ist eher was für die `__str__()`-Methode, oder es fehlen einfach die "<"/">" vorne und hinten.

Das mit den beiden optionalen Schlüsselwort-Argumenten finde ich auch nicht so schön. Für alternative Konstruktoren gibt es ja `classmethod()`. Ich verlagere in solche Klassenmethoden eigentlich alles was ein bisschen Arbeit macht, damit die `__init__()` so ”dumm” wie möglich bleibt und sich auf validieren von Werten und setzen von Attributen beschränkt. Wenn man die `__init__()` Arbeit erledigen lässt, muss man für das Erstellen von Exemplaren für Tests beispielsweise immer vorher die ”gegenteilige” Arbeit verrichten. Ebenso wenn man eine `__repr__()` schreiben möchte die einen Ausdruck als Zeichenkette liefert der wieder ein wertegleiches Objekt liefert.

Diese Aufteilung wird auch vom `attr`-Modul gefördert, was ich ja gerne benutze um mir Boilerplate-Code zu sparen. Zum Beispiel eine `__repr__()` zu schreiben.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Manul
User
Beiträge: 53
Registriert: Samstag 13. Februar 2021, 16:00

__blackjack__ hat geschrieben: Donnerstag 7. Dezember 2023, 01:01 @Manul: Die `__repr__()`-Implementierungen sind falsch. Also ”soft”-falsch, weil sie nicht den Konventionen entsprechen.
[...]
Das was Du da bei `__repr__()` machst ist eher was für die `__str__()`-Methode, oder es fehlen einfach die "<"/">" vorne und hinten.
Danke für den Hinweis! Versuche ich in Zukunft zu beherzigen.
__blackjack__ hat geschrieben: Donnerstag 7. Dezember 2023, 01:01 Das mit den beiden optionalen Schlüsselwort-Argumenten finde ich auch nicht so schön. Für alternative Konstruktoren gibt es ja `classmethod()`. Ich verlagere in solche Klassenmethoden eigentlich alles was ein bisschen Arbeit macht, damit die `__init__()` so ”dumm” wie möglich bleibt und sich auf validieren von Werten und setzen von Attributen beschränkt. Wenn man die `__init__()` Arbeit erledigen lässt, muss man für das Erstellen von Exemplaren für Tests beispielsweise immer vorher die ”gegenteilige” Arbeit verrichten. Ebenso wenn man eine `__repr__()` schreiben möchte die einen Ausdruck als Zeichenkette liefert der wieder ein wertegleiches Objekt liefert.
`classmethod()` wäre vermutlich wirklich die Methode der Wahl gewesen. Die beiden letzten Sätze kann ich in diesem konkreten Fall zwar nicht nachvollziehen, prinzipiell leuchten sie mir aber auch ein.
__blackjack__ hat geschrieben: Donnerstag 7. Dezember 2023, 01:01 Diese Aufteilung wird auch vom `attr`-Modul gefördert, was ich ja gerne benutze um mir Boilerplate-Code zu sparen. Zum Beispiel eine `__repr__()` zu schreiben.
Das steht auch noch auf meiner Liste von Dingen, die ich mir mal in Ruhe anschauen will, wenn ich mal gaaaaanz viel Zeit habe. ;)

Meine Lösung zu Tag 7, inklusive meiner ersten doctest-Strings und einer hoffentlich besseren `__repr__()`-Implementierung:

Code: Alles auswählen

import fileinput

from collections import Counter
from itertools import combinations_with_replacement

# value of a card = CARDS.index(card)
CARDS = "23456789TJQKA"
CARDS_WITH_JOKERS = "J23456789TQKA"

JOKER = "J"

class Hand(str):
  J_is_Joker = False

  def __new__(cls, src = None, bid = 0, *args, **kwargs):
    self = super().__new__(cls, src, *args, **kwargs)

    self.bid = int(bid)

    if len(self) != 5 or any(card not in CARDS for card in self):
      raise ValueError

    return self

  def __repr__(self):
    return "Hand({}, bid = {})".format(super().__repr__(), self.bid)

  def __lt__(self, other):
    return self.signature < other.signature

  @property
  def kind(self):
    """
    map hand to its type represented by ordered numbers of unique cards

    provides natural sorting since lists are sorted lexically:

    >>> [1, 1, 1, 1, 1] < [2, 1, 1, 1] < [2, 2, 1] < [3, 1, 1] < [3, 2] < [4, 1] < [5]
    True
    >>> Hand("AAAAA").kind
    [5]
    >>> Hand("AAAKK").kind
    [3, 2]
    >>> Hand("23456").kind
    [1, 1, 1, 1, 1]
    """

    if Hand.J_is_Joker and JOKER in self:
      # given the scoring system, it's always the best strategy to replace both jokers with the same card
      # combinations = [ list(combo) for combo in combinations_with_replacement(CARDS_WITH_JOKERS[1:], self.count(JOKER)) ]
      # candidates = [ Hand("".join([combo.pop() if card == JOKER else card for card in self])) for combo in combinations ]
      stripped = Counter(self.replace(JOKER, ""))
      if stripped:
        replacement = stripped.most_common()[0][0]

        return Hand(self.replace(JOKER, replacement)).kind
      else:
        return Hand("AAAAA").kind
    else:
      return sorted(Counter(self).values(), reverse = True)

  @property
  def card_values(self):
    if Hand.J_is_Joker:
      base = CARDS_WITH_JOKERS
    else:
      base = CARDS
    return tuple(base.index(card) for card in self)

  @property
  def signature(self):
    return (self.kind, self.card_values)

def main():
  hands = [ Hand(*line.split()) for line in fileinput.input() ]

  # part 1
  print(sum(rank * hand.bid for rank, hand in enumerate(sorted(hands), 1)))

  # part 1
  Hand.J_is_Joker = True
  print(sum(rank * hand.bid for rank, hand in enumerate(sorted(hands), 1)))

if __name__ == "__main__":
  main()
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

Hier meine Lösung von Tag 7.2, basierend auf Teil 1, aber mit Joker Wert 1 und der zusätzlichen `_adjust_for_jokers()` Methode. Mit einer `Card` Klasse vielleicht etwas überstrukturiert, aber sei's drum. Und endlich mal eine Einsatzmöglichkeit für match-case:

Code: Alles auswählen

import collections


SYMBOL_VALUES = {"A": 14, "K": 13, "Q": 12, "J": 1, "T": 10}
QUINDECIM_BASE = 15
FNAME = "input.txt"


class Card:

    def __init__(self, symbol):
        self.symbol = symbol
        self.value = int(SYMBOL_VALUES.get(symbol, symbol))

    def __repr__(self):
        return f"Card({self.symbol})"


class Hand:

    def __init__(self, symbols, bid):
        self.symbols = symbols
        self.bid = int(bid)
        self.cards = [Card(symbol) for symbol in symbols]
        self._value = None
        self._kind = None

    @property
    def value(self):
        if self._value is None:
            self._value = sum(
                card.value * QUINDECIM_BASE ** i
                for i, card in enumerate(reversed(self.cards))
            )
        return self._value

    @property
    def kind(self):
        if self._kind is None:
            c = collections.Counter(self.symbols)
            v = sorted(c.values(), reverse=True)
            match v:
                case [5]:  # cheat poker
                    value = 6
                case [4, 1]:  # poker
                    value = 5
                case [3, 2]:  # full house
                    value = 4
                case [3, 1, 1]:  # triple
                    value = 3
                case [2, 2, 1]:  # two pairs
                    value = 2
                case [2, 1, 1, 1]:  # single pair
                    value = 1
                case _:
                    value = 0
            self._kind = self._adjust_for_jokers(value)
        return self._kind

    def _adjust_for_jokers(self, value):
        jokers = self.symbols.count("J")
        if jokers:
            if value % 2:
                value += 2
            else:
                if value <= 2:
                    value += 1
                if value > 2:
                    value += jokers
        return min(value, 6)

    def __lt__(self, other):
        if self.kind < other.kind:
            return True
        if self.kind > other.kind:
            return False
        return self.value < other.value

    def __str__(self):
        return f"symbols:{self.symbols}\nkind:{self.kind}\nbid:{self.bid}\nvalue:{self.value}\n"

class HandSet:

    def __init__(self):
        self.hands = []

    def __repr__(self):
        return "\n".join(str(hand) for hand in self.hands)

    def get_total_winnings(self):
        return sum(
            hand.bid * rank for rank, hand in
            enumerate(sorted(self.hands), 1)
        )

    def parse(self, fname):
        with open(fname) as fobj:
            for line in fobj:
                self.hands.append(Hand(*line.split()))


hand_set = HandSet()
hand_set.parse(FNAME)
print(hand_set.get_total_winnings())
Antworten