• # Les données imposent la méthode

    Posté par (Mastodon) . En réponse au message Advent of Code 2023, jour 20. Évalué à 3.

    Ici aussi, l'énoncé ne suffit pas à résoudre le problème, les données imposent la méthode.

    Il faut constater qu'à l'instar de l'exemple 2 avec output, le rx de sortie n'existe pas dans les données en entrée, et qu'il est uniquement relié à une boîte à conjonction, l'équivalent du inv du second exemple.
    rx recevra un low pulse ssi inv reçoit des high pulses de partout à la fois.

    Et là on lance des simulations (ou on analyse les données en entrée ?), pour voir quand notre inv reçoit des high pulses, on va constater en environ 13000 itérations qu'on a vu passer 3 high pulses de chacun des 4 sources, et que chacun de ces évènement cycle.

    Chez moi :

    • 3797 HP depuis la 1ère entrée ;
    • 3847 HP depuis la 3è entrée ;
    • 3877 HP depuis la 4è entrée ;
    • 4051 HP depuis la 2è entrée.

    Et ça cycle, donc 1ère entrée en 3797, 7594, 11391, 2è entrée en 4051, 8102, 12153, 3è entrée en 3847, 7694, 11541, 4è entrée en 3877, 7754, 11631.
    4 cycles, et qui commencent tous à 0 en plus, heureux hasard, n'est-ce pas ?

    En analysant les données on doit réaliser qu'on a 4 parcours indépendants, depuis chacune des 4 entrées de notre broadcaster, qui mènent chacun à une entrée de notre boîte inv, donc les cycles sont bien indépendants.
    Par contre, deviner qu'ils commencent à zéro ne me semble pas si évident, même si tout les FlipFlop commencent à off, et les Conjoncteurs à low partout, pourquoi une fois un low reçu à l'arrivée est-ce qu'on retomberait sur l'état initial ?

    Bon, bah le résultat c'est de multiplier les longueurs de cycles entre eux.
    - Parce qu'on a des cycles indépendants.
    - Et qu'ils commencent tous à zéro.

    Le résultat est dans les 256 billions (proche de 40004 ), donc on lance pas la force brute, quelle que soit la qualité de notre modélisation.

    Côté code j'ai pas lourd à montrer, peut-être ma modélisation pour le 1, mignonne, que j'ai un peu pensée en amont pour pouvoir détecter des cycles, donc avec des frozen dataclass. Ça tourne pas trop mal avec 1 millions de cycles en 15 secondes et une RAM constante à 80Mo (c'est PyPy, ya un overhead de RAM de 70Mo, c'est comme ça).
    Le problème réel (4051 cycles) prend moins d'une seconde en CPython, avec 10Mo de RAM.
    L'état des Conjunctions est un binaire avec les bits à 0 ou 1 selon le dernier pulse reçu de chaque entrée, donc les entrées sont ordonnées. Et bien sûr, partout, low c'est 1 ou True, et high c'est 0 ou False, ça simplifie de le prendre comme ça !
    L'état est dans boxes qui ne contient que des frozen dataclasses, donc en pratique quand une boîte change d'état, elle retourne une nouvelle boîte dans le nouvel état, je ne modifie jamais une boîte, ce n'est pas possible, j'en crée des nouvelles selon la situation.
    Cette façon de faire permettrait de comparer deux états global du système.
    On n'a pas à faire ça, donc probablement qu'il vaudrait mieux coder des boîtes dynamiques à états.
    de toute façon le code final est un mélange de trucs fixes et de trucs qui bougent magiquement avec des variables globales, on a vu plus propre (je pense aux statistiques pour le premier problème)...

    data = stdin.read().strip().splitlines()
    FINAL = "ns" # manuellement extrait de mes données
    from dataclasses import dataclass
    from collections import deque
    from functools import cached_property
    messages = deque()
    stats = [0, 0]
    # Modules
    class Module:
     def send(self, src, low): # low if True, high if False
     for dst in self.destination:
     stats[not low] += 1
     messages.append((src, dst, low))
    @dataclass(frozen=True)
    class Broadcaster(Module):
     destination: tuple = tuple()
     def __call__(self, src, name, low):
     self.send(name, low)
     return self
    @dataclass(frozen=True)
    class FlipFlop(Module):
     on: bool = False
     destination: tuple = tuple()
     def __call__(self, src, name, low):
     if not low:
     return self
     self.send(name, self.on) # High if off, low if on
     return FlipFlop(not self.on, self.destination)
    @dataclass(frozen=True)
    class Conjunction(Module):
     src: tuple = tuple()
     destination: tuple = tuple()
     state: int = 0
     def addsrc(self, *src):
     return Conjunction(self.src + src, self.destination, self.state + 2**len(self.src))
     @cached_property
     def full(self):
     return 2 ** len(self.src) - 1 # low pulse for each
     def __call__(self, src, name, low):
     state = self.full if self.state == -1 else self.state
     bit = 2 ** self.src.index(src)
     if low:
     state |= bit
     else:
     state &= self.full - bit
     self.send(name, state == 0)
     return Conjunction(self.src, self.destination, state)
    # data modelisation
    boxes = {}
    conjunctions = set()
    for line in data:
     name, _, *dst = line.replace(",", "").split()
     if name[0] == "b":
     boxes[name] = Broadcaster(tuple(dst))
     elif name[0] == "%":
     boxes[name[1:]] = FlipFlop(False, tuple(dst))
     else:
     boxes[name[1:]] = Conjunction(destination=tuple(dst))
     conjunctions.add(name[1:]) # storing all conjunction names
    # Updating src for conjunctions
    for name, box in boxes.items():
     i = set(box.destination).intersection(conjunctions)
     if i:
     for c in i:
     boxes[c] = boxes[c].addsrc(name)
    # Red Button, DO NOT PUSH !
    button = Broadcaster(("broadcaster",))
    def push():
     button.send("button", True)
     cstate = 0
     while messages:
     src, dst, low = messages.popleft()
     if dst in boxes:
     boxes[dst] = boxes[dst](src, dst, low)
     if dst == FINAL and not low:
     cstate = boxes[FINAL].state
     return cstate
    # Cycling...
    cycles = {}
    for _ in range(5000**4):
     if _ == 1000:
     stats1000 = tuple(stats)
     state = push()
     if state and state not in cycles:
     cycles[state] = _ + 1
     if len(cycles) == len(boxes[FINAL].src):
     break
    # Results
    def mul(i):
     return reduce(lambda x, y: x * y, i)
    ex1 = mul(stats1000)
    ex2 = mul(cycles.values())

    Le coût fixe de PyPy le rend inutile sur le problème réel, je monte de 0,55s à 0,65s, par contre si je cycle 1 million de fois, ça passe de 2min10 à 15s.
    Ça doit être pour ça que je n'ai pas trouvé PyPy si pertinent cette année : l'an dernier je devais être un gros bourrin, cette année je suis tout en finesse (......).

    Bon, c'était mignon, mais toujours un poil agaçant quand l'énoncé ne suffit pas à avoir la résolution, et qu'il faut compter sur des particularités des données.
    Mais bon, si on nous expliquait dès le début qu'il y avait 4 cycles et qu'il fallait les coordonner, l'exercice serait assez trivial...

    • Yth.