Le sol est de la lave
Ce problème prend en entrée une grille composées de différentes tuiles:
- la tuile vide ("."),
- les mirroirs ("/" et "\")
- et les diviseurs ("|" and "-").
Par exemple, on a la grille suivante.
.|...\....
|.-.\.....
.....|-...
........|.
..........
.........\
..../.\\..
.-.-/..|..
.|....-|.\
..//.|....
Dans la partie 1, un faisceau de lumière commence du bord en haut à gauche et se dirige vers la droite.
- Lorsque le faisceau rencontre une tuile vide (.), il continue sa direction.
- Lorsqu'il rencontre un miroir (/ou \), il change de direction en fonction du sens du miroir.
- Lorsqu'il rencontre un diviseur (-) alors qu'il venait de la gauche ou de la droite alors le faisceau est divisé en deux, un partant vers le haut et un partant vers le bas. Sinon, il continue sa direction comme si c'était une tuile vide.
- Même chose pour le diviseur (|): si le faisceau le rencontre alors qu'il venait du haut ou du bas, alors le faisceau est divisé en deux, un partant vers la gauche et un partant vers la droite.
Le but de la partie 1 est de déterminer combien de tuiles seront énergisées, c'est à dire traversées par le faisceau.
Dans la partie 2, le faisceau peut partir de n'importe quel bord et le but est de trouver la position et direction de départ qui maximise le nombre de tuiles traversées par le faisceau.
# Solution en Haskell
Posté par Guillaume.B . É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 Yth (Mastodon) . Évalué à 2.
J'ai encore codé en marchant, sur mon téléphone.
D'ailleurs, pour les masochistes dans mon genre, je conseille : Qpython, on ne le trouve pas sur fdroid, je ne l'ai pas non plus trouvé sur Aurora, mais on peut installer l'apk depuis github. Il ne demande aucune autorisation, mais ne fonctionnera pas sans un accès aux fichiers, qu'on doit activer soi-même : il installe des trucs dans un répertoire accessible ([userdata]/qpython/).
C'est bien moins casse-pied qu'un éditeur en ligne, plus rapide, et on peut partager facilement les fichiers avec un PC par exemple avec Syncthing
Trêve de publicités, ce genre d'algo, à débugger comme un brontosaure, sur un téléphone, c'est galère, et je n'ai vraiment pu le finir qu'une fois devant un vrai clavier, mais j'étais pas loin !
Par contre, pas lourd d'optimisations, j'avais pas le temps d'y réfléchir, par chance le code tourne malgré tout en 3,5 secondes, donc ça va.
Après trois secondes de réflexion, je me suis dis qu'un cache serait inutile, en pratique un cache n'est pas entièrement inutile, et me fait redescendre à 1,95s, sachant qu'il est assez crétinement utilisé.
Bref, voici du code :
Donc l'idée est un peu la même : on stocke des couples (position, direction), une coordonnée est True si elle n'est pas hors champs, on peut additionner deux coordonnées, les directions sont des coordonnées qui se représentent avec une autre valeur à définir soi-même, et j'ai donc quatre instances N, S, E et W, qui vont simplement s'afficher en N, S, E et W lors des débuggages.
La fonction energize prend un rayon de départ (position, direction), et retourne le nombre de cases allumées, donc c'est la résolution d'un problème n°1, on en a 440 à résoudre pour le n°2.
Après c'est un poil du brute force : on teste vraiment toutes les positions de départ possible.
Et le cache est sur un ensemble de rayons (couple position/direction), donc il n'y a pas tellement de chances de retomber exactement sur le même ensemble (en fait c'est pas si mal), et ça n'optimise pas du tout sur un trajet qu'on retrouverait dans pleins de parcours.
Mais ça divise le temps quasiment par deux, donc ce n'est pas entièrement inutile !
Une meilleure approche serait une fonction qui retourne tous les rayons générés à partir d'un rayon, et là, on sait qu'on va repasser par les mêmes endroits lors de chaque exploration, rayon par rayon quand il y a bifurcation, et ça peut accélérer énormément.
Le terrain faisant 110*110, soit 12100 cases, donc 48400 rayons possibles, la plus grosse exploration en générant déjà 12246 chez moi, on a rapidement fait le tour quand même, et ça peut certainement aller plus vite au bout du compte.
En pratique, ma fonction
move
est appelée 167654 fois sans cache et 61561 fois avec cache, donc entre 48200 et 61561, ya pas si lourd à gagner que ça. Allez, je passerais allègrement sous la seconde et voilà, rien de plus. Peut-être que l'implémentation réelle montrerait que pas mal de rayons n'existent pas (sont inaccessibles depuis le bord), et qu'on pourrait vraiment y gagner.Après, c'est assez marrant d'utiliser un cache sur une fonction qui yield, il doit cacher un générateur, ou peut-être toutes les valeurs, sinon ça cache pas, puisqu'un générateur c'est transitoire. Franchement je ne sais pas comment il se débrouille en interne, mais ça fonctionne !
# Du rust pour ma part
Posté par YetiBarBar . Évalué à 1. Dernière modification le 16 décembre 2023 à 23:00.
Mon code est par ici
Rien de bien inventif et assez brutal: on propage le rayon lumineux comme indiqué dans l'énoncé jusqu'à sa sortie du tableau, en mémorisant les couples (x, y, direction de propagation) pour éviter de tourner en rond. Un algo très ressemblant à un bfs…
Pour la partie 2, du "brute-force" basique en testant toutes les entrées possibles.
Sans multithread, ça tourne en 620ms (parsing, partie 1 et 2) sur un seul core de mon Core i7 de 10 ans d'âge.
[^] # Re: Du rust pour ma part
Posté par Guillaume.B . Évalué à 1.
Faudrait que je m'y mettre au Rust, un jour.
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.