lem-in · 42 // TECHNICAL GUIDE
SYS:ONLINE REC // 11
Projet 42 · Branche Algorithme · Graph Theory

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.

Difficulté
★★★★☆
Temps estimé
30 – 60 h
Algorithme
BFS + Edmonds-Karp
Sortie
lem-in + WASM
▶ Lancer le Visualiseur Interactif
// Contraintes clés

Une 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.

« Les fourmis ne se cognent jamais — pas par hasard, mais parce que chaque tunnel a un sens unique, et chaque salle ne contient qu'une seule fourmi à la fois. C'est la logique du flot, pas du chaos. »
— Manuel de terrain YoRHa, module 042
02

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

graph.h// t_coord
typedef struct s_coord
{
	int	x;
	int	y;
}				t_coord;

t_room — La Structure Centrale

graph.h// t_room
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;
// Champs critiques pour la résolution
  • 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. Pour end, c'est un compteur cumulatif d'arrivées.
  • lnks : Graphe d'adjacence pour le parcours BFS. Stocke des t_room ** (double pointeur) pour partager les références de salles avec la liste globale.

t_farm — Conteneur Principal

farm.h// t_farm
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

solve.h// t_bfs
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;
// Pourquoi t_room ** (double pointeur) ?

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)

libft.h// t_list
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.

03

Parsing et validation

Le parsing utilise une machine à états finis (FSM) à 3 sections, gérée par fill_farm() et is_cmd() :

SectionContenuFormat
0Nombre de fourmisEntier naturel (positif ou nul)
1Sallesnom x y (3 tokens séparés par espaces)
2Liensroom1-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 L ou #
  • Les coordonnées x et y doivent être des entiers naturels (chiffres uniquement, pas de signe négatif)
  • Noms de salles uniques (is_name())
  • Coordonnées uniques (is_coords())
  • Exactement un ##start et un ##end (repeated_endpoints())
  • Les liens doivent connecter deux salles existantes et différentes
  • Pas de doublons de liens (is_linked() dans add_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.

04

Algorithmes

1. Vue d'ensemble du flux de résolution

solve.c// flux
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 mode undo
  • Gère le mode undo : si le nœud courant a un f_to qui ne pointe pas vers son parent, on est en mode "undo" — seul le voisin f_fm est autorisé (reverse edge d'Edmonds-Karp)
  • Insère dans la file via insert_bfs() (FIFO)
  • Si le voisin est end → chemin trouvé, retourne 1
// Conditions de flow_to(from, to)
  • Si from est le start : l'arête est "utilisée" si to->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 :

flow.c// update_flow (pseudo-code)
// 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;
// Gestion des conflits

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.

// Formule clé

Tours = population + shortest_path_size

La distribution se calcule via populate_path() :

path_computation.c// 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.

05

Simulation des fourmis

La simulation se fait tour par tour dans send_ants() :

flow.c// 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 ant actuel (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émente end->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_ants du chemin
  • Place une nouvelle fourmi (ID = ants - farm->ants + 1) dans la première salle
  • Décrémente le compteur global farm->ants
// Outil pédagogique

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.

// Évitement de collision

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.

06

Fonctions principales

FonctionFichierDescription
main()lem_in.cPoint d'entrée : parse flags, init farm, solve, cleanup
fill_farm()lem_in.cFSM 3 sections : ants → rooms → links
is_cmd()lem_in.cDispatche chaque ligne (commentaires ignorés, add_ants en section 0, is_room en section 1, is_link en section 2)
is_room()valid_input.cValide le format salle (nom x y) + unicité
is_link()valid_input.cValide le format lien (room1-room2) + existence
add_link()list.cCrée un lien bidirectionnel entre deux salles
solve()solve.cOrchestration : BFS → update_flow → send_ants
bfs()bfs.cBFS récursif avec flow_to + undo (Edmonds-Karp)
update_flow()flow.cMarque f_to/f_fm + annule les conflits
path_list()flow_treatment.cExtrait les chemins via f_to depuis les voisins de start
path_config()path_computation.cCalcule min_ants par chemin (optimisation)
populate_path()path_computation.cDistribution : send_ants[i+1] = send_ants[i] - abs(diff)
send_ants()flow.cBoucle de simulation tour par tour
move_ants()flow.cAppelle push_ants sur chaque chemin pour avancer toutes les fourmis
push_ants()flow.cDécale les fourmis d'une case sur un chemin
select_paths()flow.cInjecte de nouvelles fourmis depuis start
07

Exemple complet

Input :

map.txt// exemple
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) :

sortie// 6 tours
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.

08

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
09

Compilation & build

Build native

shell// compilation
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)

shell// WASM
make web    # nécessite emsdk installé
# génère docs/lem-in.js + docs/lem-in.wasm
// Flags de debug
  • --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)
10

Pièges courants

  • Oublier de libérer la mémoire (fuites — chaque malloc doit avoir son free)
  • Ne pas gérer les commentaires # (lignes à ignorer sauf ##start/##end)
  • Accepter plusieurs ##start ou ##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) vs insert_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)
« Un algorithme de flot ne se prouve pas par ses succès — il se prouve par les collisions qu'il évite. Chaque tunnel à sens unique est la cicatrice d'un bug qu'on a appris à anticiper. »
— 9S, analyse post-mission
11

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.

// Le malentendu fondamental

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.

// Notre solution

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.

// Notre solution

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.

// Notre solution

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.

// Notre solution

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()).

// Notre solution

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.

// Notre solution

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 modeSymptômeNotre solution
Graphe déconnectéSimulation sans fin ou crashbfs() retourne 0 → sortie propre
Salles auto-liéesBFS infini ou chemins circulairesis_link() rejette les liens same-room
Liens vers salles inexistantesSegfault (pointeur NULL)is_link() + is_name() valident l'existence
Salles dupliquéesGraphe ambigu, BFS incorrectis_name() + is_coords() vérifient l'unicité
Pas de start/endNULL dereference dans solve()endpoints() valide avant résolution
Fourmis invalidesOverflow, boucle infinie, malloc incorrectis_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é.

// Dinic vs Edmonds-Karp

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.
// Outil pédagogique

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.

// Checklist anti-échec

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.

« Un algorithme de flot ne se prouve pas par ses succès — il se prouve par les chemins qu'il ne trouve pas, et par la manière dont il échoue proprement quand le graphe est vide. »
— 9S, analyse post-mission