• [^] # Re: Rien de vraiment compliqué, il faut juste utiliser tout ce qu'on sait faire.

    Posté par (Mastodon) . En réponse au message Advent of Code 2023, jour 12. Évalué à 3. Dernière modification le 12 décembre 2023 à 14:07.

    Et donc voici ma solution, en python, et là encore, de façon surprenante, PyPy est quasiment équivalent à CPython, malgré les gros gros calculs !

    Juste pour info, avec un coefficient de pliage à 10, et une réponse de 739 944 601 532 013 104 445 095 536 (740 millions de milliards de milliards), mon programme final sort la réponse en 5 secondes.
    L'exercice 2 normal prend 2 secondes.

    On oublie les regexp, on va simplement faire un parcours récursif.
    On va de gauche à droite et on va consommer les indices, et brancher sur chaque ? qui n'a pas une valeur contrainte en considérant soit une source en bon état . soit une source endommagée #.
    Dès qu'on est au bout du chemin avec tous les indices consommés, on a trouvé une solution, on remonte donc 1. En cas d'impossibilité, on s'arrête et on remonte 0.

    Ça c'est malin.
    Ça ne suffit pas, il faut être rusé, et optimiser les conditions d'arrêt, sinon on peut avoir un résultat faux déjà, et puis explorer des trucs assez loin pour réaliser au bout que c'est pas bon, parce qu'il nous reste des indices non exploités.
    Donc calculer l'espace minimal nécessaire à placer les indices non utilisés, et si on dépasse, on sait qu'on va dans le mur, on s'arrête tout de suite.

    Ça fait gagner du temps, mais fichtre, pas encore assez, ça turbine, ça turbine, j'ai envisagé de sortir la grosse machine, 4 cœurs, 1 quart du programme chacun, mais non, déjà avec un facteur de 4 ça va être long, à 5 c'est mort.

    Alors ruser encore plus ?
    Et si à chaque branchement :

    • on teste avec une source en bon état.
    • si on a des solutions, alors on sait que l'indice suivant pouvait être décalé d'un cran vers la gauche, donc on peut multiplier par deux !
    • sinon on teste le chemin avec une source endommagée.

    Ça va beaucoup plus vite, beaucoup beaucoup.
    Mais c'est faux, il va falloir encore plus d'intelligence, de ruse, pour comprendre les effets de bords, pourquoi ça ne fonctionne pas.
    J'ai plus de cerveau, je suis fatigué, ça ne va pas fonctionner...

    Allez, encore un effort, il faut une idée !

    Et là c'est l'évidence, ma fonction récursive est mauvaise mais elle peut être bonne, j'ai fait en sorte de transmettre uniquement le strict nécessaire pour passer à l'étape d'après :

    • La position actuelle dans le parcours du plan ;
    • ce qu'il me reste à consommer dans l'indice actuel, cette valeur est négative si je suis entre deux indices ;
    • le numéro de l'indice en cours de consommation ;
    • la gueule du puzzle en l'état pour le débuggage ;
    • la valeur de remplacement pour une source inconnue (lors d'un branchement j'appelle à l'identique avec . puis #, ça remplace le ? des données initiales).

    Le calcul de ce qui reste à parcourir ne dépend pas de ce qui s'est passé avant, mais uniquement de ces paramètres : (position, reste de l'indice actuel, numéro de l'indice actuel, valeur de remplacement).
    On vire le puzzle, et on dégage tout le débuggage, de toute façon on sait que notre algo fonctionne.

    Et là, on utilise @cache sur notre fonction récursive.

    Et voilà.

    Une dernière ruse lors de la rédaction de ce message pour réaliser que le reste à consommer peut être optimisé en étant toujours à -1 comme valeur négative, je faisais un x -= 1 donc la valeur pouvait être à -2, -3 etc, mais ça n'a pas de valeur autre que : je ne suis pas en train de parcourir un indice.
    Ça fait passer de 5 à 2 secondes, et divise la RAM consommée par 4.

    Voici le code :

    from sys import stdin, argv
    from functools import cache
    MUL = int(argv[1] if len(argv) > 1 else 1)
    data = stdin.read().strip().splitlines()
    class Spring:
     def __init__(self, puzzle, clues, mul=1):
     self.clues = [int(_) for i in range(mul) for _ in clues.split(",")]
     self.str = "?".join(puzzle for _ in range(mul))
     self.size = len(self.str)
     self.puzzle = [
     {"?": 0, ".": 1, "#": -1}.get(_)
     for _ in (self.str)
     ]
     def __str__(self):
     return f"Spring: {self.str}{self.clues}"
     def run(self):
     r = self._run(0, -1, 0, 0)
     return r
     @cache
     def clue_max_pos(self, n):
     return self.size - sum(self.clues[n:]) - len(self.clues[n + 1:])
     @cache
     def _run(self, pos, clue, clue_id, force_value=0):
     if clue < 0:
     if pos > self.clue_max_pos(clue_id):
     return 0 # Not enough space remaining
     else:
     if pos > self.clue_max_pos(clue_id + 1) - clue:
     return 0 # Not enough space remaining
     if pos == len(self.puzzle):
     return 1 # That path is working, yay!
     value = force_value or self.puzzle[pos]
     if value == -1: # Damaged
     if clue < 0: # We are starting to consumate a new clue
     if clue_id >= len(self.clues): # None is available, wrong path
     return 0
     clue = self.clues[clue_id]
     if clue: # We consumate an active clue
     return self._run(pos + 1, clue - 1, clue_id)
     else: # the clue was zero, the path is wrong
     return 0
     elif value == 1: # functional, current clue must be <= 0
     if clue > 0: # wrong path
     return 0
     # If we just finished a clue, preparing for the next one
     # clue will now be < 0 until starting the next clue
     return self._run(pos + 1, -1, clue_id + (clue == 0))
     else: # unknown
     if clue > 0: # this *must* be a damaged one
     return self._run(pos, clue, clue_id, -1)
     elif clue == 0: # this must be a proper one
     return self._run(pos, clue, clue_id, 1)
     else: # trying both possibilities
     return (
     self._run(pos, clue, clue_id, 1)
     + self._run(pos, clue, clue_id, -1)
     )
    springs = [Spring(*line.split(), MUL) for line in data]
    print(sum(s.run() for s in springs))

    Côté RAM, avec MUL = 10 on monte à 300Mo, c'est 120Mo pour le problème réel, et PyPy consomme plus de RAM que CPython (800Mo et 270Mo).
    Bref, les 120Mo pour le problème à résoudre sont assez raisonnables, on est loin du OOM.

    • Yth.