lem-in
// Faire passer N fourmis de start à end dans le minimum de tours — BFS, flot maximum, Edmonds-Karp.
Lem-in est un projet de la 42 School qui consiste à faire passer un nombre donné de fourmis d'un point de départ à un point d'arrivée dans le minimum de tours, en évitant les collisions. Le projet utilise un algorithme de BFS (Breadth-First Search) combiné à une gestion de flot maximum inspirée d'Edmonds-Karp. Ce guide décortique chaque structure, chaque fonction, chaque piège — avec le code réel, des diagrammes, et l'exemple complet tracé pas à pas.
▶ Lancer le Visualiseur InteractifUne seule salle = une fourmi à la fois. Le nombre de tours doit être minimal. Le
programme doit gérer les commentaires (#), les commandes
spéciales (##start/##end), et
valider rigoureusement l'input. Aucune fuite mémoire autorisée — chaque
malloc doit avoir son free.
— Manuel de terrain YoRHa, module 042
Structures de données
Le projet utilise 5 structures principales, définies dans
include/graph.h, include/farm.h,
include/solve.h et libft/libft.h.
t_coord — Coordonnées 2D
typedef struct s_coord
{
int x;
int y;
} t_coord;
t_room — La Structure Centrale
typedef struct s_room
{
char *name; // Nom de la salle
t_coord c; // Coordonnées (x, y)
int ant; // ID de la fourmi (0 si vide)
int start; // 1 si départ
int end; // 1 si arrivée
t_list *lnks; // Liste chaînée des salles connectées
struct s_room *f_to; // Salle suivante dans le flux (forward)
struct s_room *f_fm; // Salle précédente dans le flux (from)
struct s_room *next; // Salle suivante dans la liste globale
} t_room;
f_to/f_fm: Créent un graphe résiduel — des "tunnels" virtuels pour le flux de fourmis après BFS. Inspiré d'Edmonds-Karp.ant: Permet de suivre quelle fourmi est où à chaque tour. Pourend, c'est un compteur cumulatif d'arrivées.lnks: Graphe d'adjacence pour le parcours BFS. Stocke dest_room **(double pointeur) pour partager les références de salles avec la liste globale.
t_farm — Conteneur Principal
typedef struct s_farm
{
int ants; // Nombre total de fourmis
t_room *rms; // Pointeur vers la première salle
} t_farm;
t_bfs — Pour la Résolution
typedef struct s_bfs
{
int path_size; // Nombre d'arêtes du voisin de start jusqu'à end
int send_ants; // Fourmis à envoyer (temporaire)
int min_ants; // Fourmis optimales (final)
t_room **room; // Double pointeur vers la salle
struct s_bfs *prev; // Nœud précédent (backtracking)
struct s_bfs *next; // Nœud suivant dans la file BFS
} t_bfs;
Pour pouvoir modifier les références de salles lors du backtracking et de la mise à jour du flux. C'est une technique nécessaire en C qui n'a pas de références natives.
t_list — Liste Chaînée Générique (libft)
typedef struct s_list
{
void *obj; // Pointeur vers l'objet stocké
size_t dim; // Taille de l'objet
struct s_list *next;
} t_list;
Utilisée pour stocker les liens entre salles dans t_room.lnks.
Les fonctions ft_lstadd (ajout en tête, LIFO) et
ft_lstnew2 sont de la libft personnalisée.
Parsing et validation
Le parsing utilise une machine à états finis (FSM) à 3 sections, gérée
par fill_farm() et is_cmd() :
| Section | Contenu | Format |
|---|---|---|
| 0 | Nombre de fourmis | Entier naturel (positif ou nul) |
| 1 | Salles | nom x y (3 tokens séparés par espaces) |
| 2 | Liens | room1-room2 (un seul -) |
Commandes spéciales
##start: Marque la prochaine salle comme départ##end: Marque la prochaine salle comme arrivée#commentaire: Ligne ignorée (tout ce qui commence par#sauf##start/##end)
Règles de validation
- Le nom d'une salle ne doit pas commencer par
Lou# - Les coordonnées
xetydoivent être des entiers naturels (chiffres uniquement, pas de signe négatif) - Noms de salles uniques (
is_name()) - Coordonnées uniques (
is_coords()) - Exactement un
##startet un##end(repeated_endpoints()) - Les liens doivent connecter deux salles existantes et différentes
- Pas de doublons de liens (
is_linked()dansadd_link— les doublons sont silencieusement ignorés, non rejetés)
Lecture de l'input
La lecture se fait via get_next_line2(), une version custom
de get_next_line prenant un buffer (pitcher) en paramètre,
statique côté fill_farm, permettant la lecture ligne par
ligne depuis stdin.
Algorithmes
1. Vue d'ensemble du flux de résolution
solve()
│
├── endpoints() : valider 1 start + 1 end
│
├── BOUCLE de découverte de chemins :
│ ├── bfs() : BFS récursif pour trouver un chemin start → end
│ ├── update_flow() : marquer f_to/f_fm sur le chemin trouvé
│ └── répéter jusqu'à ce qu'aucun chemin ne soit trouvé (ou ants ≤ 1)
│ └── (si 1er bfs échoue → ERROR sur stderr + return, pas de send_ants)
│
└── send_ants() : simulation tour par tour
├── path_list() : extraire les chemins via f_to
├── path_config() : optimiser la distribution des fourmis
└── boucle de simulation (move_ants + select_paths)
2. BFS (Breadth-First Search) — bfs()
Le BFS est récursif et utilise une file chaînée FIFO. Pour chaque nœud courant, il explore tous les voisins :
- Ignore si déjà visité (
room_in()) - Ignore si l'arête est déjà saturée dans le flux (
flow_to()) — empêche de réutiliser un tunnel déjà pris dans le sens forward ; les arêtes inverses restent disponibles via le modeundo - Gère le mode undo : si le nœud courant a un
f_toqui ne pointe pas vers son parent, on est en mode "undo" — seul le voisinf_fmest autorisé (reverse edge d'Edmonds-Karp) - Insère dans la file via
insert_bfs()(FIFO) - Si le voisin est end → chemin trouvé, retourne 1
- Si
fromest le start : l'arête est "utilisée" sito->f_fm == from - Sinon : l'arête est "utilisée" si
from->f_to == to
Cela empêche de réutiliser un flux existant. En mode undo, l'arête inverse est naturellement disponible car flow_to renvoie faux (le flux forward va dans l'autre sens).
3. Mise à jour du flux — update_flow()
Après un BFS réussi, on remonte depuis end vers start via les pointeurs
prev :
// Pour chaque paire (prv, r) sur le chemin :
if (!(r->f_to == prv || prv->f_fm == r)) {
r->f_fm = prv; // marquer le backward (sauf start/end)
prv->f_to = r; // marquer le forward (sauf start comme prv)
}
// Annuler les conflits :
if (r->f_to == prv) r->f_to = NULL;
if (prv->f_fm == r) prv->f_fm = NULL;
Si un tunnel existait déjà dans l'autre sens, il est annulé
(f_to = NULL ou f_fm = NULL).
C'est ce qui permet à l'algorithme de "défaire" un flux précédent si un meilleur chemin
est trouvé — c'est le cœur d'Edmonds-Karp.
4. Extraction des chemins — path_list()
Après la boucle de découverte, on extrait tous les chemins valides en parcourant les
voisins du start et en suivant f_to
jusqu'à end. Les chemins sont triés par longueur (plus court
en tête) via insertio() (insertion triée par
path_size).
5. Optimisation du nombre de tours — path_config()
Calcule combien de fourmis envoyer sur chaque chemin pour minimiser le nombre total de tours. Teste tous les préfixes de la liste triée (1, 2, ..., N chemins les plus courts) et garde la configuration optimale.
Tours = population + shortest_path_size
La distribution se calcule via populate_path() :
nxt->send_ants = aux->send_ants - ft_abs(nxt->path_size - aux->path_size);
Les chemins plus longs reçoivent moins de fourmis car ils "occupent" le pipeline plus
longtemps. Le résultat est stocké dans min_ants de chaque chemin.
Simulation des fourmis
La simulation se fait tour par tour dans send_ants() :
while (end->ant < ants)
{
turns++;
entro = move_ants(&paths); // faire avancer toutes les fourmis
entro += select_paths(&paths, farm, ants); // injecter de nouvelles fourmis
ft_putendl(""); // nouvelle ligne = nouveau tour
if (!entro) break; // plus aucun mouvement possible
}
push_ants() — Déplacement sur un chemin
Fait avancer les fourmis d'une case sur un chemin, en parcourant via f_to :
- Sauvegarde
antactuel (aux_ant) - Met
ant = prev_ant(la fourmi précédente prend cette place) prev_ant = aux_ant(pour la prochaine itération)- Si une fourmi arrive à
end→ incrémenteend->ant
select_paths() — Injection de nouvelles fourmis
Pour chaque chemin avec min_ants > 0 et s'il reste des fourmis à envoyer :
- Décrémente
min_antsdu chemin - Place une nouvelle fourmi (ID =
ants - farm->ants + 1) dans la première salle - Décrémente le compteur global
farm->ants
Notre implémentation inclut un visualiseur WASM interactif qui permet de suivre visuellement le déplacement des fourmis tour par tour — un outil de débogage et de présentation indispensable, mais distinct des optimisations algorithmiques ci-dessus.
Les fourmis ne se cognent jamais car chaque salle a au plus une fourmi
(ant), le mouvement est synchronisé (toutes bougent en même
temps), et les tunnels f_to garantissent un sens unique.
Fonctions principales
| Fonction | Fichier | Description |
|---|---|---|
main() | lem_in.c | Point d'entrée : parse flags, init farm, solve, cleanup |
fill_farm() | lem_in.c | FSM 3 sections : ants → rooms → links |
is_cmd() | lem_in.c | Dispatche chaque ligne (commentaires ignorés, add_ants en section 0, is_room en section 1, is_link en section 2) |
is_room() | valid_input.c | Valide le format salle (nom x y) + unicité |
is_link() | valid_input.c | Valide le format lien (room1-room2) + existence |
add_link() | list.c | Crée un lien bidirectionnel entre deux salles |
solve() | solve.c | Orchestration : BFS → update_flow → send_ants |
bfs() | bfs.c | BFS récursif avec flow_to + undo (Edmonds-Karp) |
update_flow() | flow.c | Marque f_to/f_fm + annule les conflits |
path_list() | flow_treatment.c | Extrait les chemins via f_to depuis les voisins de start |
path_config() | path_computation.c | Calcule min_ants par chemin (optimisation) |
populate_path() | path_computation.c | Distribution : send_ants[i+1] = send_ants[i] - abs(diff) |
send_ants() | flow.c | Boucle de simulation tour par tour |
move_ants() | flow.c | Appelle push_ants sur chaque chemin pour avancer toutes les fourmis |
push_ants() | flow.c | Décale les fourmis d'une case sur un chemin |
select_paths() | flow.c | Injecte de nouvelles fourmis depuis start |
Exemple complet
Input :
4 ##start 1 0 0 2 1 0 3 0 1 4 1 1 ##end 5 2 0 1-2 1-3 2-4 3-4 4-5
Output (vérifié avec le binaire) :
L1-3 L1-4 L2-3 L2-4 L1-5 L3-3 L3-4 L2-5 L4-3 L4-4 L3-5 L4-5
6 tours — Toutes les fourmis prennent le même chemin
1→3→4→5 (ou 1→2→4→5, qui est
équivalent). Comme les deux chemins partagent le nœud 4,
ils ne sont pas disjoints en sommets. Par conséquent, l'algorithme d'Edmonds-Karp ne
conserve qu'un seul chemin pour éviter les collisions sur le nœud
4, et les 4 fourmis y sont envoyées une par tour, en pipeline.
Complexité
- BFS : O(V × E) par appel (room_in est O(V), appelé pour chaque arête). Le BFS est appelé plusieurs fois (au plus P fois, où P = nombre de chemins trouvés). Soit O(P × V × E) ≤ O(V² × E) pour la phase de découverte complète.
- Update flow : O(V + L) où V = taille de la file BFS, L = longueur du chemin (recherche d'end + backtracking)
- Path config : O(P² × A) où P = nombre de chemins, A = nombre de fourmis
- Simulation : O(T × P × L) où T = nombre de tours, P = nombre de chemins, L = longueur moyenne
- Mémoire : O(V + E) pour le graphe + O(P × L) pour les chemins
Compilation & build
Build native
make # compile lem-in + libft ./lem-in < map.txt ./lem-in --d < map.txt # debug : chemins BFS + tours ./lem-in --dp < map.txt # debug avancé : + infos nœuds
Build WebAssembly (Emscripten)
make web # nécessite emsdk installé # génère docs/lem-in.js + docs/lem-in.wasm
--d: affiche les chemins BFS, les chemins extraits et le nombre de tours--dp: idem que --d, plus les informations détaillées des nœuds (print_farm)
Pièges courants
- Oublier de libérer la mémoire (fuites — chaque
mallocdoit avoir sonfree) - Ne pas gérer les commentaires
#(lignes à ignorer sauf##start/##end) - Accepter plusieurs
##startou##end - Laisser des fourmis se cogner (une salle = une fourmi à la fois)
- Ne pas gérer les chemins de longueurs différentes (sans
path_config, la distribution est sous-optimale) - Oublier d'annuler les conflits dans
update_flow(tunnels inverses non nettoyés → chemins invalides) - Utiliser
ft_lstadd(LIFO pour les liens) vsinsert_bfs(FIFO pour la file BFS) — l'ordre des voisins affecte le parcours BFS et donc le résultat - Ne pas valider l'overflow d'entiers (
exceeds_int()pour le nombre de fourmis)
— 9S, analyse post-mission
Diagnostic : ce qui se passe quand ça échoue
Notre implémentation réussit tous les tests, mais comprendre pourquoi elle réussit exige de comprendre ce qui se passe quand un lem-in échoue. Cette section analyse les failure modes classiques d'après un diagnostic architectural réel. Chaque échec révèle une faiblesse conceptuelle précise — et chaque feature de notre implémentation existe pour l'éviter.
Beaucoup d'étudiants pensent que lem-in est un problème de plus court
chemin (Dijkstra, A*). C'est faux. C'est un problème de flot
maximum dans un réseau de graphes. L'objectif n'est pas d'amener une seule
fourmi à destination, mais d'orchestrer le passage collectif de N fourmis simultanément
en évitant les collisions et en minimisant le nombre total de tours. Cette distinction
détermine tout : le choix de l'algorithme (Edmonds-Karp, pas Dijkstra), la structure
des données (graphe résiduel avec f_to/f_fm),
et la stratégie de simulation (pipeline multi-chemins, pas séquentiel).
Failure mode 1 : pas de chemin entre start et end
Si le graphe est déconnecté (start et end dans des composantes connexes distinctes),
aucune fourmi ne peut atteindre sa destination. Un programme robuste doit le détecter
après le premier BFS : si end n'a pas été
atteint, il faut conclure qu'il n'existe aucune solution et le signaler — plutôt que de
démarrer un processus de simulation qui aboutira à une impasse.
bfs() retourne 0 si aucun chemin n'est trouvé. La boucle
dans solve() s'arrête, et si aucun chemin n'a été extrait,
le programme sort proprement en émettant ERROR:Unreachable endpoint sur stderr (sans simulation).
Failure mode 2 : salles auto-liées (RoomA-RoomA)
Un lien RoomA-RoomA est syntaxiquement valide mais introduit
un cycle de longueur 1 qui peut perturber les algorithmes de parcours (BFS infini dans
certains cas). Un programme robuste doit détecter et rejeter ces liens pendant le parsing.
add_link() dans list.c vérifie
que les deux salles du lien sont différentes avant de créer la connexion.
Failure mode 3 : liens vers des salles inexistantes
Un lien faisant référence à une salle qui n'a jamais été déclarée est une erreur critique. Ignorer cette vérification mène à des pointeurs invalides (segfault) ou à un comportement indéfini quand l'algorithme tente de suivre le lien.
is_link() utilise find_name()
pour vérifier que les deux salles existent dans la liste globale avant d'appeler
add_link().
Failure mode 4 : duplication de salles
Définir la même salle deux fois (même nom ou mêmes coordonnées) crée une ambiguïté dans le graphe. Le BFS peut boucler ou produire des chemins invalides.
is_room() appelle is_name()
(unicité du nom) et is_coords() (unicité des coordonnées)
avant d'ajouter une salle.
Failure mode 5 : pas de start ou pas de end
Sans ##start ou ##end, le problème
est insoluble. Un programme qui ne valide pas cette condition aura un comportement
imprévisible (pointeur NULL déréférencé dans solve()).
endpoints() dans valid_solution.c
vérifie qu'exactement un start et un end
existent avant de lancer la résolution.
Failure mode 6 : nombre de fourmis invalide
Un nombre de fourmis nul, négatif, ou non numérique (dépassement d'INT_MAX)
doit être détecté dès le parsing. Ignorer cette vérification conduit à des allocations
mémoire incorrectes ou des boucles infinies.
is_num() valide que la chaîne ne contient que des digits.
exceeds_int() vérifie l'overflow avant conversion. La section
0 de la FSM refuse tout ce qui n'est pas un entier naturel valide.
Le tableau récapitulatif
| Failure mode | Symptôme | Notre solution |
|---|---|---|
| Graphe déconnecté | Simulation sans fin ou crash | bfs() retourne 0 → sortie propre |
| Salles auto-liées | BFS infini ou chemins circulaires | is_link() rejette les liens same-room |
| Liens vers salles inexistantes | Segfault (pointeur NULL) | is_link() + is_name() valident l'existence |
| Salles dupliquées | Graphe ambigu, BFS incorrect | is_name() + is_coords() vérifient l'unicité |
| Pas de start/end | NULL dereference dans solve() | endpoints() valide avant résolution |
| Fourmis invalides | Overflow, boucle infinie, malloc incorrect | is_num() + exceeds_int() au parsing |
Pourquoi Edmonds-Karp et pas Dijkstra
Dijkstra trouve le plus court chemin d'un nœud à un autre. Mais lem-in demande de faire passer N fourmis simultanément — pas une par une. Si on utilisait Dijkstra, on trouverait un seul chemin optimal, puis on y enverrait toutes les fourmis l'une après l'autre. C'est sous-optimal : si le graphe a plusieurs chemins disjoints, on devrait les utiliser en parallèle pour réduire le nombre total de tours.
Edmonds-Karp (BFS + flot) trouve itérativement des chemins augmentants
— des chemins qui ne partagent pas d'arêtes avec les chemins déjà trouvés (ou qui
"défont" des arêtes existantes via le graphe résiduel). C'est exactement ce dont on a
besoin : un ensemble de chemins disjoints ou quasi-disjoints pour router les fourmis en
parallèle. Notre implémentation utilise f_to/f_fm
comme graphe résiduel, et le mode "undo" du BFS permet de corriger un chemin précédent
si un meilleur ensemble est trouvé.
L'algorithme de Dinic est plus performant qu'Edmonds-Karp pour les graphes denses (O(V²E) vs O(VE²)). Pour les maps typiques de lem-in (quelques centaines de salles), Edmonds-Karp suffit largement. Dinic devient intéressant pour des graphes de plusieurs milliers de nœuds — au-delà du scope du sujet 42.
Stratégies d'optimisation avancées (aller plus loin)
Au-delà de notre implémentation, plusieurs optimisations sont envisageables pour réduire encore le nombre de tours :
- Heuristiques de séquencement : envoyer d'abord les fourmis qui ont le plus long chemin à parcourir, pour libérer les passages communs plus rapidement. Inspiré du Multi-Agent Path Finding (MAPF).
- Optimisation par Colonie de Fourmis (ACO) : les fourmis laissent des "phéromones" sur les arêtes qu'elles empruntent, rendant ces arêtes plus attractives pour les fourmis suivantes. Méta-heuristique inspirée de la nature, démontrant une compréhension des paradigmes d'optimisation bio-inspirés.
- Parallélisation : calculer plusieurs chemins augmentants simultanément (Edmonds-Karp parallèle). Chaque fourmi pourrait être gérée par un thread distinct, avec synchronisation sur les passages partagés.
- Visualiseur graphique : notre implémentation inclut déjà un visualiseur WASM interactif (voir le visualiseur), qui permet de suivre visuellement le déplacement des fourmis tour par tour — un outil pédagogique et de débogage indispensable.
Notre implémentation inclut un visualiseur WASM interactif qui permet de suivre visuellement le déplacement des fourmis tour par tour — un outil de débogage et de présentation indispensable, mais distinct des optimisations algorithmiques ci-dessus.
Si votre lem-in échoue, voici l'ordre de diagnostic recommandé :
(1) Le parsing fonctionne-t-il ? — testez avec une map simple, vérifiez
que les salles et liens sont correctement stockés.
(2) Le BFS trouve-t-il un chemin ? — activez --d
pour voir les chemins BFS. Si aucun chemin n'est trouvé, vérifiez que le graphe est
connecté.
(3) La simulation génère-t-elle des collisions ? — vérifiez que
f_to/f_fm sont correctement
positionnés (mode undo d'Edmonds-Karp).
(4) Le nombre de tours est-il optimal ? — vérifiez
path_config() : la formule
Tours = population + shortest_path_size doit donner le
même résultat que la simulation.
(5) Y a-t-il des fuites mémoire ? — chaque
malloc (salles, liens, file BFS) doit avoir un
free correspondant.
— 9S, analyse post-mission