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 :
fromsysimportstdin,argvfromfunctoolsimportcacheMUL=int(argv[1]iflen(argv)>1else1)data=stdin.read().strip().splitlines()classSpring:def__init__(self,puzzle,clues,mul=1):self.clues=[int(_)foriinrange(mul)for_inclues.split(",")]self.str="?".join(puzzlefor_inrange(mul))self.size=len(self.str)self.puzzle=[{"?":0,".":1,"#":-1}.get(_)for_in(self.str)]def__str__(self):returnf"Spring: {self.str}{self.clues}"defrun(self):r=self._run(0,-1,0,0)returnr@cachedefclue_max_pos(self,n):returnself.size-sum(self.clues[n:])-len(self.clues[n+1:])@cachedef_run(self,pos,clue,clue_id,force_value=0):ifclue<0:ifpos>self.clue_max_pos(clue_id):return0# Not enough space remainingelse:ifpos>self.clue_max_pos(clue_id+1)-clue:return0# Not enough space remainingifpos==len(self.puzzle):return1# That path is working, yay!value=force_valueorself.puzzle[pos]ifvalue==-1:# Damagedifclue<0:# We are starting to consumate a new clueifclue_id>=len(self.clues):# None is available, wrong pathreturn0clue=self.clues[clue_id]ifclue:# We consumate an active cluereturnself._run(pos+1,clue-1,clue_id)else:# the clue was zero, the path is wrongreturn0elifvalue==1:# functional, current clue must be <= 0ifclue>0:# wrong pathreturn0# If we just finished a clue, preparing for the next one# clue will now be < 0 until starting the next cluereturnself._run(pos+1,-1,clue_id+(clue==0))else:# unknownifclue>0:# this *must* be a damaged onereturnself._run(pos,clue,clue_id,-1)elifclue==0:# this must be a proper onereturnself._run(pos,clue,clue_id,1)else:# trying both possibilitiesreturn(self._run(pos,clue,clue_id,1)+self._run(pos,clue,clue_id,-1))springs=[Spring(*line.split(),MUL)forlineindata]print(sum(s.run()forsinsprings))
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.
[^] # Re: Rien de vraiment compliqué, il faut juste utiliser tout ce qu'on sait faire.
Posté par Yth (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 :
Ç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 :
.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
@cachesur 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 -= 1donc 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 :
Côté RAM, avec
MUL = 10on 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.