Pour déterminer la position de départ donc, la première idée consiste à chercher l'intersection de deux trajectoires corrigées. Une intersection de droites donc. Sauf que c'est assez moche à calculer, on peut faire plus élégant.
Les trajectoires corrigées étant toutes sécantes au même point, celui qu'on recherche, en les prenant deux par deux on peut définir des plans. Il est temps d'ouvrir une petite parenthèse.
Plan défini par deux droites
Dans l'espace, deux droites peuvent définir un plan, à condition nécessaire et suffisante qu'elles soient soit parallèles mais non identiques, soit sécantes.
Soient deux droites pertinentes selon cette condition et définies chacune par un point et par un vecteur (p0, v0) et (p1, v1). On cherche à caractériser leur plan commun, par un point et un vecteur normal.
Si elles ne sont pas parallèles, leurs vecteurs directeurs v0 et v1 ne sont pas colinéaires. Leur produit vectoriel n = v0 ^ v1 est non nul et orthogonal aux deux droites et fait donc un très bon vecteur normal pour le plan cherché.
Si elles sont parallèles leurs vecteurs directeurs sont certes colinéraires, mais p0 - p1 n'est pas colinéaire à ces derniers (sinon, vous pouvez vérifier, les droites seraient identiques). On peut donc choisir n = v0 ^ (p0 - p1) comme vecteur normal pour le plan cherché.
Il nous reste à trouver un point sur le plan cherché : c'est trivial, il suffit de prendre par exemple p0 puisque ce plan contient par définition la première droite. Pour obtenir une équation de plan du type n ⋅ r = d, il suffit de prendre d = p0 ⋅ n.
En fait, nous nous intéressons seulement au cas de droites sécantes, voici donc le calcul pour obtenir l'équation du plan dans ce cas spécifique :
n = v0 ^ v1
d = p0 ⋅ n
n ⋅ r = d
Retour au problème
En prenant les trajectoires corrigées deux par deux, on peut définir un tas de plans contenant tous le point cherché. Or l'intersection de trois plans non parallèles est un point, qui sera forcément ce dernier ! Ouvrons une nouvelle parenthèse.
Intersection de plans
Allons-y pour l'intersection de trois plan non parallèles (p0, d0), (p1, d1) et (p2, d2).
Le point d'intersection, que nous allons noter r, respecte les équation des trois plans :
n0 ⋅ r = d0
n1 ⋅ r = d1
n2 ⋅ r = d2
L'astuce consiste ici à choisir une base vectorielle alternative :
u0 = n1 ^ n2
u1 = n2 ^ n0
u2 = n0 ^ n1
En exprimant la position cherchée comme combinaison de ces trois vecteurs r = a0 u 0 + a1 u1 + a2 u2, l'équation du premier plan devient :
On peut reconnaître ici le produit mixte de nos trois vecteurs normaux, un scalaire que l'on va noter U = n0 ⋅ (n1 ^ n2). Les trois équations deviennent de la même façon :
a0 U = d0
a1 U = d1
a2 U = d2
Ce qui nous donne finalement ces fameux coefficients :
a0 = d0 / U
a1 = d1 / U
a2 = d2 / U
Qui nous donnent donc le vecteur position de l'intersection de nos trois plans. Pour rappel, voici les calculs à effectuer à partir des constantes définissant les trois plans :
u0 = n1 ^ n2
u1 = n2 ^ n0
u2 = n0 ^ n1
U = n0 ⋅ (n1 ^ n2)
a0 = d0 / U
a1 = d1 / U
a2 = d2 / U
r = a0 u0 + a1 u1 + a2 u2
Re-retour au problème
En prenant trois trajectoires corrigées (p0, v0 - v), (p1, v1 - v) et (p2, v2 - v) , on sait désormais caractériser leurs plans communs deux à deux, soit trois plan. Et on sait également déterminer l'intersection de ces trois plan. Problème résolu !
Je passe sur la première partie, trouver des intersections de ligne dans le plan c'est sans grand intérêt.
La seconde partie est beaucoup, beaucoup plus intéressante. J'ai vainement cherché une solution élégante avant d'en trouver une sur Reddit. Je vous l'explique.
À supposer que l'on trouve une position de départ p et une vitesse v qui permette de percuter tous les grêlons, en se plaçant dans le référentiel de notre projectile, ce sont tous les grêlons qui vont converger jusqu'à l'atteindre, chacun à son tour. Autrement dit, pour un grêlon (p0, v0), la droite (p0, v0 - v) passe par notre position de départ p. Et il en est de même pour les autres grêlons.
Par conséquent, les trajectoires corrigées des trois premiers grêlons (p0, v0 -v), (p1, v1 -v) et (p2, v2 -v) se coupent deux à deux en p. Il est temps d'ouvrir une parenthèse sur les droites sécantes.
Des droites sécantes
Dans l'espace, contrairement au plan, des droites peuvent être identiques, parallèles, sécantes ou… rien de tout ça.
Étant données deux droites caractérisées chacune par un point et un vecteur directeur l0 = (p0, v0) et l1 = (p1, v1), la première chose à vérifier est qu'elles ne sont ni identiques ni parallèles. Autrement dit, que leurs vecteurs directeurs ne sont pas colinéaires. Une façon de faire consiste à prendre leur produit vectoriel, qui ne doit pas être nul.
Si ces droites ne sont pas parallèles, les droites vectorielles associées (O, v0) et (O, v1), définissent un plan vectoriel. Ce plan vectoriel peut être caractérisé par un vecteur normal facile à construire par le produit vectoriel des vecteurs directeurs de ces deux droites : n = v0 ^ v1.
Le plan affine parallèle à ce plan vectoriel et incluant d0 a pour équation vectorielle r ⋅ n = p0 ⋅ n. Le plan affine incluant d1 a quant a lui pour équation vectorielle r ⋅ n = p1 ⋅ n.
Ces plans sont identiques si et seulement si p0 ⋅ n = p1 ⋅ n. Autrement dit, en revenant à la définition de ce vecteur normal n, si (p0 - p1) ⋅ (v0 ^ v1) = 0. Si ces plans sont identiques, les droites sont coplanaire, et n'étant pas parallèles, elles sont donc sécantes. Réciproquement, si les droites sont coplanaires, les plans sont identiques.
Retour au problème
Puisque le problème a une solution, les trajectoires corrigées de deux grêlons (p0, v0 - v) et (p1, v1 - v) sont sécantes. Bon, ok, à condition qu'elles ne soient pas identiques : si c'était le cas on prendrait juste une autre grêlon, on en a plein à notre disposition. Mais vous pouvez vérifier sur vos données, ce cas ne se présente pas. Par conséquent :
Pour résoudre ça simplement, il y a une astuce. On va utiliser une base vectorielle ainsi définie, avec des vecteurs conçus que chacun d'entre eux soit orthogonal à deux vecteurs des équations précédentes :
u0 = A1 ^ A2
u1 = A2 ^ A0
u2 = A0 ^ A1
Et écrire la vitesse que nous cherchons sur cette base : v = a0 u0 + a1 u1 + a2 u2. Les équations précédentes deviennent désormais :
La vitesse, c'est bien, mais c'est surtout la position de départ qu'on nous demande. On va dire que c'est facile à déduire désormais : c'est l'intersection des trajectoires corrigées des deux premiers grêlons. Par exemple, on peut en prendre d'autres si on veut.
Sauf que non, ce n'est vraiment pas trivial à calculer, l'intersection de deux droites sécantes dans l'espace. Il y a encore une astuce, et celle-là est vraiment de moi. Je vous la donnerai plus tard, là je suis fatigué de raconter mes aventures mathématiques.
Et vous, quels fromages utilisez-vous pour vos pâtes, ou autres préparations, sous quel format et avec quel résultat, et pour quel impact sur les finances ?
Du fromage acheté chez un producteur à l'occasion de vacances à la montagne. Ou du fromage acheté à la fromagerie de mon quartier. Ou au marché. Mais en aucun cas du fromage industriel pré-râpé.
Bref, du fromage en morceau que je râpe moi-même… ou que je demande au fromager de râper et de mettre dans une gamelle qui m'appartient. De l'achat en vrac quoi.
À noter que les exemples fournis en première partie ne s'appliquaient pas à la deuxième partie, et que cette dernière ne fournissait aucun exemple utilisable. J'ai trouvé ça vache.
Bon, j'ai eu mon résultat en un peu moins d'une heure avec PyPy. Mais ça reste moche à mes yeux. À l'occasion, je m'essaierai à optimiser encore ça avec un découpage plus astucieux.
Ah, je crois que je devine la subtilité. En descendant la chaîne des workflows, ou plutôt la chaîne des règles et des workflows, on peut découper sur chaque test, ce qui fait au final beaucoup moins de pavés qu'en quadrillant l'espace entier.
Je ne suis pas sûr de comprendre, et ne lisant pas la Haskell, je me demande si tu peux m'éclairer. Est-ce un genre de dichotomie que tu fais ? Sur quel critère découpes-tu ou non un pavé de valeurs ?
La première partie ne pose pas de problème particulier. La seconde en revanche, c'est une autre paire de manches.
Mon idée pour optimiser cela consiste à réduire le nombre de cas que l'on teste. En effet, les workflows ont des règles basées sur des seuils, et fondamentalement, il suffit de balayer l'ensemble des combinaisons de ces valeurs seuils pour pouvoir déterminer ce qui arrivera à l'importe quelle combinaison entre ces valeurs.
Il faut faire attention à la façon dont on définit les seuils en question, parce que les définitions utilisent les opérateurs < et > qui ne sont pas l'opposé l'un de l'autre. Bref, il y a un détail important que je ne donnerai pas ici.
Toujours est-il que, pour mes données, ça me ramène à 6,4 × 10⁹ = 6,4 milliards de possibilités. Et c'est un peu long à tester : à un rythme de l'ordre de 150.000 combinaisons par seconde, ça va me prendre une douzaine d'heures. Il faut que je trouve mieux que ça…
Pour ce problème, j'ai considéré qu'on n'était pas vraiment en deux dimensions, mais plutôt en trois. Parce que l'état d'un creuset, ce n'est pas seulement sa position sur le terrain, mais aussi le nombre de cases qu'il a parcouru dans une direction donnée… ce qui se représente également très bien par un nombre, que je considère comme une troisième coordonnée.
Ça permet de se ramener à un problème de parcours de proche en proche, avec une définition bien particulière des voisins d'un point.
Voici le code :
fromcollections.abcimportIterable,Iterator,SequencefromtypingimportSelfimportnumpyasnpimportnumpy.typingasnptimportaocclassCity:def__init__(self,array:Sequence[Sequence[int]]):self.matrix:npt.NDArray[np.ubyte]=np.array(array,dtype=np.ubyte)self.ly,self.lx=self.matrix.shape@classmethoddefimport_lines(cls,lines:Iterable[str])->Self:array=[[int(char)forcharinlineifchar!='\n']forlineinlines]returncls(array)defwalk(self,minturn,maxturn)->int:# We will be using a matrix with three coordinates:# * z indicates how many steps were made in which direction:# - z < 0: starting, no previous steps!# - 0 ≤ z < maxturn: 1 to maxturn steps up ↑# - maxturn ≤ z < 2*maxturn: 1 to maxturn steps down ↓# - 2*maxturn ≤ z < 3*maxturn: 1 to maxturn steps left ←# - 3*maxturn ≤ z < 4*maxturn: 1 to maxturn steps right →visits:npt.NDArray[np.int_]=np.full((4*maxturn,self.ly,self.lx),-1,dtype=np.int_)def_neighs(z:int,y:int,x:int)->Iterator[tuple[int,int,int]]:"""Yield all directly accessible neighbors of a point, including points outside the limits of the city."""ifz<0:# Special case for startyield(0*maxturn,y-1,x)yield(1*maxturn,y+1,x)yield(2*maxturn,y,x-1)yield(3*maxturn,y,x+1)returndiv,mod=divmod(z,maxturn)ifdiv==0:ifmod<maxturn-1:yield(z+1,y-1,x)ifmod+1>=minturn:yield(2*maxturn,y,x-1)yield(3*maxturn,y,x+1)returnifdiv==1:ifmod<maxturn-1:yield(z+1,y+1,x)ifmod+1>=minturn:yield(2*maxturn,y,x-1)yield(3*maxturn,y,x+1)returnifdiv==2:ifmod+1>=minturn:yield(0*maxturn,y-1,x)yield(1*maxturn,y+1,x)ifmod<maxturn-1:yield(z+1,y,x-1)returnifdiv==3:ifmod+1>=minturn:yield(0*maxturn,y-1,x)yield(1*maxturn,y+1,x)ifmod<maxturn-1:yield(z+1,y,x+1)returndefneighs(z:int,y:int,x:int)->Iterator[tuple[int,int,int]]:forz_,y_,x_in_neighs(z,y,x):if0<=x_<self.lxand0<=y_<self.ly:yieldz_,y_,x_currents:dict[tuple[int,int,int],int]={(-1,0,0):0}whilelen(currents)>0:nexts:dict[tuple[int,int,int],int]={}forcurrent_pos,current_totalincurrents.items():fornext_posinneighs(*current_pos):z,y,x=next_posloss=self.matrix[y,x]next_total=current_total+lossif(visits[next_pos]<0ornext_total<visits[next_pos]):visits[next_pos]=next_totalnexts[next_pos]=next_totalcurrents=nextsloss=-1fordivinrange(4):formodinrange(minturn-1,maxturn):z=div*maxturn+modpos=(z,self.ly-1,self.lx-1)ifloss<0or0<visits[pos]<loss:loss=visits[pos]returnlossdefpart1(lines:aoc.Data)->int:"""Solve puzzle part 1: determine the minimum total heat loss from start to end, using normal crucibles"""city=City.import_lines(lines)returncity.walk(1,3)defpart2(lines:aoc.Data)->int:"""Solve puzzle part 2: determine the minimum total heat loss from start to end, using ultra crucibles"""city=City.import_lines(lines)returncity.walk(4,10)
importdataclassesimportenumimportioimportrefromcollections.abcimportIterable,IteratorfromtypingimportClassVar,Selfimportnumpyasnpimportnumpy.typingasnptimportaocCoords=tuple[int,int]Vector=tuple[int,int]classDirection(enum.Enum):UP='U'DOWN='D'LEFT='L'RIGHT='R'@propertydefvector(self)->Vector:ifselfisself.UP:return(-1,0)ifselfisself.DOWN:return(1,0)ifselfisself.LEFT:return(0,-1)ifselfisself.RIGHT:return(0,1)assertFalse,"we covered all cases"deftranslate(self,position:Coords)->Coords:y,x=positiondy,dx=self.vectorreturny+dy,x+dx@dataclasses.dataclass(frozen=True)classInstruction:direction:Directionlength:intimport_pattern1:ClassVar[re.Pattern]=re.compile(r'^([UDLR]) (\d+) \(#[0-9A-Fa-f]{6}\)\n?$')import_pattern2:ClassVar[re.Pattern]=re.compile(r'^[UDLR] \d+ \(#([0-9A-Fa-f]{5})([0-9A-Fa-f])\)\n?$')@classmethoddefimport_line1(cls,line:str)->Self:if(m:=cls.import_pattern1.match(line))isnotNone:returncls(Direction(m[1]),int(m[2]))raiseValueError("invalid instruction line")@classmethoddefimport_line2(cls,line:str)->Self:if(m:=cls.import_pattern2.match(line))isnotNone:length=int(m[1],16)ifm[2]=='0':direction=Direction.RIGHTelifm[2]=='1':direction=Direction.DOWNelifm[2]=='2':direction=Direction.LEFTelifm[2]=='3':direction=Direction.UPelse:raiseValueError("invalid instruction line")returncls(direction,length)raiseValueError("invalid instruction line")defapply(self,position:Coords)->Coords:y,x=positiondy,dx=self.direction.vectorreturny+self.length*dy,x+self.length*dxclassTerrain1:def__init__(self,instructions:Iterable[Instruction])->None:position:Coords=(0,0)excavated:set[Coords]={(0,0)}forinstructionininstructions:for_inrange(instruction.length):position=instruction.direction.translate(position)excavated.add(position)ymin,ymax=0,0xmin,xmax=0,0for(y,x)inexcavated:ymin=min(ymin,y)ymax=max(ymax,y)xmin=min(xmin,x)xmax=max(xmax,x)# Time to write a terrain matrix# It needs to span the entire excavated area, plus a margin:# * vertically: from ymin - 1 to ymax + 1 included;# * horizontally: fron xmin - 1 to xmax + 1 included.self.matrix:npt.NDArray[np.bool_]=np.zeros((ymax-ymin+3,xmax-xmin+3),dtype=np.bool_)self.ly,self.lx=self.matrix.shapefor(y,x)inexcavated:y=y-ymin+1x=x-xmin+1self.matrix[y,x]=Truedefdig_pool(self)->None:defneighs(coords:Coords)->Iterator[Coords]:def_neighs(coords:Coords):y,x=coordsyieldy,x-1yieldy,x+1yieldy-1,xyieldy+1,xfory,xin_neighs(coords):if0<=y<self.lyand0<=x<self.lx:yieldy,xoutside=np.zeros_like(self.matrix)visited=np.zeros_like(self.matrix)start=(0,0)outside[start]=Truevisited[start]=Truecurrents:set[Coords]={(0,0)}whilelen(currents)>0:nexts:set[Coords]=set()forpositionincurrents:forneighinneighs(position):ifvisited[neigh]:continueifnotself.matrix[neigh]:outside[neigh]=Truenexts.add(neigh)visited[neigh]=Truecurrents=nextsforposition,_innp.ndenumerate(self.matrix):# type: ignoreifnotoutside[position]:self.matrix[position]=Truedefarea(self)->int:returnnp.sum(self.matrix)# type: ignoredef__str__(self)->str:s=io.StringIO()forlineinself.matrix:forexcavatedinline:ifexcavated:s.write('█')else:s.write(' ')s.write('\n')returns.getvalue()@dataclasses.dataclass(frozen=True)classHSegment:x1:intx2:inty:intdefcuts(self,x:int)->bool:returnself.x1<=x<=self.x2def__len__(self)->int:returnself.x2-self.x1+1@dataclasses.dataclass(frozen=True)classVSegment:x:inty1:inty2:intdefcuts(self,y:int)->bool:returnself.y1<=y<=self.y2def__len__(self)->int:returnself.y2-self.y1+1classTerrain2:def__init__(self,instructions:Iterable[Instruction]):hsegments:set[HSegment]=set()vsegments:set[VSegment]=set()ys:set[int]={0,1}xs:set[int]={0,1}ymin,ymax=0,1xmin,xmax=0,1y,x=0,0forinstructionininstructions:y_,x_=instruction.apply((y,x))ymin,ymax=min(ymin,y_),max(ymax,y_+1)xmin,xmax=min(xmin,x_),max(xmax,x_+1)ys.add(y_)ys.add(y_+1)xs.add(x_)xs.add(x_+1)ifinstruction.directionisDirection.UP:vsegments.add(VSegment(x,y_,y))elifinstruction.directionisDirection.DOWN:vsegments.add(VSegment(x,y,y_))elifinstruction.directionisDirection.LEFT:hsegments.add(HSegment(x_,x,y))elifinstruction.directionisDirection.RIGHT:hsegments.add(HSegment(x,x_,y))else:assertFalse,"we covered all cases for instruction direction"y,x=y_,x_ys.add(ymin-1)ys.add(ymax+1)xs.add(xmin-1)xs.add(xmax+1)self.ys=sorted(ys)self.xs=sorted(xs)self.matrix:npt.NDArray[np.bool_]=np.zeros((len(ys)-1,len(xs)-1),dtype=np.bool_)self.lj,self.li=self.matrix.shapeforhsegmentinhsegments:j=self.ys.index(hsegment.y)foriinrange(self.xs.index(hsegment.x1),self.xs.index(hsegment.x2)+1):self.matrix[j,i]=Trueforvsegmentinvsegments:i=self.xs.index(vsegment.x)forjinrange(self.ys.index(vsegment.y1),self.ys.index(vsegment.y2)+1):self.matrix[j,i]=Truedefdig_pool(self)->None:defneighs(coords:Coords)->Iterator[Coords]:def_neighs(coords:Coords):j,i=coordsyieldj,i-1yieldj,i+1yieldj-1,iyieldj+1,iforj,iin_neighs(coords):if0<=j<self.ljand0<=i<self.li:yieldj,ioutside=np.zeros_like(self.matrix)visited=np.zeros_like(self.matrix)start=(0,0)outside[start]=Truevisited[start]=Truecurrents:set[Coords]={(0,0)}whilelen(currents)>0:nexts:set[Coords]=set()forpositionincurrents:forneighinneighs(position):ifvisited[neigh]:continueifnotself.matrix[neigh]:outside[neigh]=Truenexts.add(neigh)visited[neigh]=Truecurrents=nextsforposition,_innp.ndenumerate(self.matrix):# type: ignoreifnotoutside[position]:self.matrix[position]=Truedefarea(self)->int:count=0forjinrange(self.lj):foriinrange(self.li):ifself.matrix[j,i]:count+=((self.ys[j+1]-self.ys[j])*(self.xs[i+1]-self.xs[i]))returncountdef__str__(self)->str:s=io.StringIO()forlineinself.matrix:forexcavatedinline:ifexcavated:s.write('█')else:s.write(' ')s.write('\n')returns.getvalue()defpart1(lines:aoc.Data)->int:"""Solve puzzle part 1: determine the sum of stuff"""terrain=Terrain1(Instruction.import_line1(line)forlineinlines)terrain.dig_pool()returnterrain.area()defpart2(lines:aoc.Data)->int:"""Solve puzzle part 2: determine the sum of staff"""terrain=Terrain2(Instruction.import_line2(line)forlineinlines)terrain.dig_pool()returnterrain.area()
Pour la première partie, je fais du remplissage, voici les étapes :
J'alloue une matrice suffisante pour contenir tout le tracé – oui, il y a une étape pour déterminer la taille nécessaire, mais c'est sans intérêt à dérailler –, plus une marge d'une unité. En fait, pas une matrice mais trois, à valeurs booléennes : le terrain, une matrice qui indiquera si on est à l'extérieur du tracé, et une matrice qui indiquera si on a déjà visité chaque point.
Je trace les tranchées.
Je pars du point en haut à gauche, dont je suis certain qu'il est hors du bassin puisqu'il fait partie de la marge sus-mentionnée. De proche en proche, je remplis l'extérieur du bassin dans la seconde de mes matrices. C'est beaucoup plus facile que d'essayer de remplir directement l'intérieur. La troisième matrice sert à cette étape, pour ne pas passer indéfiniment sur les mêmes points.
Je remplis l'intérieur du bassin dans la matrice du terrain, la seule que je vais garder.
Trouver l'aire, c'est trivial.
Deuxième partie
Pareil. Comment ça, pareil, alors que les coordonnées sont beaucoup trop grandes pour pouvoir faire ça avec des matrices ? Eh bien, on n'a pas besoin de toutes les coordonnées, voyez-vous. On a seulement besoin de celles où commencent ou finissent des tranchées en fait.
Bref, je me construis une matrice donc les cases ont des dimensions virtuelles carrément variables. Ensuite, on se ramène à l'algorithme précédent, avec un détail de pondération pour le calcul de l'aire.
En effet. Mais c'est moins que 100×100 pour chaque galet, tout simplement parce que peu importe la position de chaque galet, ils se ressemblent tous au point d'être interchangeables. C'est plutôt la combinaison de 2.000 parmi 10.000 qui majore le nombre de possibilités. Ça fait quand même vachement beaucoup trop.
D'accord, j'ai été un peu rapide dans mon interprétation d'hier, on avait simplement focalisé la lumière du soleil vers le chambre de fusion.
Oui, justement, j'ai comme l'impression que ce n'est pas parce qu'on a remis en service la production de lave que les forges de l'île du métal vont se remettre en marche toutes seules et que les lutins arriveront à produire des pièces de rechange sans aide. Et même alors quelque chose me dit que même avec des pièces de rechange toutes neuves, les lutins de l'île du désert vont avoir besoin de notre aide pour réparer leurs machines et pour collecter du sable. Sur l'île de l'île, on risque aussi d'avoir besoin d'assistance pour relancer la filtration de l'eau et l'envoyer sur l'île de la neige. Quand à l'île de la neige, je pense que la machine à neige ne va pas recevoir directement cette eau, et que les lutins ont sûrement fini par oublier comment cette machine.
Si les lutins savaient se débrouiller tout seuls, ça se saurait depuis le temps. Et là, on vient de relancer un processus vachement de pure ingénierie lutinesque donc vachement complexe et instable. On n'est pas encore sortis de l'auberge.
Pourquoi utilisez-vous des dictionnaires plutôt qu'un tableau ? On a 256 boîtes, indexées par un entier de 0 à 255, ça semble naturel de représenter ça par un tableau de 256 boîtes non ?
Mon code :
fromenumimportEnumfromdataclassesimportdataclassimportioimportrefromtypingimportOptional,Selfimportaocdefhash_(s:bytes)->int:value=0forbins:value+=bvalue*=17value%=256returnvalue@dataclass(frozen=True)classLens:focal:intlabel:bytesdef__str__(self)->str:return'%s%d'%(self.label.decode(),self.focal)classOperation(Enum):REM='-'PUT='='classInstruction:def__init__(self,label:bytes,op:Operation,arg:Optional[int]=None):self.label=labelself.box=hash_(label)self.op=opself.arg=argpattern=re.compile('^(.+)([=-])(.*)$')@classmethoddefimport_word(cls,word:str)->Self:if(m:=cls.pattern.match(word))isnotNone:label=m[1].encode('ascii')op=Operation(m[2])arg=Noneif(s:=m[3])!='':arg=int(s)returncls(label,op,arg)raiseValueError('cannot parse instruction')classBox:def__init__(self)->None:self.lenses:list[Lens]=[]defremove(self,label:bytes)->None:fori,lensinenumerate(self.lenses):iflens.label==label:delself.lenses[i]returndefput(self,new_lens:Lens):fori,lensinenumerate(self.lenses):iflens.label==new_lens.label:self.lenses[i]=new_lensreturnself.lenses.append(new_lens)defis_empty(self)->bool:returnlen(self.lenses)==0def__str__(self)->str:return' '.join("[%s]"%str(lens)forlensinself.lenses)classMachine:def__init__(self)->None:self.boxes:tuple[Box]=tuple(Box()for_inrange(256))# type: ignoredef__str__(self)->str:s=io.StringIO()fori,boxinenumerate(self.boxes):ifnotbox.is_empty():s.write('Box %d: %s\n'%(i,str(box)))returns.getvalue()defapply(self,instruction:Instruction)->None:box=self.boxes[instruction.box]ifinstruction.opisOperation.REM:box.remove(instruction.label)returnifinstruction.opisOperation.PUTandinstruction.argisnotNone:lens=Lens(instruction.arg,instruction.label)box.put(lens)returnraiseValueError("invalid instruction")defpower(self)->int:total=0forbox_number,boxinenumerate(self.boxes):forlens_number,lensinenumerate(box.lenses):total+=(box_number+1)*(lens_number+1)*lens.focalreturntotaldefpart1(lines:aoc.Data)->int:"""Solve puzzle part 1: determine the sum of hash value of all instructions"""total=0forlineinlines:forpartinline.rstrip().split(','):h=hash_(part.encode('ascii'))total+=hash_(part.encode('ascii'))returntotaldefpart2(lines:aoc.Data)->int:"""Solve puzzle part 2: determine the sum of staff"""instructions=(Instruction.import_word(part)forlineinlinesforpartinline.rstrip().split(','))machine=Machine()forinstructionininstructions:machine.apply(instruction)returnmachine.power()
En voyant la partie 2, j'ai immédiatement pensé deux choses :
il faudrait que j'optimise un minimum ma fonction de cycle d'essorage, parce qu'on va l'appeler un certain nombre de fois, même si ce ne sera pas un milliard de fois ;
je vais finir par tomber sur un cycle de cycle en effet.
Ce dernier point est une certitude absolue. Pourquoi donc ? Parce qu'il y a 100 ligne et 100 colonnes, donc moins de 100 × 100 = 10.000 positions possibles des pierres qui roulent. En fait, bien moins que ça, parce que les positions occupées par les pierres qui ne roulent pas ne sont pas utilisables, et que seules les dispositions stables par le nord – on se comprend – sont admissibles.
Bref, en moins de 10.000 cycles je suis sûr d'avoir au moins deux fois la même disposition. Et retomber sur une disposition déjà vue, c'est aussi retomber sur la disposition suivante au cycle d'après, etc.
Chez moi ça cycle en 64 cycles.
Le code :
fromcollections.abcimportIterable,SequencefromtypingimportOptional,Selfimportenumimportioimportitertoolsimportnumpyasnpimportnumpy.typingasnptimportaocclassTile(enum.Enum):EMPTY='.'CUBE='#'ROUND='O'def__str__(self)->str:ifselfisself.EMPTY:return' 'ifselfisself.CUBE:return'■'ifselfisself.ROUND:return'○'assertFalseclassPlatform:def__init__(self,array:Sequence[Sequence[Tile]]):self.matrix:npt.NDArray[np.object_]=np.array(array)self.ly,self.lx=self.matrix.shapeself.spaces_horiz:list[list[range]]=[]self.spaces_vert:list[list[range]]=[]foryinrange(self.ly):self.spaces_horiz.append([])xs=[-1]+[xforxinrange(self.lx)ifself.matrix[y,x]isTile.CUBE]+[self.lx]forx1,x2initertools.pairwise(xs):ifx2-x1>1:self.spaces_horiz[-1].append(range(x1+1,x2))forxinrange(self.lx):self.spaces_vert.append([])ys=[-1]+[yforyinrange(self.ly)ifself.matrix[y,x]isTile.CUBE]+[self.ly]fory1,y2initertools.pairwise(ys):ify2-y1>1:self.spaces_vert[-1].append(range(y1+1,y2))@classmethoddefimport_lines(cls,lines:Iterable[str])->Self:array=[]forlineinlines:array.append([Tile(char)forcharinline.rstrip()])returncls(array)def__str__(self)->str:s=io.StringIO()forlineinself.matrix:fortileinline:s.write(str(tile))s.write('\n')returns.getvalue()defpositions(self):returntuple((y,x)for(y,x),valueinnp.ndenumerate(self.matrix)ifvalueisTile.ROUND)deftilt_north(self)->None:forxinrange(self.lx):column=self.matrix[:,x]forspaceinself.spaces_vert[x]:rounds=0foryinspace:ifcolumn[y]isTile.ROUND:rounds+=1column[y]=Tile.EMPTYforyinrange(space.start,space.start+rounds):column[y]=Tile.ROUNDdeftilt_south(self)->None:forxinrange(self.lx):column=self.matrix[:,x]forspaceinself.spaces_vert[x]:rounds=0foryinspace:ifcolumn[y]isTile.ROUND:rounds+=1column[y]=Tile.EMPTYforyinrange(space.stop-1,space.stop-1-rounds,-1):column[y]=Tile.ROUNDdeftilt_west(self)->None:foryinrange(self.ly):row=self.matrix[y]forspaceinself.spaces_horiz[y]:rounds=0forxinspace:ifrow[x]isTile.ROUND:rounds+=1row[x]=Tile.EMPTYforxinrange(space.start,space.start+rounds):row[x]=Tile.ROUNDdeftilt_east(self)->None:foryinrange(self.ly):row=self.matrix[y]forspaceinself.spaces_horiz[y]:rounds=0forxinspace:ifrow[x]isTile.ROUND:rounds+=1row[x]=Tile.EMPTYforxinrange(space.stop-1,space.stop-1-rounds,-1):row[x]=Tile.ROUNDdefcycle(self)->None:self.tilt_north()self.tilt_west()self.tilt_south()self.tilt_east()defload_north(self)->int:load=0for(y,x),tileinnp.ndenumerate(self.matrix):iftileisTile.ROUND:load+=self.ly-yreturnloaddefpart1(lines:aoc.Data)->int:"""Solve puzzle part 1: determine the sum of stuff"""platform=Platform.import_lines(lines)platform.tilt_north()returnplatform.load_north()defpart2(lines:aoc.Data)->int:"""Solve puzzle part 2: determine the sum of staff"""platform=Platform.import_lines(lines)position_cycles:dict[Tuple[Tuple[int]],int]={}target=1000000000first:Optional[int]=Noneforcycleinrange(platform.ly*platform.ly):positions=platform.positions()ifpositionsinposition_cycles:first=position_cycles[positions]breakposition_cycles[positions]=cycleplatform.cycle()else:raiseValueError("cannot find a cycle‽")assertfirstisnotNone# `first` is the number of a cycle when, /before/ cycling, the positions# were the same as now.# `cycle` is the number of current cycle, /before/ cycling.loop=cycle-firstremaining=target-firstremaining%=loopfor_inrange(remaining):platform.cycle()returnplatform.load_north()
Après des années à utiliser des pilotes spécifiques, les fabricants d'imprimantes de sont enfin mis ensemble pour concevoir une, enfin deux normes inspirées de CUPS. Une le la compatibilité avec les appareils Apple et une pour Android. CUPS prend en charge les deux.
Concrètement, ça veut dire que si tu prends une imprimante qui prend en charge AirPrint ou Mopria, ça marchera juste sans rien avoir à faire. Et c'est le cas de la grande majorité des imprimantes récentes, donc tu es assez libre dans ton choix.
Maintenant, si par hasard tu veux éviter de contribuer au modèle de financement par le consommable, avec des imprimantes vendues presque à perte et de l'encre vendue plus cher que le Chanel n°5, tu peux choisir une imprimante à réservoir d'encre. Ça coûte plus cher à l'achat, mais l'encre fournie avec correspond à un volume d'impression qui, en cartouches, coûterait déjà plus cher que le prix total de l'imprimante !
Question bête : n'existe-t-il pas des casses-têtes légaux ? Quand une compagnie vous vend un produit rendu volontairement défectueux, n'a-t-on donc aucune loi qui puisse la faire condamner ?
Ça s'appelle un vice caché. Dans le cas présent, ce serait reconnu sans aucune difficulté. Avec en prime une mauvaise foi patente de la part du fabricant. Bref, si c'était en France, la SNCF pourrait le contraindre à payer des dommages-intérêts suffisants pour les mettre en faillite.
fromcollections.abcimportIterablefromtypingimportOptional,SelffromenumimportEnumimportioimportaocCoords=tuple[int,int]classTerrain(Enum):ASH='.'ROCK='#'def__str__(self)->str:ifselfisself.ASH:return' 'ifselfisself.ROCK:return'▒'raiseValueError('invalid terrain value')classPattern:def__init__(self,array:Iterable[Iterable[Terrain]]):self.matrix=list(list(line)forlineinarray)self.ly=len(self.matrix)self.lx=len(self.matrix[0])@classmethoddefimport_lines(cls,lines:Iterable[str])->Self:returncls((Terrain(char)forcharinline.rstrip())forlineinlines)def__getitem__(self,coords:Coords)->Terrain:y,x=coordsreturnself.matrix[y][x]def__str__(self)->str:s=io.StringIO()forlineinself.matrix:forterraininline:s.write(str(terrain))s.write('\n')returns.getvalue()defy_reflection_errors(self,y:int)->int:"""Count the number of errors in a horizontal reflection between y - 1 and y. Return 0, 1 or 2 (meaning many)."""errors=0foriinrange(min(y,self.ly-y)):y1=y-1-iy2=y+iforxinrange(self.lx):errors+=self[y1,x]!=self[y2,x]iferrors>=2:returnerrorsreturnerrorsdefx_reflection_errors(self,x:int)->int:"""Count the number of errors in a vertical reflection between x - 1 and x. Return 0, 1 or 2 (meaning many)."""errors=0foriinrange(min(x,self.lx-x)):x1=x-1-ix2=x+iforyinrange(self.ly):errors+=self[y,x1]!=self[y,x2]iferrors>=2:returnerrorsreturnerrorsdefy_reflection1(self)->Optional[int]:foryinrange(1,self.ly):ifself.y_reflection_errors(y)==0:returnyreturnNonedefx_reflection1(self)->Optional[int]:forxinrange(1,self.lx):ifself.x_reflection_errors(x)==0:returnxreturnNonedefy_reflection2(self)->Optional[int]:foryinrange(1,self.ly):ifself.y_reflection_errors(y)==1:returnyreturnNonedefx_reflection2(self)->Optional[int]:forxinrange(1,self.lx):ifself.x_reflection_errors(x)==1:returnxreturnNonedefpart1(lines:aoc.Data)->int:"""Solve puzzle part 1: determine the sum of perfect reflection columns and 100× that of perfect reflection lines."""total=0fori,groupinenumerate(aoc.group_lines(lines)):pattern=Pattern.import_lines(group)if(x:=pattern.x_reflection1())isnotNone:total+=xelif(y:=pattern.y_reflection1())isnotNone:total+=100*yelse:raiseValueError('pattern without reflection‽')returntotaldefpart2(lines:aoc.Data)->int:"""Solve puzzle part 1: determine the sum of imperfect reflection columns and 100× that of imperfect reflection lines."""total=0fori,groupinenumerate(aoc.group_lines(lines)):pattern=Pattern.import_lines(group)if(x:=pattern.x_reflection2())isnotNone:total+=xelif(y:=pattern.y_reflection2())isnotNone:total+=100*yelse:raiseValueError('pattern without reflection‽')returntotal
La première partie ne pose aucune difficulté particulière. Pour la seconde, l'énoncé suggère presque une implémentation directe : essayer de changer les points, un par un, et chercher à chaque fois si on trouve une réflexion. Inutile de dire que ça risque de prendre un peu trop de temps.
En reformulant le problème, on peut trouver une solution plus intelligente : on ne cherche plus une réflexion parfaite mais une réflexion avec exactement une erreur. Voilà, vous avez l'algorithme, je vous laisse. :-)
Ah, je crois que j'ai trouvé ce qui consomme de la mémoire dans ton cache. Et qui doit peut-être pas mal le ralentir aussi. Tu as self parmi les arguments de la fonction sur laquelle le cache travaille.
Intuitivement, je dirais que pour un maximum d'efficacité, il faut minimiser les arguments d'une fonction sur laquelle on veut appliquer un cache. Quitte à définir une fonction auxiliaire locale et que ce soit celle-là qui soit cachée.
Et donc voici ma solution, en python, et là encore, de façon surprenante, PyPy est quasiment équivalent à CPython, malgré les gros gros calculs !
Ça ne m'étonne pas trop. Les gros calculs en question sont surtout des appels de fonctions, des branchements, des tests et des additions d'entiers. Je doute que ça soit les points forts de PyPy, si ?
Bon, voilà quoi. J'ai utilisé un cache, évidemment.
Pour info, avec un coefficient de pliage de 10 et en utilisant CPython, j'obtiens une réponse 1 498 094 344 344 361 041 914 985 222 578 (1,5×10³⁰ soit 1,5 millier de milliards de milliards de milliards) en 3,3 secondes et une consommation mémoire de 22 Mio :
% /usr/bin/time -v bin/12.py -2 < in/12.in
Solving part 2:
1498094344344361041914985222578
Command being timed: "bin/12.py -2"
User time (seconds): 2.86
System time (seconds): 0.41
Percent of CPU this job got: 99%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:03.29
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 22772
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 111390
Voluntary context switches: 1
Involuntary context switches: 305
Swaps: 0
File system inputs: 0
File system outputs: 0
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0
[^] # Re: Géométrie vectorielle et analytique
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code 2023, jour 24. Évalué à 3.
Pour déterminer la position de départ donc, la première idée consiste à chercher l'intersection de deux trajectoires corrigées. Une intersection de droites donc. Sauf que c'est assez moche à calculer, on peut faire plus élégant.
Les trajectoires corrigées étant toutes sécantes au même point, celui qu'on recherche, en les prenant deux par deux on peut définir des plans. Il est temps d'ouvrir une petite parenthèse.
Plan défini par deux droites
Dans l'espace, deux droites peuvent définir un plan, à condition nécessaire et suffisante qu'elles soient soit parallèles mais non identiques, soit sécantes.
Soient deux droites pertinentes selon cette condition et définies chacune par un point et par un vecteur
(p0, v0)
et(p1, v1)
. On cherche à caractériser leur plan commun, par un point et un vecteur normal.Si elles ne sont pas parallèles, leurs vecteurs directeurs
v0
etv1
ne sont pas colinéaires. Leur produit vectorieln = v0 ^ v1
est non nul et orthogonal aux deux droites et fait donc un très bon vecteur normal pour le plan cherché.Si elles sont parallèles leurs vecteurs directeurs sont certes colinéraires, mais
p0 - p1
n'est pas colinéaire à ces derniers (sinon, vous pouvez vérifier, les droites seraient identiques). On peut donc choisirn = v0 ^ (p0 - p1)
comme vecteur normal pour le plan cherché.Il nous reste à trouver un point sur le plan cherché : c'est trivial, il suffit de prendre par exemple
p0
puisque ce plan contient par définition la première droite. Pour obtenir une équation de plan du typen ⋅ r = d
, il suffit de prendred = p0 ⋅ n
.En fait, nous nous intéressons seulement au cas de droites sécantes, voici donc le calcul pour obtenir l'équation du plan dans ce cas spécifique :
Retour au problème
En prenant les trajectoires corrigées deux par deux, on peut définir un tas de plans contenant tous le point cherché. Or l'intersection de trois plans non parallèles est un point, qui sera forcément ce dernier ! Ouvrons une nouvelle parenthèse.
Intersection de plans
Allons-y pour l'intersection de trois plan non parallèles
(p0, d0)
,(p1, d1)
et(p2, d2)
.Le point d'intersection, que nous allons noter
r
, respecte les équation des trois plans :L'astuce consiste ici à choisir une base vectorielle alternative :
En exprimant la position cherchée comme combinaison de ces trois vecteurs
r = a0 u 0 + a1 u1 + a2 u2
, l'équation du premier plan devient :On peut reconnaître ici le produit mixte de nos trois vecteurs normaux, un scalaire que l'on va noter
U = n0 ⋅ (n1 ^ n2)
. Les trois équations deviennent de la même façon :Ce qui nous donne finalement ces fameux coefficients :
Qui nous donnent donc le vecteur position de l'intersection de nos trois plans. Pour rappel, voici les calculs à effectuer à partir des constantes définissant les trois plans :
Re-retour au problème
En prenant trois trajectoires corrigées
(p0, v0 - v)
,(p1, v1 - v)
et(p2, v2 - v)
, on sait désormais caractériser leurs plans communs deux à deux, soit trois plan. Et on sait également déterminer l'intersection de ces trois plan. Problème résolu !# Géométrie vectorielle et analytique
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code 2023, jour 24. Évalué à 3.
Sommaire
Bon, c'est un problème de géométrie.
Je passe sur la première partie, trouver des intersections de ligne dans le plan c'est sans grand intérêt.
La seconde partie est beaucoup, beaucoup plus intéressante. J'ai vainement cherché une solution élégante avant d'en trouver une sur Reddit. Je vous l'explique.
À supposer que l'on trouve une position de départ p et une vitesse v qui permette de percuter tous les grêlons, en se plaçant dans le référentiel de notre projectile, ce sont tous les grêlons qui vont converger jusqu'à l'atteindre, chacun à son tour. Autrement dit, pour un grêlon
(p0, v0)
, la droite(p0, v0 - v)
passe par notre position de départ p. Et il en est de même pour les autres grêlons.Par conséquent, les trajectoires corrigées des trois premiers grêlons
(p0, v0 -v), (p1, v1 -v)
et(p2, v2 -v)
se coupent deux à deux en p. Il est temps d'ouvrir une parenthèse sur les droites sécantes.Des droites sécantes
Dans l'espace, contrairement au plan, des droites peuvent être identiques, parallèles, sécantes ou… rien de tout ça.
Étant données deux droites caractérisées chacune par un point et un vecteur directeur
l0 = (p0, v0)
etl1 = (p1, v1)
, la première chose à vérifier est qu'elles ne sont ni identiques ni parallèles. Autrement dit, que leurs vecteurs directeurs ne sont pas colinéaires. Une façon de faire consiste à prendre leur produit vectoriel, qui ne doit pas être nul.Si ces droites ne sont pas parallèles, les droites vectorielles associées
(O, v0)
et(O, v1)
, définissent un plan vectoriel. Ce plan vectoriel peut être caractérisé par un vecteur normal facile à construire par le produit vectoriel des vecteurs directeurs de ces deux droites :n = v0 ^ v1
.Le plan affine parallèle à ce plan vectoriel et incluant
d0
a pour équation vectorieller ⋅ n = p0 ⋅ n
. Le plan affine incluantd1
a quant a lui pour équation vectorieller ⋅ n = p1 ⋅ n
.Ces plans sont identiques si et seulement si
p0 ⋅ n = p1 ⋅ n
. Autrement dit, en revenant à la définition de ce vecteur normal n, si(p0 - p1) ⋅ (v0 ^ v1) = 0
. Si ces plans sont identiques, les droites sont coplanaire, et n'étant pas parallèles, elles sont donc sécantes. Réciproquement, si les droites sont coplanaires, les plans sont identiques.Retour au problème
Puisque le problème a une solution, les trajectoires corrigées de deux grêlons
(p0, v0 - v)
et(p1, v1 - v)
sont sécantes. Bon, ok, à condition qu'elles ne soient pas identiques : si c'était le cas on prendrait juste une autre grêlon, on en a plein à notre disposition. Mais vous pouvez vérifier sur vos données, ce cas ne se présente pas. Par conséquent :En réorganisant un peu et en utilisant les propriété du produit mixte :
Donnons un nom à ces termes constants :
On a donc :
Plus de droites
De la même façon, en utilisant une droite de plus, posons :
On a maintenant trois équations sur v :
Pour résoudre ça simplement, il y a une astuce. On va utiliser une base vectorielle ainsi définie, avec des vecteurs conçus que chacun d'entre eux soit orthogonal à deux vecteurs des équations précédentes :
Et écrire la vitesse que nous cherchons sur cette base :
v = a0 u0 + a1 u1 + a2 u2
. Les équations précédentes deviennent désormais :On peut reconnaître ici le produit mixte de nos vecteurs
A0
et compagnie, que l'on va nommerA* = A0 ⋅ (A1 ^ A2)
, ce qui donne :Et finalement nos coefficients :
Récapitulons
Avec les trois premiers grêlons, on calcule les vecteurs et scalaires constants suivants :
Puis les vecteurs suivants :
Et enfin les coefficients suivants :
La vitesse de notre projectile doit être :
Et la position alors ?
La vitesse, c'est bien, mais c'est surtout la position de départ qu'on nous demande. On va dire que c'est facile à déduire désormais : c'est l'intersection des trajectoires corrigées des deux premiers grêlons. Par exemple, on peut en prendre d'autres si on veut.
Sauf que non, ce n'est vraiment pas trivial à calculer, l'intersection de deux droites sécantes dans l'espace. Il y a encore une astuce, et celle-là est vraiment de moi. Je vous la donnerai plus tard, là je suis fatigué de raconter mes aventures mathématiques.
[^] # Re: J'ai aussi utilisé le Z3 theorem prover.
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code 2023, jour 24. Évalué à 3.
Un code de cette longueur, ça aurait valu la peine de le mettre ailleurs, là ça nous fait une page terriblement longue à parcourir.
# Fromagerie
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au journal [ HS ] Fromage râpé pour accompagner les pâtes ou autre .... Évalué à 4.
Du fromage acheté chez un producteur à l'occasion de vacances à la montagne. Ou du fromage acheté à la fromagerie de mon quartier. Ou au marché. Mais en aucun cas du fromage industriel pré-râpé.
Bref, du fromage en morceau que je râpe moi-même… ou que je demande au fromager de râper et de mettre dans une gamelle qui m'appartient. De l'achat en vrac quoi.
# Pas d'exemple
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code 2023, jour 20. Évalué à 3.
À noter que les exemples fournis en première partie ne s'appliquaient pas à la deuxième partie, et que cette dernière ne fournissait aucun exemple utilisable. J'ai trouvé ça vache.
[^] # Re: La solution la plus courte à écrire, la plus longue à expliquer.
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code, jour 18. Évalué à 3.
Non mais là, faut savoir s'arrêter quand même. Ce serait dommage de mourir ou de tuer quelqu'un pour cause d'AoC.
[^] # Re: Optimisation insuffisante
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code, jour 19. Évalué à 3.
Bon, j'ai eu mon résultat en un peu moins d'une heure avec PyPy. Mais ça reste moche à mes yeux. À l'occasion, je m'essaierai à optimiser encore ça avec un découpage plus astucieux.
[^] # Re: Solution en Haskell.
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code, jour 19. Évalué à 3.
Ah, je crois que je devine la subtilité. En descendant la chaîne des workflows, ou plutôt la chaîne des règles et des workflows, on peut découper sur chaque test, ce qui fait au final beaucoup moins de pavés qu'en quadrillant l'espace entier.
[^] # Re: Solution en Haskell.
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code, jour 19. Évalué à 3.
Est-ce que tu découpes donc tout l'espace autour de chaque plan correspondant aux valeurs des seuils des critères des workflows ?
[^] # Re: Solution en Haskell.
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code, jour 19. Évalué à 3.
Je ne suis pas sûr de comprendre, et ne lisant pas la Haskell, je me demande si tu peux m'éclairer. Est-ce un genre de dichotomie que tu fais ? Sur quel critère découpes-tu ou non un pavé de valeurs ?
# Optimisation insuffisante
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code, jour 19. Évalué à 3.
La première partie ne pose pas de problème particulier. La seconde en revanche, c'est une autre paire de manches.
Mon idée pour optimiser cela consiste à réduire le nombre de cas que l'on teste. En effet, les workflows ont des règles basées sur des seuils, et fondamentalement, il suffit de balayer l'ensemble des combinaisons de ces valeurs seuils pour pouvoir déterminer ce qui arrivera à l'importe quelle combinaison entre ces valeurs.
Il faut faire attention à la façon dont on définit les seuils en question, parce que les définitions utilisent les opérateurs < et > qui ne sont pas l'opposé l'un de l'autre. Bref, il y a un détail important que je ne donnerai pas ici.
Toujours est-il que, pour mes données, ça me ramène à 6,4 × 10⁹ = 6,4 milliards de possibilités. Et c'est un peu long à tester : à un rythme de l'ordre de 150.000 combinaisons par seconde, ça va me prendre une douzaine d'heures. Il faut que je trouve mieux que ça…
# Trois dimensions
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code, jour 17. Évalué à 3. Dernière modification le 18 décembre 2023 à 20:15.
Pour ce problème, j'ai considéré qu'on n'était pas vraiment en deux dimensions, mais plutôt en trois. Parce que l'état d'un creuset, ce n'est pas seulement sa position sur le terrain, mais aussi le nombre de cases qu'il a parcouru dans une direction donnée… ce qui se représente également très bien par un nombre, que je considère comme une troisième coordonnée.
Ça permet de se ramener à un problème de parcours de proche en proche, avec une définition bien particulière des voisins d'un point.
Voici le code :
[^] # Re: Sans géométrie
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code, jour 18. Évalué à 3. Dernière modification le 18 décembre 2023 à 20:01.
Le code :
# Sans géométrie
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code, jour 18. Évalué à 3.
Première partie
Pour la première partie, je fais du remplissage, voici les étapes :
Trouver l'aire, c'est trivial.
Deuxième partie
Pareil. Comment ça, pareil, alors que les coordonnées sont beaucoup trop grandes pour pouvoir faire ça avec des matrices ? Eh bien, on n'a pas besoin de toutes les coordonnées, voyez-vous. On a seulement besoin de celles où commencent ou finissent des tranchées en fait.
Bref, je me construis une matrice donc les cases ont des dimensions virtuelles carrément variables. Ensuite, on se ramène à l'algorithme précédent, avec un détail de pondération pour le calcul de l'aire.
[^] # Re: Pourquoi ça cycle
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code, jour 14. Évalué à 3.
En effet. Mais c'est moins que 100×100 pour chaque galet, tout simplement parce que peu importe la position de chaque galet, ils se ressemblent tous au point d'être interchangeables. C'est plutôt la combinaison de 2.000 parmi 10.000 qui majore le nombre de possibilités. Ça fait quand même vachement beaucoup trop.
# Pas si vite
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code, jour 15. Évalué à 3.
Oui, justement, j'ai comme l'impression que ce n'est pas parce qu'on a remis en service la production de lave que les forges de l'île du métal vont se remettre en marche toutes seules et que les lutins arriveront à produire des pièces de rechange sans aide. Et même alors quelque chose me dit que même avec des pièces de rechange toutes neuves, les lutins de l'île du désert vont avoir besoin de notre aide pour réparer leurs machines et pour collecter du sable. Sur l'île de l'île, on risque aussi d'avoir besoin d'assistance pour relancer la filtration de l'eau et l'envoyer sur l'île de la neige. Quand à l'île de la neige, je pense que la machine à neige ne va pas recevoir directement cette eau, et que les lutins ont sûrement fini par oublier comment cette machine.
Si les lutins savaient se débrouiller tout seuls, ça se saurait depuis le temps. Et là, on vient de relancer un processus vachement de pure ingénierie lutinesque donc vachement complexe et instable. On n'est pas encore sortis de l'auberge.
# Pourquoi des dictionnaires ?
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code, jour 15. Évalué à 2. Dernière modification le 15 décembre 2023 à 13:10.
Pourquoi utilisez-vous des dictionnaires plutôt qu'un tableau ? On a 256 boîtes, indexées par un entier de 0 à 255, ça semble naturel de représenter ça par un tableau de 256 boîtes non ?
Mon code :
# Pourquoi ça cycle
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code, jour 14. Évalué à 2.
En voyant la partie 2, j'ai immédiatement pensé deux choses :
Ce dernier point est une certitude absolue. Pourquoi donc ? Parce qu'il y a 100 ligne et 100 colonnes, donc moins de 100 × 100 = 10.000 positions possibles des pierres qui roulent. En fait, bien moins que ça, parce que les positions occupées par les pierres qui ne roulent pas ne sont pas utilisables, et que seules les dispositions stables par le nord – on se comprend – sont admissibles.
Bref, en moins de 10.000 cycles je suis sûr d'avoir au moins deux fois la même disposition. Et retomber sur une disposition déjà vue, c'est aussi retomber sur la disposition suivante au cycle d'après, etc.
Chez moi ça cycle en 64 cycles.
Le code :
# Imprimante à réservoir d'encre
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Imprimante. Évalué à 5.
Après des années à utiliser des pilotes spécifiques, les fabricants d'imprimantes de sont enfin mis ensemble pour concevoir une, enfin deux normes inspirées de CUPS. Une le la compatibilité avec les appareils Apple et une pour Android. CUPS prend en charge les deux.
Concrètement, ça veut dire que si tu prends une imprimante qui prend en charge AirPrint ou Mopria, ça marchera juste sans rien avoir à faire. Et c'est le cas de la grande majorité des imprimantes récentes, donc tu es assez libre dans ton choix.
Maintenant, si par hasard tu veux éviter de contribuer au modèle de financement par le consommable, avec des imprimantes vendues presque à perte et de l'encre vendue plus cher que le Chanel n°5, tu peux choisir une imprimante à réservoir d'encre. Ça coûte plus cher à l'achat, mais l'encre fournie avec correspond à un volume d'impression qui, en cartouches, coûterait déjà plus cher que le prix total de l'imprimante !
[^] # Re: casse tête
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au journal devoir hacker un train !. Évalué à 3.
Ça s'appelle un vice caché. Dans le cas présent, ce serait reconnu sans aucune difficulté. Avec en prime une mauvaise foi patente de la part du fabricant. Bref, si c'était en France, la SNCF pourrait le contraindre à payer des dommages-intérêts suffisants pour les mettre en faillite.
[^] # Re: Pas bien compliqué… en reformulant
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code, jour 13. Évalué à 3.
Allez, voici le code :
# Pas bien compliqué… en reformulant
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code, jour 13. Évalué à 3.
La première partie ne pose aucune difficulté particulière. Pour la seconde, l'énoncé suggère presque une implémentation directe : essayer de changer les points, un par un, et chercher à chaque fois si on trouve une réflexion. Inutile de dire que ça risque de prendre un peu trop de temps.
En reformulant le problème, on peut trouver une solution plus intelligente : on ne cherche plus une réflexion parfaite mais une réflexion avec exactement une erreur. Voilà, vous avez l'algorithme, je vous laisse. :-)
[^] # Re: Rien de vraiment compliqué, il faut juste utiliser tout ce qu'on sait faire.
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code 2023, jour 12. Évalué à 5.
Ah, je crois que j'ai trouvé ce qui consomme de la mémoire dans ton cache. Et qui doit peut-être pas mal le ralentir aussi. Tu as
self
parmi les arguments de la fonction sur laquelle le cache travaille.Intuitivement, je dirais que pour un maximum d'efficacité, il faut minimiser les arguments d'une fonction sur laquelle on veut appliquer un cache. Quitte à définir une fonction auxiliaire locale et que ce soit celle-là qui soit cachée.
[^] # Re: Rien de vraiment compliqué, il faut juste utiliser tout ce qu'on sait faire.
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code 2023, jour 12. Évalué à 4.
Ça ne m'étonne pas trop. Les gros calculs en question sont surtout des appels de fonctions, des branchements, des tests et des additions d'entiers. Je doute que ça soit les points forts de PyPy, si ?
[^] # Re: Rien de vraiment compliqué, il faut juste utiliser tout ce qu'on sait faire.
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Advent of Code 2023, jour 12. Évalué à 4. Dernière modification le 12 décembre 2023 à 14:28.
Bon, voilà quoi. J'ai utilisé un cache, évidemment.
Pour info, avec un coefficient de pliage de 10 et en utilisant CPython, j'obtiens une réponse 1 498 094 344 344 361 041 914 985 222 578 (1,5×10³⁰ soit 1,5 millier de milliards de milliards de milliards) en 3,3 secondes et une consommation mémoire de 22 Mio :