🐜 Lem-in C — Guide Complet
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.
▶️ Lancer le Visualiseur Interactif📋 Sommaire
- Structures de Données
- Parsing et Validation
- Algorithmes (BFS, Flux, Optimisation)
- Simulation des Fourmis
- Fonctions Principales
- Exemple Complet
- Complexité
- Compilation & Build WASM
- Pièges Courants
🏗️ Structures de Données
Le projet utilise 5 structures principales, définies dans include/graph.h, include/farm.h et include/solve.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 (ex: "1", "A", "start")
t_coord c; // Coordonnées (x, y)
int ant; // ID de la fourmi actuellement dans la salle (0 si vide)
int start; // 1 si c'est le point de départ, 0 sinon
int end; // 1 si c'est le point d'arrivée, 0 sinon
t_list *lnks; // Liste chaînée des salles connectées (voisins)
struct s_room *f_to; // Salle suivante dans le flux de fourmis (forward)
struct s_room *f_fm; // Salle précédente dans le flux (from)
struct s_room *next; // Salle suivante dans la liste chaînée 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 permettre le backtracking.
t_farm — Conteneur Principal
typedef struct s_farm {
int ants; // Nombre total de fourmis à déplacer
t_room *rms; // Pointeur vers la première salle (liste chaînée)
} t_farm;
t_bfs — Pour la Résolution
typedef struct s_bfs {
int path_size; // Longueur du chemin depuis le départ
int send_ants; // Nombre de fourmis à envoyer sur ce chemin (temporaire)
int min_ants; // Nombre optimal de fourmis calculé (final)
t_room **room; // Pointeur vers le pointeur de salle (double indirection)
struct s_bfs *prev; // Noeud précédent dans le chemin BFS (backtracking)
struct s_bfs *next; // Noeud suivant dans la file BFS
} t_bfs;
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)
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 positif |
| 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 (io = 0)##end: Marque la prochaine salle comme arrivée (io = 1)#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 numériques - 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())
Lecture de l'input
La lecture se fait via get_next_line2(), une version custom de get_next_line avec un buffer statique pitcher qui permet 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é
│
└── 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 noeud courant, il explore tous les voisins :
- Ignore si déjà visité (
room_in()) - Ignore si "contre-flux" (
flow_to()) — empêche d'utiliser un tunnel dans le sens inverse - Gère le mode undo : si le noeud 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
flow_to(from, to) :
- 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
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
prv->f_to = r; // marquer le forward
}
// Annuler les conflits :
if (r->f_to == prv) r->f_to = NULL;
if (prv->f_fm == r) prv->f_fm = NULL;
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 toutes les combinaisons de chemins (1, 2, ..., N chemins) et garde la configuration optimale.
Formule : Tours = population + longest_path_size
La distribution se calcule via populate_path() :
nxt->send_ants = aux->send_ants - 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); // 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
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 vers is_room/is_link |
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 |
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 séquentiellement.
⏱️ Complexité
- BFS : O(V + E) par appel, où V = nombre de salles, E = nombre de liens. Le BFS est appelé plusieurs fois (une par chemin trouvé).
- Update flow : O(L) où L = longueur du chemin trouvé
- Path config : O(P² × A) où P = nombre de chemins, A = nombre de fourmis (incrémentation de population)
- 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 WASM
Build native
make # compile lem-in + libft ./lem-in < map.txt ./lem-in --d < map.txt # debug : nombre de lignes ./lem-in --dp < map.txt # debug avancé : infos nœuds + flux
Build WebAssembly (Emscripten)
make web # nécessite emsdk installé # génère docs/lem-in.js + docs/lem-in.wasm
--d: affiche le nombre de lignes en sortie--dp: affiche les informations détaillées des nœuds et le flux (BFS paths, chemins extraits, nombre de tours)
⚠️ 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) vspush(FIFO) — 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)
Projet Lem-in C — 42 School | Lancer le visualiseur | Voir sur GitHub