Petite erreur de ma part, ce n'est pas un système d'équations linéaires mais quadratiques.
Mais bon, ça n'empêche pas à Z3 de le résoudre.
Comme annoncé, j'ai écrit des petites fonctions utilitaires pour Z3.
Ca donne ça. C'est plus lisible qu'avant (si on a un peu l'habitude de la syntaxe d'Haskell.
Pour la première partie, il faut déterminer le point d'intersection entre deux droites données par chacune par un point et un vecteur.
Je n'avais pas trop envie de me prendre la tête à calculer ça alors j'ai regardé sur Wikipedia. J'ai bien fait parce que la formule n'est pas évidente.
Pour la partie 2:
étant données n grelons de positions initiales px_i, py_i pz_i et de vitesse vx_i, vy_i vz_i, il faut trouver un grelon de position initial px, py, pz et de vitesse vx, vy, vz qui rentre en collision avec tous les autres.
C'est à dire que pour tout i dans [1..n], il existe un temps t_i tel que
- px + vx * t_i = px_i + vx_i * t_i
- py + vy * t_i = py_i + vy_i * t_i
- pz + vz * t_i = pz_i + vz_i * t_i
Ca revient donc à résoudre un système d'équations linéaires (avec une matrice sparse).
Comme je n'avais pas envie de réimplémenter ça, j'ai utilisé Z3. Et là, c'est vraiment une purge, le binding Z3 n'est pas terrible, il est beaucoup trop verbeux.
Je vais essayer d'écrire quelques helpers pour simplifier ça si j'ai le temps.
Ou alors, utiliser une autre librairie.
J'ai trouvé le problème assez simple aujourd'hui. En tout cas plus simple que les jours précédents. Dommage que je me sois levé tard.
Tout d'abord, remarquons que le problème qu'on essaie de résoudre (Longest Path) est NP-difficile. Ce qui ne veut pas dire qu'on ne va réussir car il n'y a pas tellement de choix et donc de backtrack à faire.
La première partie est du backtracking classique. Dans la deuxième partie, l'espace d'exploration augmente considérablement. Mais on se rend qu'il y a de longs couloirs, c'est à dire une suite de sommets de degré 2.
On va dans compresser la grille de cette manière:
on dit qu'une tuile est intéressante si c'est la tuile de départ, d'arrivée ou si c'est une jonction, c'est à dire un sommet de degré au moins 3.
Et pour chaque tuile intéressante, on va chercher dans chaque direction la prochaine tuile intéssante ainsi que la distance qui les sépare.
On va appliquer notre algo de backtracking sur cette instance.
Voici le code:
comme d'habitude, on va essayer d'être le plus générique possible et on va définir une fonction longestPath qui prend en entrée un sommet de départ, un sommet d'arrivée et une fonction de voisinages. Elle s'applique donc à n'importe quelle strucutre et pas seulement aux grilles. On va la mettre dans ma librairie de fonctions de recherche dans un graphe.
Je viens de me rendre compte que support et supported pouvaient être des tableaux plutôt que des dictionnaires. Avec quelques autres trucs, je descend à 10ms pour la partie 2.
Le problème d'aujourd'hui n'est pas très dur conceptuellement mais j'ai un peu galéré à cause de bugs. Heureusement que l'exemple est là pour nous aider contrairement à hier et avant-hier.
Tout d'abord, on va trier les briques selon la coordonnée z. Ca rend les choses plus à traiter.
On va ensuite simuler la chute de chaque pièce par ordre d'apparition, ce qui ne pose pas de problème grâce au tri que l'on vient de faire.
On va ensuite calculer support et supported. support est un dictionnaire qui, à une brique i, associe les index des pièces qui la supportent.
De même supported est un dictionnaire qui, à une brique i, associe les index des pièces supportées par elle.
On dira qu'une brique est stable si elle est supportée par au moins deux autres pièces.
Du coup, une pièce peut être désintégrée si les pièces supportées par elle sont toutes stables.
Pour la partie 2, il s'agit pour chaque pièce de faire un parcours en profondeur (en largeur marche aussi) pour simuler la cascade entrainée par la désintégration d'une pièce.
Une pièce chute si les pièces qui la supportent sont soi la pièce qui est désintégrée soit vont chuter.
J'avais écrit une fonction DFS générique qui m'a déjà servie dans plusieurs exemples.
Cette fonction prenait comme paramètre un sommet de départ et une fonction qui a un sommet associe son voisinage, c'est à dire ses sommets successeurs.
Problème: on a besoin ici de connaitre les sommets déjà parcourus (qui correspondent aux briques qui vont être désintégrées) pour calculer le voisinage.
J'ai donc écrit une fonction BFS générique mais qui prend en compte ce paramètre: la fonction de voisinage prend en paramètre un sommet ainsi que l'ensemble des sommets déjà visités.
Pour la partie 1, il faut remarquer vu que la parité de la distance au point de départ change à chaque déplacement. La distance entre le point de départ et un sommet accessible après 64 mouvements est donc toujours pair.
Du coup, les sommets à trouver sont ceux situés exactement à distance n où n est pair et n <= 64.
Un simple parcours en largeur fait l'affaire.
Pour la partie 2, ça se complique.
Mais on repère que l'instance est assez particulière:
- la grille de départ est carrée;
- le point de départ se trouve au centre;
- la ligne horizontale, la ligne verticale ainsi que les diagonales autour du centre sont vides.
Notons f(n) le nombre de points accessibles après n mouvements et notons M la taille (verticale et horizontale) de la grille.
On peut donc se dire qu'il doit y avoir une régularité entre f(n) et f(n+M).
Notons r = 26501365 mod M et regardons la suite des u_i = f(r + iM) pour i entier naturel.
Après avoir calculé les 5 ou 6 premières valeurs par ordinateur, on se rend compte que la suite est une séquence quadratique ou dit autrement que la suite des dérivées discrètes secondes est constante. Elle est donc de la forme u_n = a *n^2 + b * n + c.
Il faut donc calculer u_n avec n = 26501365 // M où // désigne la division entière.
Pour calculer u_n, on a besoin que des 3 premiers termes de la suite.
Notons d1 = u1 - u0, d2 = u2 - u1 et d' = d2 - d1 alors u_n = u0 + n * d1 + n * (n-1) * d' / 2.
Et voilà !
Voici le code un peu commenté.
Tout d'abord le parsing et un précalcul pour transformer l'entrée en matrice et repérer le sommet de départ.
Pour la partie 1, on définir une fonction nbors qui donne le voisinage d'une tuile.
Comme on ne sort pas de la grille dans la partie 1 et pour factoriser avec la partie 2,
on fait des modulo sur les coordonnées.
Pour la partie 2, on définit d'abord une fonction générique pour calculer le n-ième d'une séquence quadratique.
-- given a quadratic sequence with first terms u0, u1, u0, compute u_nquadraticSequence::Integer->Integer->Integer->Integer->IntegerquadraticSequenceu0u1u2n=u0+n*d1+n*(n-1)*d'`div`2whered1=u1-u0d2=u2-u1d'=d2-d1
Ensuite, on peut l'appliquer à notre problème en calculant les trois premiers termes de la suite grâce à un parcours en largeur.
En Haskell, on la structure Seq. C'est des structures immuables permettant d'ajouter ou d'enlever un élément en début ou fin en temps constant. Quand je dis enlever ou ajouter, je veux dire créer une nouvelle séquence en se basant sur l'existante mais sans la modifier.
Sous le capot, c'est basé sur les finger trees.
Je peux dire que j'ai galéré sur celui là. Tout ça parce que j'ai mal lu l'énoncé.
Je n'avais pas vu qu'il fallait traiter les gestions de pulsations sous forme de file.
Je les traitais sous forme de pile et bizarrement ça m'a quand même donné la bonne réponse pour la première partie.
Pour la deuxième partie, c'est comme pour le jour 8, c'est très compliqué dans le cas général mais facile parce que l'instance a de bonnes propriétés (je n'aime pas trop ce genre de journée).
Tout d'abord, on remarque que rx a un seul prédécesseur qui est de type Conjonction et que celui a 4 prédécesseurs (que je vais appeler a, b, d) chacun de type Conjonction.
Ensuite, si on enlève broadcaster, rx et son prédécesseur, on se retrouve avec 4 composantes connexes et donc les pulsations de chacune vont vivre leur vie indépendamment des autres.
Si, on regarde quand a, b, c ou d envoie une pulsation forte, on remarque que ça forme un cycle sans prépériode et qu'une pulsation forte n'apparait qu'une seule fois durant un cycle.
Il suffit donc de repérer pour a, b, c et d la première fois qu'il y a une pulsation forte et faire le PPCM entre les différentes valeurs trouvées.
Pour le code en Haskell, j'utilise une monade State et des Lens, ce qui me permet de simplifier l'écriture.
dataType=FlipFlop|Conjunction|BroadcasterdataModule=Module!Type[String]typeNetwork=HashMapStringModuledataNState=NState{_ffState::!(HashMapStringBool)-- the state of flip flap mdoules,_from::!(HashMapString(HashMapStringBool))-- last signal sent by predecessor,_nbLow::!Int,_nbHigh::!Int,_seen::!(HashMapStringBool)}makeLenses''NStateparser::ParserNetworkparser=insertRx.Map.fromList<$>module_`sepEndBy1`eolwheremodule_=dot<-type_n<-name<*" -> "ns<-name`sepBy1`", "pure(n,Moduletns)name=somelowerChartype_=FlipFlop<$"%"<|>Conjunction<$"&"<|>pureBroadcasterinsertRx=Map.insert"rx"(ModuleBroadcaster[])sendSignal::Network->Seq(String,String,Bool)->StateNState()sendSignalnetwork=\caseSeq.Empty->pure()((name,srcName,pulse):<|queue')->doifpulsethennbHigh+=1elsedonbLow+=1seen.ixname.=TrueletModuletype_dests=networkMap.!namecasetype_ofBroadcaster->sendSignalnetwork$queue'><Seq.fromList(map(,name,False)dests)FlipFlop->ifpulsethensendSignalnetworkqueue'elsedonstate<-getletstate=_ffStatenstateMap.!nameffState.ixname.=notstatesendSignalnetwork$queue'><Seq.fromList(map(,name,notstate)dests)Conjunction->dofrom.ixname.ixsrcName.=pulsenstate<-getletsignal'=anynot$Map.elems(_fromnstateMap.!name)sendSignalnetwork$queue'><Seq.fromList(map(,name,signal')dests)round::Network->StateNState()roundnetwork=doseen.=Map.map(constFalse)networksendSignalnetwork$Seq.singleton("broadcaster","$dummy",False)initNState::Network->NStateinitNStatenetwork=NStateinitFfStateinitFrom00initSeenwhereinitFfState=Map.map(constFalse)networkemptyFrom=Map.map(constMap.empty)networkedgeList=concat.Map.elems$Map.mapWithKeygonetworkgou(Module_vs)=map(u,)vsinitFrom=foldl'go'emptyFromedgeListgo'from_(u,v)=Map.adjust(Map.insertuFalse)vfrom_initSeen=Map.map(constFalse)networkpart1::Network->Intpart1network=_nbLowfinalState*_nbHighfinalStatewherenstate=initNStatenetworkfinalState=flipexecStatenstatedoforM_[(1::Int)..1000]\_->roundnetworkpart2::Network->Integerpart2network=foldl'lcm1cycleswherenstate=initNStatenetworkpredRx=head.Map.keys$_fromnstateMap.!"rx"predPredRx=Map.keys$_fromnstateMap.!predRxnstates=iterate'(execState(roundnetwork))nstatecycles=mapextractCyclepredPredRxextractCyclename=head[idx|(idx,True)<-zip[0..].map((Map.!name)._seen)$nstates]solve::Text->IO()solve=aocparserpart1part2
J'ai une fonction go qui prend comme paramètre une liste de pavés et une liste de tests.
Elle va me renvoyer la liste des pavés qu'il y aura au final.
La fonction go fonctionne ainsi:
Je regarde le prochain test à faire.
Selon le résultat du test, je découpe ma liste de pavés en deux listes: les réussis et les échoués (en ayant éventuellement divisé des pavés).
Pour les réussis, je regarde l'instruction à faire quand le test est réussi et je stocke le résultat suivant dans une variable réussis2
- si l'instruction est "accepter": la liste des réussis.
- si l'instruction est "refuser": la liste vide
- si l'instruction est d'aller à un workflow x, j'appelle récursivement ma fonction go avec comme paramètre
-- la liste des réussis
-- la liste des tests pour le workflow x.
Pour les refusés, j'appelle récursivement ma fonction go avec comme paramètre
-- la liste des refusés
-- la liste des tests privés du premier élément.
et je stocke ça dans échoués2.
Le résultat de la fonction go sera reussis2 concaténé à échoués2.
Ca se fait bien en récursif. Je pense que c'est plus compliqué en itératif.
Par exemple si j'ai un pavé x = [1..200], m = [1..100], a = [1..100], s= [1..100] et que j'ai un test x<96, alors je divise mon pavé en
- un pavé accepté x = [1..95], m = [1..100], a = [1..100], s= [1..100]
- un pavé refusé x = [96..200], m = [1..100], a = [1..100], s= [1..100].
150 microsecondes pour la partie 1 et 600 microsecondes pour la partie 2.
La partie 1 est facile, passons.
J'ai rapidement eu l'idée pour la partie 2 mais j'ai galéré à débugger mon code.
Je ne suis pas très satisfait de mon code, je pense qu'il peut être simplifié.
L'idée est de considérer des ensembles de RatingRange deux à deux disjoints.
Les RatingRange étant des intervalles de valeurs pour chaque évaluation (x, m, a ou s).
On peut voir ça comme un rectangle en 4 dimensions.
On démarre avec un ensemble composé d'un seul RatingRange avec x = [1..4000], m=[1..4000], a=[1..4000] et s=[1..4000]
A chaque test effectué, on va séparer les RatingRange en deux catégories, ceux qui sont acceptés et ceux qui sont refusés. Pour cela, on devra parfois diviser un RatingRange en deux parties.
C'est ce que fait la fonction suivante haskell
splitRatings :: Test -> [RatingRange] -> ([RatingRange], [RatingRange])
A partir de là, il est relativement facile de simuler les worflow en prenant en entrée des RatingRange et de calculer le nombre total de possibilités de pièces vu que les RatingRange sont deux à deux disjoints.
Ce problème ressemble à la partie 2 du jour 10 de cette année.
La première partie peut être aisément fait avec un parcours en largeur/profondeur mais ça devient clairement impossible pour la partie 2.
Remarquons que l'on cherche à trouver le nombre de sommets de coordonnées entières situées à l'intérieur d'un polygone.
On va donc de baser sur le théorème de Pick et la shoelace formula.
La shoelace formula est une formule pour calculer l'aire d'un polygone.
Si les sommets du polygone sont (x1, y1), ..., (xn, yn) alors l'aire du polygone est |x1 y2 - y1 x2 + ... + x_{n-1} yn - y_{n-1} xn + xn y1 - yn x1| / 2
Le théorème de Pick donne une formule pour calculer le nombre de points intérieurs à un polygone à partir de son aire (qui est calculé par la shoelace formula) et du nombre de points à sa frontière (c'est juste la somme des distances données par les instructions).
La formule est la suivante: A = i + b/2 - 1 où A est l'aire, i le nombre de points à l'intérieur et b le nombre de points à la frontière.
Pour calculer le nombre de "#" total, il faut faire la somme i + b où i = A - b/2 - 1.
Donc, voici le code Haskell
dataDirection=Up|Down|Left|RightdataInstr=Instr!Direction!InthexToInt::String->InthexToInt=foldl'(\accx->acc*16+hexDigitToIntx)0wherehexDigitToIntx|isDigitx=ordx-ord'0'|otherwise=ordx-ord'a'+10parser::Parser[(Instr,Instr)]parser=instr`sepEndBy1`eolwhereinstr=dodir1<-direction<*" "len1<-decimal<*" (#"len2<-hexToInt<$>count5hexDigitChardir2<-direction2<*")"pure(Instrdir1len1,Instrdir2len2)direction=choice[Up<$"U",Down<$"D",Left<$"L",Right<$"R"]direction2=choice[Right<$"0",Down<$"1",Left<$"2",Up<$"3"]trenchPoints::[Instr]->[V2Int]trenchPoints=scanl'go(V200)wheregop(Instrdirlen)=p+casedirofLeft->V20(-len)Right->V20lenUp->V2(-len)0Down->V2len0-- return the double of the polygon areashoelaceFormula::[V2Int]->IntshoelaceFormulapoints=abs$sum(zipWithgopoints(drop1points++points))wherego(V2xy)(V2x'y')=x*y'-x'*y-- via Pick theorem and Shoelace Formula-- https://en.wikipedia.org/wiki/Pick%27s_theorem-- https://en.wikipedia.org/wiki/Shoelace_formulasolveFor::((Instr,Instr)->Instr)->[(Instr,Instr)]->IntsolveForfinstrs=boundary+interiorwhereinstrs'=mapfinstrsdoubleArea=shoelaceFormula(trenchPointsinstrs')boundary=sum[len|Instr_len<-instrs']interior=(doubleArea-boundary)`div`2+1solve::Text->IO()solve=aocparser(solveForfst)(solveForsnd)
Moi, je suis arrivé à descendre à 300ms mais je pense pas faire beaucoup mieux.
J'ai tenté des heuristiques pour A* mais rien de concluant.
Enfin, j'en avais une bien mais elle prenait trop de temps à être calculer.
Après quelques optimisations
200ms pour la partie 1 et 800ms pour la partie 2.
L'idée est qu'au lieu de dire qu'un noeud du graphe est un couple (position, direction) avec 4 directions possibles, je dis qu'un noeud est un couple (position, booléen)
ou le booléen m'indique si je me déplace horizontalement ou verticalement.
Ca fait 2 fois moins de noeuds dans le graphe. Du coup, logiquement, ça divise le temps d'exécution par deux (et même plus avec quelques autres optimisations).
Je trouve que ce problème ressemble à celui d'hier.
Il s'agit encore d'un parcours dans un graphe.
Ici, le graphe est pondéré. On va donc utiliser l'algorithme de Dijkstra au lieu d'un simple parcours en profondeur/largeur.
Comme hier, la direction est importante. Les sommets du graphe seront donc les paires (Position, Direction).
Les voisins d'un sommet ne seront pas les positions adjacentes dans la grille mais celle à distance entre 1 et 3 pour la partie 1 et entre 4 et 10 pour la partie 2.
La direction devra également changer à chaque fois.
Le poids des arêtes sera la somme des chiffres indiqués sur chacune des tuiles que l'on a parcouru durant ce déplacement.
Dans mon code, j'utilise la fonction dijkstra' que j'ai également écrite pour des problèmes des années précédentes.
Voici sa signature
Le type v est celui des sommets et le type w est celui des poids des arêtes.
La fonction prend en entrée
- une fonction de voisinage qui à chaque sommet associe une liste des sommets voisins ainsi que le poids de l'arête entre les deux sommets,
- une fonction de prédicat pour le sommet de fin,
- un ensemble de sommets de départ.
et renvoit la distance entre un des sommets de départs et un des sommets de fin si un chemin existe.
C'est un problème qui peut se faire un parcours en longueur ou largeur.
Problème: selon la direction du faisceau, les cases à visiter suivantes peuvent être différentes.
Du coup un sommet du graphe que l'on veut parcourir ne sera pas seulement une position dans la grille mais un couple (position, direction).
Je commence par importer mes fonctions nécessaires et définir les types utilisés dans le problème.
Les positions et directions sont des vecteurs de dimension 2.
J'appelerai le couple (Position, Direction) est un Beam.
Ensuite, vient le parcours en largeur. Je réutilise une fonction reachableFrom définie pour des problèmes précédents.
Elle prend deux arguments
- une fonction qui étant donné renvoit la liste des sommets voisins
- un sommet de départ
et renvoit l'ensemble des sommets accessibles depuis le sommet de départ.
Pour utiliser reachableFrom, je dois calculer, étant donné un beam, les beams suivants.
Je le fais en deux temps en définissant d'abord une fonction nextDirections qui étant donné une direction et une tuile me renvoit les directions suivantes.
Je peux maintenant définir ma fonction energized qui me renvoit le nombre de tuiles énergisées en utilisant les fonctions reachableFrom et neighbors définis plus haut.
Pour la partie 2, c'est du brute-force sur toutes les positions de départ possibles, j'ai pas trouvé mieux.
Ca se parallélise bien, j'utilise parMap qui est une version parallèle de map.
1400ms sur un seul core et 800ms en multicore pour la partie 2. Pas terrible le parallélisme, j'aurais espéré mieux. Je ne me suis peut-être pas pris comme il fallait.
Problème très simple aujourd'hui. Aucune subtilité algorithmique ou autre astuce à avoir.
Juste un petit détail, j'ai pensé à utiliser, pour représenter les boites, des hashmaps avec pour clé des strings (les labels) et pour valeur des ints (les longueurs focales).
Petit problème, les HashMap de la librairie containers ne préservent pas l'ordre d'insertion, cet ordre étant nécessaire pour calculer le "focusingPower".
J'ai donc utilisé des HashMaps d'une librairie tierce qui, elles, préservent l'ordre d'insertion.
250 microseconds pour la partie 1 et 600 microseconds pour la partie 2.
Je pense que ce serait plus performant si j'utilisais des structures de données mutables mais ce serait moins joli.
En se rendant compte qu'un cycle est un enchainement tilt, rotation, tilt, rotation, tilt, rotation, tilt, rotation, on peut écrire le code plus simplement.
Je viens de me rendre compte que mon code n'était pas correct tout le temps même s'il marchait bien sur l'input. En effet, il se pourrait que deux configurations identiques apparaissent mais pas dans la même position dans le cycle des directions.
Par exemple, une configuration alors que la prochaine instruction est d'aller vers l'ouest puis la même configuration alors que la prochaine instruction est d'aller au nord.
Du coup, je l'ai corrigé (il y a juste à changer la fonction part2).
Problème intéressant, il me rappelle le jour 17 de 2022.
J'ai écrit une fonction tilt qui fait tomber les briques vers l'ouest.
Pour cela, pour chaque ligne, je split la ligne selon # et pour chaque morceau du split, je sépare les O et les "." et je reconcatène tout en mettant les O d'abord et les "." ensuite.
Ensuite j'ai une fonction tiltInDirection qui se ramène à tilt en faisant des rotate et des reverse selon le cas de figure.
Pour la partie 2, je génère une suite infinie cyclique de North, West, South, East
et à partir de celle ci, je génère une liste infinie des différentes configurations après avoir effectué les tilts dans la direction donnée.
Dans cette liste, je cherche les indices x et y de la même configuration (fonction findRepetition) et la configuration qui nous intéresse est celle à l'indice x + (1000000000 - x) mod (y - x).
La partie 2 tourne en 600ms. Pas très satisfait mais ça reste raisonnable.
Voici le code
importAOC.Preludehiding(empty)importData.List((!!))importAOC(aoc)importqualifiedData.HashMap.StrictasMapimportAOC.Parser(Parser,sepEndBy1,eol,some)importAOC.Util(flattenWithIndex,splitWhen)dataRock=Empty|Round|Cubederiving(Eq,Enum)dataDirection=West|East|North|SouthtypeGrid=[[Rock]]instanceHashableRockwherehashWithSaltsrock=s`hashWithSalt`fromEnumrockparser::ParserGridparser=somerock`sepEndBy1`eolwhererock=Empty<$"."<|>Round<$"O"<|>Cube<$"#"-- tilt to Westtilt::Grid->Gridtilt=mapperRowwhereperRow=intercalate[Cube].mapgo.splitWhen(==Cube)goxs=rounded++emptywhere(rounded,empty)=partition(==Round)xstiltInDirection::Direction->Grid->GridtiltInDirection=\caseWest->tiltEast->mapreverse.tilt.mapreverseNorth->transpose.tilt.transposeSouth->reverse.transpose.tilt.transpose.reverse-- return the first two indices of the same element in a infinite list of elementsfindRepetition::Hashablea=>[a]->(Int,Int)findRepetition=goMap.empty.zip[0..]wheregom((i,x):xs)=casemMap.!?xofJustj->(j,i)Nothing->go(Map.insertxim)xsgo_[]=error"findRepetition: not an infinite list"-- compute the laod of a gridload::Grid->Intloadgrid=sum.mapscore$flattenWithIndexgridwherescore(i,_,Round)=len-iscore_=0len=lengthgridpart1::Grid->Intpart1=load.tiltInDirectionNorthpart2::Grid->Intpart2grid=load(grids!!y')where(x,y)=findRepetitiongridsy'=x+(1000000000-x)`mod`(y-x)grids=scanl'(fliptiltInDirection)griddirectionsdirections=cycle[North,West,South,East]solve::Text->IO()solve=aocparserpart1part2
Voici ma solution en Haskell.
Pas vraiment de difficulté, j'ai factorisé le code pour résoudre les deux parties de la même manière.
Pour les connaisseurs d'Haskell, le test isSymetry est lazy et s'arrête dès qu'il a détecté au moins une différence pour la partie 1 et deux différences pour la partie 2.
700 microsecondes pour chacune des parties.
En fait, j'utilise des lazy arrays à deux dimensions (la variable arr).
Je peux écrire un truc du genre à l'initialisation du tableau. arr[x][y] = quelque chose avec arr[x+1][y].
Si arr[x+1][y] n'est pas encore calculé, il va le faire et le stocker en mémoire dans le tableau. Sinon, il renvoit directement la valeur.
Je pense que c'est similaire au cache en python, comme tu disais.
[^] # Re: Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code 2023, jour 24. Évalué à 2. Dernière modification le 24 décembre 2023 à 12:26.
Petite erreur de ma part, ce n'est pas un système d'équations linéaires mais quadratiques.
Mais bon, ça n'empêche pas à Z3 de le résoudre.
Comme annoncé, j'ai écrit des petites fonctions utilitaires pour Z3.
Ca donne ça. C'est plus lisible qu'avant (si on a un peu l'habitude de la syntaxe d'Haskell.
# Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code 2023, jour 24. Évalué à 2.
Pour la première partie, il faut déterminer le point d'intersection entre deux droites données par chacune par un point et un vecteur.
Je n'avais pas trop envie de me prendre la tête à calculer ça alors j'ai regardé sur Wikipedia. J'ai bien fait parce que la formule n'est pas évidente.
https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
Pour la partie 2:
étant données n grelons de positions initiales
px_i, py_i pz_i
et de vitessevx_i, vy_i vz_i
, il faut trouver un grelon de position initialpx, py, pz
et de vitessevx, vy, vz
qui rentre en collision avec tous les autres.C'est à dire que pour tout i dans [1..n], il existe un temps
t_i
tel que-
px + vx * t_i = px_i + vx_i * t_i
-
py + vy * t_i = py_i + vy_i * t_i
-
pz + vz * t_i = pz_i + vz_i * t_i
Ca revient donc à résoudre un système d'équations linéaires (avec une matrice sparse).
Comme je n'avais pas envie de réimplémenter ça, j'ai utilisé Z3. Et là, c'est vraiment une purge, le binding Z3 n'est pas terrible, il est beaucoup trop verbeux.
Je vais essayer d'écrire quelques helpers pour simplifier ça si j'ai le temps.
Ou alors, utiliser une autre librairie.
Voici le code:
# Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code 2023, jour 23. Évalué à 4.
100 ms pour la partie 1, 4s pour la partie 2.
J'ai trouvé le problème assez simple aujourd'hui. En tout cas plus simple que les jours précédents. Dommage que je me sois levé tard.
Tout d'abord, remarquons que le problème qu'on essaie de résoudre (Longest Path) est NP-difficile. Ce qui ne veut pas dire qu'on ne va réussir car il n'y a pas tellement de choix et donc de backtrack à faire.
La première partie est du backtracking classique. Dans la deuxième partie, l'espace d'exploration augmente considérablement. Mais on se rend qu'il y a de longs couloirs, c'est à dire une suite de sommets de degré 2.
On va dans compresser la grille de cette manière:
on dit qu'une tuile est intéressante si c'est la tuile de départ, d'arrivée ou si c'est une jonction, c'est à dire un sommet de degré au moins 3.
Et pour chaque tuile intéressante, on va chercher dans chaque direction la prochaine tuile intéssante ainsi que la distance qui les sépare.
On va appliquer notre algo de backtracking sur cette instance.
Voici le code:
comme d'habitude, on va essayer d'être le plus générique possible et on va définir une fonction
longestPath
qui prend en entrée un sommet de départ, un sommet d'arrivée et une fonction de voisinages. Elle s'applique donc à n'importe quelle strucutre et pas seulement aux grilles. On va la mettre dans ma librairie de fonctions de recherche dans un graphe.Ensuite, vient le code du problème à proprement parler.
Tout d'abord les types utilisés et le parsing. Rien de bien compliqué.
Ensuite, on définit une fonction de voisnage pour la partie 1.
Pour la partie 2, on définit une fonction de voisinage qui ne prend pas en compte les pentes.
et on définit la fonction de compression de grille.
[^] # Re: Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code 2023, jour 22. Évalué à 1.
Je viens de me rendre compte que
support
etsupported
pouvaient être des tableaux plutôt que des dictionnaires. Avec quelques autres trucs, je descend à 10ms pour la partie 2.# Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code 2023, jour 22. Évalué à 1. Dernière modification le 22 décembre 2023 à 11:46.
5ms pour la partie 1 et 15ms pour la partie 2.
Le problème d'aujourd'hui n'est pas très dur conceptuellement mais j'ai un peu galéré à cause de bugs. Heureusement que l'exemple est là pour nous aider contrairement à hier et avant-hier.
Tout d'abord, on va trier les briques selon la coordonnée z. Ca rend les choses plus à traiter.
On va ensuite simuler la chute de chaque pièce par ordre d'apparition, ce qui ne pose pas de problème grâce au tri que l'on vient de faire.
On va ensuite calculer
support
etsupported
.support
est un dictionnaire qui, à une brique i, associe les index des pièces qui la supportent.De même
supported
est un dictionnaire qui, à une brique i, associe les index des pièces supportées par elle.On dira qu'une brique est stable si elle est supportée par au moins deux autres pièces.
Du coup, une pièce peut être désintégrée si les pièces supportées par elle sont toutes stables.
Pour la partie 2, il s'agit pour chaque pièce de faire un parcours en profondeur (en largeur marche aussi) pour simuler la cascade entrainée par la désintégration d'une pièce.
Une pièce chute si les pièces qui la supportent sont soi la pièce qui est désintégrée soit vont chuter.
J'avais écrit une fonction DFS générique qui m'a déjà servie dans plusieurs exemples.
Cette fonction prenait comme paramètre un sommet de départ et une fonction qui a un sommet associe son voisinage, c'est à dire ses sommets successeurs.
Problème: on a besoin ici de connaitre les sommets déjà parcourus (qui correspondent aux briques qui vont être désintégrées) pour calculer le voisinage.
J'ai donc écrit une fonction BFS générique mais qui prend en compte ce paramètre: la fonction de voisinage prend en paramètre un sommet ainsi que l'ensemble des sommets déjà visités.
Voici le code:
# Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code 2023, jour 21. Évalué à 3.
300ms pour la partie 2.
Pour la partie 1, il faut remarquer vu que la parité de la distance au point de départ change à chaque déplacement. La distance entre le point de départ et un sommet accessible après 64 mouvements est donc toujours pair.
Du coup, les sommets à trouver sont ceux situés exactement à distance n où n est pair et
n <= 64
.Un simple parcours en largeur fait l'affaire.
Pour la partie 2, ça se complique.
Mais on repère que l'instance est assez particulière:
- la grille de départ est carrée;
- le point de départ se trouve au centre;
- la ligne horizontale, la ligne verticale ainsi que les diagonales autour du centre sont vides.
Notons
f(n)
le nombre de points accessibles après n mouvements et notons M la taille (verticale et horizontale) de la grille.On peut donc se dire qu'il doit y avoir une régularité entre f(n) et f(n+M).
Notons
r = 26501365 mod M
et regardons la suite desu_i = f(r + iM)
pour i entier naturel.Après avoir calculé les 5 ou 6 premières valeurs par ordinateur, on se rend compte que la suite est une séquence quadratique ou dit autrement que la suite des dérivées discrètes secondes est constante. Elle est donc de la forme
u_n = a *n^2 + b * n + c
.Il faut donc calculer
u_n
avecn = 26501365 // M
où//
désigne la division entière.Pour calculer
u_n
, on a besoin que des 3 premiers termes de la suite.Notons
d1 = u1 - u0
,d2 = u2 - u1
etd' = d2 - d1
alorsu_n = u0 + n * d1 + n * (n-1) * d' / 2
.Et voilà !
Voici le code un peu commenté.
Tout d'abord le parsing et un précalcul pour transformer l'entrée en matrice et repérer le sommet de départ.
Pour la partie 1, on définir une fonction
nbors
qui donne le voisinage d'une tuile.Comme on ne sort pas de la grille dans la partie 1 et pour factoriser avec la partie 2,
on fait des modulo sur les coordonnées.
Pour la partie 2, on définit d'abord une fonction générique pour calculer le n-ième d'une séquence quadratique.
Ensuite, on peut l'appliquer à notre problème en calculant les trois premiers termes de la suite grâce à un parcours en largeur.
[^] # Re: Les données imposent la méthode
Posté par Guillaume.B . En réponse au message Advent of Code 2023, jour 20. Évalué à 1.
En Haskell, on la structure
Seq
. C'est des structures immuables permettant d'ajouter ou d'enlever un élément en début ou fin en temps constant. Quand je dis enlever ou ajouter, je veux dire créer une nouvelle séquence en se basant sur l'existante mais sans la modifier.Sous le capot, c'est basé sur les finger trees.
https://en.wikipedia.org/wiki/Finger_tree
# Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code 2023, jour 20. Évalué à 2.
Je peux dire que j'ai galéré sur celui là. Tout ça parce que j'ai mal lu l'énoncé.
Je n'avais pas vu qu'il fallait traiter les gestions de pulsations sous forme de file.
Je les traitais sous forme de pile et bizarrement ça m'a quand même donné la bonne réponse pour la première partie.
Pour la deuxième partie, c'est comme pour le jour 8, c'est très compliqué dans le cas général mais facile parce que l'instance a de bonnes propriétés (je n'aime pas trop ce genre de journée).
Tout d'abord, on remarque que rx a un seul prédécesseur qui est de type Conjonction et que celui a 4 prédécesseurs (que je vais appeler a, b, d) chacun de type Conjonction.
Ensuite, si on enlève broadcaster, rx et son prédécesseur, on se retrouve avec 4 composantes connexes et donc les pulsations de chacune vont vivre leur vie indépendamment des autres.
Si, on regarde quand a, b, c ou d envoie une pulsation forte, on remarque que ça forme un cycle sans prépériode et qu'une pulsation forte n'apparait qu'une seule fois durant un cycle.
Il suffit donc de repérer pour a, b, c et d la première fois qu'il y a une pulsation forte et faire le PPCM entre les différentes valeurs trouvées.
Pour le code en Haskell, j'utilise une monade State et des Lens, ce qui me permet de simplifier l'écriture.
[^] # Re: Solution en Haskell.
Posté par Guillaume.B . En réponse au message Advent of Code, jour 19. Évalué à 1. Dernière modification le 19 décembre 2023 à 13:51.
Je maintiens une liste de pavés.
J'ai une fonction go qui prend comme paramètre une liste de pavés et une liste de tests.
Elle va me renvoyer la liste des pavés qu'il y aura au final.
La fonction
go
fonctionne ainsi:Je regarde le prochain test à faire.
Selon le résultat du test, je découpe ma liste de pavés en deux listes: les réussis et les échoués (en ayant éventuellement divisé des pavés).
Pour les réussis, je regarde l'instruction à faire quand le test est réussi et je stocke le résultat suivant dans une variable
réussis2
- si l'instruction est "accepter": la liste des réussis.
- si l'instruction est "refuser": la liste vide
- si l'instruction est d'aller à un workflow x, j'appelle récursivement ma fonction
go
avec comme paramètre-- la liste des réussis
-- la liste des tests pour le workflow x.
Pour les refusés, j'appelle récursivement ma fonction
go
avec comme paramètre-- la liste des refusés
-- la liste des tests privés du premier élément.
et je stocke ça dans échoués2.
Le résultat de la fonction
go
serareussis2
concaténé àéchoués2
.Ca se fait bien en récursif. Je pense que c'est plus compliqué en itératif.
[^] # Re: Solution en Haskell.
Posté par Guillaume.B . En réponse au message Advent of Code, jour 19. Évalué à 1.
Par exemple si j'ai un pavé
x = [1..200], m = [1..100], a = [1..100], s= [1..100]
et que j'ai un testx<96
, alors je divise mon pavé en- un pavé accepté
x = [1..95], m = [1..100], a = [1..100], s= [1..100]
- un pavé refusé
x = [96..200], m = [1..100], a = [1..100], s= [1..100]
.# Solution en Haskell.
Posté par Guillaume.B . En réponse au message Advent of Code, jour 19. Évalué à 2. Dernière modification le 19 décembre 2023 à 13:07.
150 microsecondes pour la partie 1 et 600 microsecondes pour la partie 2.
La partie 1 est facile, passons.
J'ai rapidement eu l'idée pour la partie 2 mais j'ai galéré à débugger mon code.
Je ne suis pas très satisfait de mon code, je pense qu'il peut être simplifié.
L'idée est de considérer des ensembles de
RatingRange
deux à deux disjoints.Les RatingRange étant des intervalles de valeurs pour chaque évaluation (x, m, a ou s).
On peut voir ça comme un rectangle en 4 dimensions.
On démarre avec un ensemble composé d'un seul RatingRange avec
x = [1..4000], m=[1..4000], a=[1..4000] et s=[1..4000]
A chaque test effectué, on va séparer les RatingRange en deux catégories, ceux qui sont acceptés et ceux qui sont refusés. Pour cela, on devra parfois diviser un RatingRange en deux parties.
C'est ce que fait la fonction suivante
haskell
splitRatings :: Test -> [RatingRange] -> ([RatingRange], [RatingRange])
A partir de là, il est relativement facile de simuler les worflow en prenant en entrée des RatingRange et de calculer le nombre total de possibilités de pièces vu que les RatingRange sont deux à deux disjoints.
Voici le code en entier.
# Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code, jour 18. Évalué à 3.
Ce problème ressemble à la partie 2 du jour 10 de cette année.
La première partie peut être aisément fait avec un parcours en largeur/profondeur mais ça devient clairement impossible pour la partie 2.
Remarquons que l'on cherche à trouver le nombre de sommets de coordonnées entières situées à l'intérieur d'un polygone.
On va donc de baser sur le théorème de Pick et la shoelace formula.
La shoelace formula est une formule pour calculer l'aire d'un polygone.
Si les sommets du polygone sont
(x1, y1), ..., (xn, yn)
alors l'aire du polygone est|x1 y2 - y1 x2 + ... + x_{n-1} yn - y_{n-1} xn + xn y1 - yn x1| / 2
Le théorème de Pick donne une formule pour calculer le nombre de points intérieurs à un polygone à partir de son aire (qui est calculé par la shoelace formula) et du nombre de points à sa frontière (c'est juste la somme des distances données par les instructions).
La formule est la suivante:
A = i + b/2 - 1
où A est l'aire, i le nombre de points à l'intérieur et b le nombre de points à la frontière.Pour calculer le nombre de "#" total, il faut faire la somme
i + b
oùi = A - b/2 - 1
.Donc, voici le code Haskell
[^] # Re: Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code, jour 17. Évalué à 1.
Moi, je suis arrivé à descendre à 300ms mais je pense pas faire beaucoup mieux.
J'ai tenté des heuristiques pour A* mais rien de concluant.
Enfin, j'en avais une bien mais elle prenait trop de temps à être calculer.
[^] # Re: Du rust pour ma part
Posté par Guillaume.B . En réponse au message Advent of Code, jour 16. Évalué à 1.
Faudrait que je m'y mettre au Rust, un jour.
[^] # Re: Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code, jour 17. Évalué à 1.
Après quelques optimisations
200ms pour la partie 1 et 800ms pour la partie 2.
L'idée est qu'au lieu de dire qu'un noeud du graphe est un couple (position, direction) avec 4 directions possibles, je dis qu'un noeud est un couple (position, booléen)
ou le booléen m'indique si je me déplace horizontalement ou verticalement.
Ca fait 2 fois moins de noeuds dans le graphe. Du coup, logiquement, ça divise le temps d'exécution par deux (et même plus avec quelques autres optimisations).
Le code qui change:
[^] # Re: Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code, jour 17. Évalué à 1.
J'ai oublié dire:
450ms pour la partie et 2s pour la partie 2.
Pas très satisfait mais je pense que c'est améliorable.
# Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code, jour 17. Évalué à 2. Dernière modification le 17 décembre 2023 à 09:19.
Je trouve que ce problème ressemble à celui d'hier.
Il s'agit encore d'un parcours dans un graphe.
Ici, le graphe est pondéré. On va donc utiliser l'algorithme de Dijkstra au lieu d'un simple parcours en profondeur/largeur.
Comme hier, la direction est importante. Les sommets du graphe seront donc les paires (Position, Direction).
Les voisins d'un sommet ne seront pas les positions adjacentes dans la grille mais celle à distance entre 1 et 3 pour la partie 1 et entre 4 et 10 pour la partie 2.
La direction devra également changer à chaque fois.
Le poids des arêtes sera la somme des chiffres indiqués sur chacune des tuiles que l'on a parcouru durant ce déplacement.
Dans mon code, j'utilise la fonction
dijkstra'
que j'ai également écrite pour des problèmes des années précédentes.Voici sa signature
Le type v est celui des sommets et le type w est celui des poids des arêtes.
La fonction prend en entrée
- une fonction de voisinage qui à chaque sommet associe une liste des sommets voisins ainsi que le poids de l'arête entre les deux sommets,
- une fonction de prédicat pour le sommet de fin,
- un ensemble de sommets de départ.
et renvoit la distance entre un des sommets de départs et un des sommets de fin si un chemin existe.
Voici le code (sans les imports)
# Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code, jour 16. Évalué à 2. Dernière modification le 16 décembre 2023 à 17:31.
C'est un problème qui peut se faire un parcours en longueur ou largeur.
Problème: selon la direction du faisceau, les cases à visiter suivantes peuvent être différentes.
Du coup un sommet du graphe que l'on veut parcourir ne sera pas seulement une position dans la grille mais un couple (position, direction).
Je commence par importer mes fonctions nécessaires et définir les types utilisés dans le problème.
Les positions et directions sont des vecteurs de dimension 2.
J'appelerai le couple (Position, Direction) est un
Beam
.Ensuite, le parsing, rien de bien intéressant
Ensuite, vient le parcours en largeur. Je réutilise une fonction
reachableFrom
définie pour des problèmes précédents.Elle prend deux arguments
- une fonction qui étant donné renvoit la liste des sommets voisins
- un sommet de départ
et renvoit l'ensemble des sommets accessibles depuis le sommet de départ.
Pour utiliser
reachableFrom
, je dois calculer, étant donné un beam, les beams suivants.Je le fais en deux temps en définissant d'abord une fonction
nextDirections
qui étant donné une direction et une tuile me renvoit les directions suivantes.A partir de ça, je peux définir ma fonction
neighbors
nécessaire à `reachableFrom
Je peux maintenant définir ma fonction
energized
qui me renvoit le nombre de tuiles énergisées en utilisant les fonctionsreachableFrom
etneighbors
définis plus haut.Pour la partie 2, c'est du brute-force sur toutes les positions de départ possibles, j'ai pas trouvé mieux.
Ca se parallélise bien, j'utilise
parMap
qui est une version parallèle demap
.1400ms sur un seul core et 800ms en multicore pour la partie 2. Pas terrible le parallélisme, j'aurais espéré mieux. Je ne me suis peut-être pas pris comme il fallait.
[^] # Re: Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code, jour 14. Évalué à 1. Dernière modification le 15 décembre 2023 à 17:25.
J'ai réécrit ma fonction
tilt
de manière plus maligne en m'inspirant de la solution d'une autre personne.Maintenant, le temps d'exécution est passé à 200ms.
# Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code, jour 15. Évalué à 2.
Problème très simple aujourd'hui. Aucune subtilité algorithmique ou autre astuce à avoir.
Juste un petit détail, j'ai pensé à utiliser, pour représenter les boites, des hashmaps avec pour clé des strings (les labels) et pour valeur des ints (les longueurs focales).
Petit problème, les HashMap de la librairie
containers
ne préservent pas l'ordre d'insertion, cet ordre étant nécessaire pour calculer le "focusingPower".J'ai donc utilisé des HashMaps d'une librairie tierce qui, elles, préservent l'ordre d'insertion.
250 microseconds pour la partie 1 et 600 microseconds pour la partie 2.
Je pense que ce serait plus performant si j'utilisais des structures de données mutables mais ce serait moins joli.
[^] # Re: Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code, jour 14. Évalué à 1.
En se rendant compte qu'un cycle est un enchainement tilt, rotation, tilt, rotation, tilt, rotation, tilt, rotation, on peut écrire le code plus simplement.
Et du coup, la fonction
tiltInDirection
ne sert plus à rien.On gagne même un peu en temps d'exécution: 400ms.
[^] # Re: Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code, jour 14. Évalué à 1.
Je viens de me rendre compte que mon code n'était pas correct tout le temps même s'il marchait bien sur l'input. En effet, il se pourrait que deux configurations identiques apparaissent mais pas dans la même position dans le cycle des directions.
Par exemple, une configuration alors que la prochaine instruction est d'aller vers l'ouest puis la même configuration alors que la prochaine instruction est d'aller au nord.
Du coup, je l'ai corrigé (il y a juste à changer la fonction part2).
Ca ne change pas le temps d'exécution.
# Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code, jour 14. Évalué à 1. Dernière modification le 14 décembre 2023 à 13:20.
Problème intéressant, il me rappelle le jour 17 de 2022.
J'ai écrit une fonction
tilt
qui fait tomber les briques vers l'ouest.Pour cela, pour chaque ligne, je split la ligne selon # et pour chaque morceau du split, je sépare les O et les "." et je reconcatène tout en mettant les O d'abord et les "." ensuite.
Ensuite j'ai une fonction
tiltInDirection
qui se ramène àtilt
en faisant des rotate et des reverse selon le cas de figure.Pour la partie 2, je génère une suite infinie cyclique de North, West, South, East
et à partir de celle ci, je génère une liste infinie des différentes configurations après avoir effectué les tilts dans la direction donnée.
Dans cette liste, je cherche les indices x et y de la même configuration (fonction
findRepetition
) et la configuration qui nous intéresse est celle à l'indicex + (1000000000 - x) mod (y - x)
.La partie 2 tourne en 600ms. Pas très satisfait mais ça reste raisonnable.
Voici le code
# Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code, jour 13. Évalué à 2.
Voici ma solution en Haskell.
Pas vraiment de difficulté, j'ai factorisé le code pour résoudre les deux parties de la même manière.
Pour les connaisseurs d'Haskell, le test
isSymetry
est lazy et s'arrête dès qu'il a détecté au moins une différence pour la partie 1 et deux différences pour la partie 2.700 microsecondes pour chacune des parties.
[^] # Re: Solution en Haskell
Posté par Guillaume.B . En réponse au message Advent of Code 2023, jour 12. Évalué à 1.
En fait, j'utilise des lazy arrays à deux dimensions (la variable
arr
).Je peux écrire un truc du genre à l'initialisation du tableau.
arr[x][y] = quelque chose avec arr[x+1][y]
.Si
arr[x+1][y]
n'est pas encore calculé, il va le faire et le stocker en mémoire dans le tableau. Sinon, il renvoit directement la valeur.Je pense que c'est similaire au cache en python, comme tu disais.