Skip to content
ccheerryPublic

About

Program that parses an ant-farm graph and routes every ant from start to end in the fewest possible turns using max-flow on a node-split graph.

Resources

Code of conduct

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

35 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

lem-in

πŸ“– About

"lem-in" is an algorithm project written in C: a digital ant farm. The program reads a colony on standard input β€” a number of ants, a set of rooms and the tunnels joining them β€” and prints, turn by turn, the movements that take every ant from ##start to ##end in as few turns as possible.

The interesting part is not walking the graph, it is that a room can hold only one ant at a time (except ##start and ##end), so the ants have to be spread over several routes that share no room. The program therefore does not look for one shortest path: it turns the colony into a flow network, splits every room in two to enforce the one-ant rule, runs a min-cost max-flow pass to obtain the shortest family of room-disjoint routes, then decides how many of those routes are worth using and how many ants each one carries.

Everything is built with the standard C toolchain and a local libft; there are no external dependencies. Input is read from stdin in one pass, and the simulation is streamed to stdout through a fixed-size buffer. The bonus is a visualizer written in Python with tkinter β€” Python's own GUI toolkit, so there is nothing to pip-install β€” that replays the simulation in a window and re-checks it against the rules of the subject.

🎯 Objectives

  • Parsing an untrusted text format and rejecting every malformed colony
  • Modelling rooms and tunnels as a graph with adjacency lists
  • Enforcing a per-room capacity with node splitting (in / out pair)
  • Implementing min-cost max flow on the resulting residual graph
  • Decomposing the flow into concrete room-disjoint routes
  • Choosing the route count that minimises the number of turns
  • Distributing the ants so the last one arrives as early as possible
  • Emitting the simulation efficiently, without thousands of small writes
  • Freeing every allocation on both the success and the error path
  • Replaying and validating the output in a windowed visualizer that needs nothing beyond Python itself

πŸ“‹ Function Overview

lem-in β€” modules breakdown
Module Feature Description
parsing Single-pass read read_stdin slurps stdin into one growable buffer, no syscall per line
parsing Ant count First line must be a positive decimal integer that fits an int, nothing else
parsing Rooms name x y, three fields exactly, int coordinates (negative allowed), no leading L or #
parsing Commands ##start / ##end apply to the next room; any other #… line is a comment
parsing Tunnels a-b, both rooms must already exist, no self-link; a repeated tunnel counts once
parsing Echo buffer Every accepted line is kept so the input can be reprinted before the moves
graph Hash table djb2 with chaining, 1024 buckets: room lookup by name in O(1)
graph Adjacency lists Each tunnel becomes two directed edges plus their residual reverses
graph Node splitting Room i becomes in = 2i and out = 2i+1, joined by an edge of capacity 1
solver Min-cost max flow Dijkstra with Johnson potentials from out(start) to in(end); the flow is the number of routes
solver Decomposition Saturated forward edges are walked back into explicit room sequences
solver Route selection Tries every count from 1 to the max flow, keeps the one with fewest turns; hopeless counts are skipped
solver Turn count Binary search on the smallest turn t whose capacity covers every ant
solver Ant distribution Every route is filled to the turn limit, the surplus is dropped from the longest ones
simulation Turn emission One line per turn, Lx-room tokens for every ant that moves
simulation Output buffer Fixed 64 KB buffer flushed whenever it fills, so memory stays flat
utils Cleanup A single free_all releases input, graph, edges, hash table and routes
bonus colony.py Parses the echoed colony, the ##visu options and the move lines
bonus replay.py Positions per turn from snapshots, routes walked by the ants, the four rules re-checked
bonus layout.py Coordinates scaled to the window with zoom and pan, a computed layout when they carry no shape, one lane per route
bonus view.py Pixel geometry of a frame β€” where every room, lane and ant goes β€” independent of the toolkit
bonus player.py Playback state: current turn, progress into the next move, speed
bonus ui_tk.py The tkinter window: canvas items, mouse, keys and the frame loop

Usage Example & Testing

Build and run

make                                    # builds libft, then the lem-in binary
./lem-in < maps/valid/example.map
make fclean                             # removes objects, libft.a and the binary

A solved colony

$ ./lem-in < maps/valid/example.map
3
##start
1 23 3
2 16 7
…
7-4
6-5

L1-3 L3-2
L1-4 L2-3 L3-7
L1-0 L2-4 L3-6
L2-0 L3-0

The map is echoed back first, then a blank line, then one line per turn. Three ants, four turns: they travel on two room-disjoint routes at once instead of queueing on the shortest one.

More routes are not always better

$ ./lem-in < maps/valid/diff_len_paths.map | tail -6
L1-a L5-c
L1-b L2-a L5-d L6-c
L1-end L2-b L3-a L5-e L6-d
L2-end L3-b L4-a L5-end L6-e
L3-end L4-b L6-end
L4-end

Six ants, one route of length 3 and one of length 4. Using both takes 6 turns, using only the short one would take 8. The solver evaluates every route count and keeps the best.

Error handling

$ ./lem-in < maps/invalid/no_path.map
ERROR
$ ./lem-in < maps/invalid/overflow_ants.map     # 18446744073709551621 ants
ERROR
$ ./lem-in extra_argument
Error!
Compile the executable without any arguments.

Any malformed colony β€” bad or overflowing ant count, duplicate room or command, unknown room in a tunnel, empty line, missing ##start / ##end, or no route between them β€” prints ERROR and exits with status 1.

Full sweep

for f in maps/valid/*.map maps/stress/*.map; do ./lem-in < "$f" > /dev/null || echo "FAIL $f"; done
for f in maps/invalid/*.map; do ./lem-in < "$f" 2>&1 | head -1; done   # 21x ERROR
valgrind --leak-check=full --show-leak-kinds=all ./lem-in < maps/valid/example.map
make maps                               # writes the large colonies to maps/stress/generated/
time ./lem-in < maps/stress/generated/dense_4000.map > /dev/null

πŸš€ Installation & Structure

πŸ“₯ Setup & Usage

Host requirements

Tool Needed for Notes
cc (gcc or clang) Compiling Built with -Wall -Wextra -Werror
make Driving the build Also builds lib/libft.a
A POSIX system Running Only read, write, malloc, free and exit are used
python3 β‰₯ 3.8 with Tk Bonus only tkinter ships with Python; Debian/Ubuntu need python3-tk, Arch tk

Nothing else has to be installed: lib/ ships the only helper library used.

Make targets

make            # build lib/libft.a and the lem-in binary
make bonus      # build lem-in and link ./visu-hex to the Python visualizer
make maps       # generate the large stress colonies (not versioned)
make clean      # remove the objects/ directory
make fclean     # clean + remove libft.a, the binary and the visualizer link
make re         # fclean + build

Running

./lem-in < maps/valid/two_paths.map     # from a file
cat colony.map | ./lem-in               # from a pipe
./lem-in                                # typed by hand, end with Ctrl-D
./lem-in < maps/valid/subject.map | ./visu-hex   # bonus: watch the ants walk

The program takes no arguments: it reads the colony on standard input, writes the simulation on standard output and ERROR on standard error.

Map format

4                 # number of ants, positive integer, first line
##start           # the next room is the entry
0 0 3             # name x y
2 2 5
3 4 0
##end             # the next room is the exit
1 8 3
0-2               # tunnel between two declared rooms
2-3
3-1

Room names may not start with L or #, coordinates must be integers that fit an int (negative ones included), and every tunnel must name two different rooms that already exist. Lines starting with # are comments; ##start and ##end are the only commands with a meaning, any other ##… line is ignored (the visualizer reads ##visu).


πŸ“ Project Structure
lem-in/
β”‚
β”œβ”€β”€ README.md                             # Main project documentation
β”œβ”€β”€ Makefile                              # Build rules: lem-in, bonus link, stress maps
β”œβ”€β”€ .gitignore                            # Files and directories ignored by Git
β”‚
β”œβ”€β”€ docs/
β”‚   └── README.md                         # Condensed project documentation
β”‚
β”œβ”€β”€ include/
β”‚   └── lem_in.h                          # Core types, constants and prototypes
β”‚
β”œβ”€β”€ lib/                                  # libft β€” the only helper library used
β”‚   β”œβ”€β”€ Makefile                          # Builds the libft static library
β”‚   β”œβ”€β”€ libft.h                           # libft declarations and shared types
β”‚   └── ft_*.c                            # Strings, memory, lists and conversions
β”‚
β”œβ”€β”€ maps/                                 # Test colonies
β”‚   β”œβ”€β”€ valid/                            # Must produce a simulation
β”‚   β”‚   β”œβ”€β”€ example.map                   # 3 ants, two routes of different length
β”‚   β”‚   β”œβ”€β”€ linear.map                    # One corridor, the ants queue up
β”‚   β”‚   β”œβ”€β”€ two_paths.map                 # Two routes of equal length, 4 ants
β”‚   β”‚   β”œβ”€β”€ diff_len_paths.map            # The longer route is still worth using
β”‚   β”‚   β”œβ”€β”€ extra_path.map                # A route that is not worth using
β”‚   β”‚   β”œβ”€β”€ single_ant.map                # Smallest possible colony
β”‚   β”‚   β”œβ”€β”€ subject.map                   # Example 1 of the subject
β”‚   β”‚   β”œβ”€β”€ negative_coords.map           # Coordinates below zero are integers too
β”‚   β”‚   β”œβ”€β”€ hundred_ants.map              # Generator --flow-thousand: 124 ants, 523 rooms
β”‚   β”‚   └── comments_commands.map         # Comments and unknown commands ignored
β”‚   β”‚
β”‚   β”œβ”€β”€ invalid/                          # Must print ERROR (21 cases)
β”‚   β”‚   β”œβ”€β”€ empty.map                     # No input at all
β”‚   β”‚   β”œβ”€β”€ no_ants.map                   # ants_zero / ants_negative / ants_not_number
β”‚   β”‚   β”œβ”€β”€ overflow_ants.map             # 2^64 + 5 ants β€” must not wrap to 5
β”‚   β”‚   β”œβ”€β”€ room_missing_field.map        # room_extra_field / coords_not_int / overflow_coords
β”‚   β”‚   β”œβ”€β”€ room_name_starts_L.map        # room_name_starts_hash
β”‚   β”‚   β”œβ”€β”€ dup_room.map                  # dup_start / dup_end
β”‚   β”‚   β”œβ”€β”€ no_start.map                  # no_end
β”‚   β”‚   β”œβ”€β”€ link_unknown_room.map         # self_link
β”‚   β”‚   β”œβ”€β”€ blank_line_middle.map         # An empty line stops the reading
β”‚   β”‚   └── no_path.map                   # Valid colony, start and end disconnected
β”‚   β”‚
β”‚   └── stress/                           # Edge cases and scale
β”‚       β”œβ”€β”€ direct_link.map               # ##start joined to ##end by a tunnel
β”‚       β”œβ”€β”€ duplicate_tunnels.map         # The same tunnel declared several times
β”‚       β”œβ”€β”€ same_coords.map               # Every room on the same point
β”‚       β”œβ”€β”€ unicode_names.map             # UTF-8 room names
β”‚       β”œβ”€β”€ long_names.map                # 5000-character room names
β”‚       β”œβ”€β”€ commands_everywhere.map       # Comments, unknown commands and ##visu
β”‚       β”œβ”€β”€ million_ants.map              # 10^6 ants on the subject's corridor
β”‚       β”œβ”€β”€ big_superposition.map         # Generator --big-superposition: 2 893 rooms
β”‚       └── generate.py                   # `make maps`: 100k-room chain, 5000 routes, grids…
β”‚
β”œβ”€β”€ bonus/
β”‚   β”œβ”€β”€ visu-hex                          # Launcher: ./lem-in < map | ./visu-hex
β”‚   └── visu_hex/                         # Python package (tkinter, no pip dependency)
β”‚       β”œβ”€β”€ __main__.py                   # Options, --dump mode
β”‚       β”œβ”€β”€ colony.py                     # Parses the echoed map and the moves
β”‚       β”œβ”€β”€ replay.py                     # Positions per turn, routes, validation
β”‚       β”œβ”€β”€ layout.py                     # Map scaling, zoom, pan, auto layout; lanes per route
β”‚       β”œβ”€β”€ view.py                       # Frame geometry, toolkit independent
β”‚       β”œβ”€β”€ player.py                     # Turn, phase and speed of the playback
β”‚       └── ui_tk.py                      # Canvas drawing, mouse, keys, frame loop
β”‚
└── srcs/
    β”œβ”€β”€ main.c                            # parse β†’ build β†’ solve β†’ simulate β†’ free
    β”‚
    β”œβ”€β”€ graph/
    β”‚   β”œβ”€β”€ graph_init.c                  # Adjacency lists + residual reverse edges
    β”‚   β”œβ”€β”€ node_split.c                  # Room β†’ in/out pair with capacity 1
    β”‚   └── hash_table.c                  # Room lookup by name (djb2, chaining)
    β”‚
    β”œβ”€β”€ parsing/
    β”‚   β”œβ”€β”€ parse_input.c                 # Line splitting, ant count and dispatch
    β”‚   β”œβ”€β”€ parse_links.c                 # Tunnel validation and storage
    β”‚   β”œβ”€β”€ parse_rooms.c                 # Room validation and storage
    β”‚   β”œβ”€β”€ read_stdin.c                  # Slurps stdin into one growable buffer
    β”‚   └── store_line.c                  # Keeps the original input for the echo
    β”‚
    β”œβ”€β”€ simulation/
    β”‚   β”œβ”€β”€ output.c                      # Fixed-size flushing output buffer
    β”‚   └── simulate.c                    # Moves ants and prints one line per turn
    β”‚
    β”œβ”€β”€ solver/
    β”‚   β”œβ”€β”€ algorithm.c                   # Main min-cost max-flow solver
    β”‚   β”œβ”€β”€ flow.c                        # Scratch state of the search
    β”‚   β”œβ”€β”€ paths.c                       # Flow decomposition into room sequences
    β”‚   β”œβ”€β”€ select.c                      # Orders the routes and shares the ants out
    β”‚   β”œβ”€β”€ shortest.c                    # Cheapest augmenting path, bucket queue
    β”‚   └── turns.c                       # Turn count and ant distribution
    β”‚
    └── utils/
        └── memory.c                      # Allocation cleanup and free helpers

Every module talks through t_lem_in, declared in include/lem_in.h: it owns the ant count, the graph, the hash table, the extracted routes and the echoed input, so a single free_all can clean up from anywhere in the program.


🧱 Algorithm Overview

Pipeline

stdin ─> parse ─> graph ─> node split ─> max flow ─> decompose ─> select ─> assign ─> simulate ─> stdout

Node splitting β€” one ant per room

   ── A ── B ── C ──         in(A) β†’ out(A) β†’ in(B) β†’ out(B) β†’ in(C) β†’ out(C)
                     ==>                        β”” cap 1 β”˜
   room B, capacity 1        crossing B consumes its single internal edge

Every room i becomes two nodes, in = 2i and out = 2i+1, joined by an edge of capacity 1. A tunnel a-b becomes out(a) β†’ in(b) and out(b) β†’ in(a), both capacity 1. Sending flow from out(start) to in(end) therefore cannot reuse a room: its internal edge is already saturated.

Min-cost max flow β€” how many routes exist

[ out(start) ] ──cheapest path──> … ──> [ in(end) ]
      each augmenting path = one more room-disjoint route
      flow value == number of routes    (0 => ERROR, start and end disconnected)

Forward edges cost +1 and their residual reverses -1, so rerouting refunds the length it gives back. Each pass pushes one unit along the cheapest augmenting path, which keeps the routes as short as possible for that flow value. Dijkstra with Johnson potentials makes the negative reverse costs non-negative, and a bucket queue settles each node in O(1).

Route selection β€” fewest turns, not most routes

routes sorted by length:  [3] [4] [7]        6 ants
   k = 1 -> 8 turns
   k = 2 -> 6 turns   <-- kept
   k = 3 -> 6 turns

A route of length len delivers t - len + 1 ants by turn t, so the turns needed by the k shortest routes are found by binary search on t. Every k from 1 to the max flow is evaluated and the cheapest one wins: a long extra route can delay the last ant instead of helping. A k whose lower bound ceil(ants / k) + shortest - 1 cannot beat the best turn count found so far is never decomposed, and the search stops as soon as the best equals the length of the shortest route.

Why this is optimal

Any schedule, even one where ants wait in rooms or take routes that overlap in time, is a dynamic flow from ##start to ##end; Ford and Fulkerson showed that the most flow that can reach the sink by a deadline is always achieved by repeating a static min-cost flow over time β€” exactly a set of room-disjoint routes each used every turn. Evaluating every flow value therefore covers the true optimum, which is what the generator's --big-superposition maps confirm.

Simulation

turn t, route of length len, ant j (0-based on that route):
     ant j leaves on turn j+1  ->  it stands in rooms[t - j]
     printed while 0 <= t - j <= len

Every route is filled up to the turn limit and the surplus dropped from the longest ones, each route gets a contiguous block of ids, and every turn goes through a 64 KB buffer flushed whenever it fills.


🐜 Bonus

Built with make bonus and used the way the subject describes it:

make bonus                                        # links ./visu-hex β†’ bonus/visu-hex
./lem-in < maps/valid/example.map | ./visu-hex
./lem-in < big.map | ./visu-hex --view lanes --speed 6 --paused
./visu-hex --help

visu-hex is a Python 3 program built on tkinter, the GUI toolkit that ships with Python itself, so there is nothing to install with pip. It opens a resizable window, reads the simulation on stdin, re-checks every move against the rules of the subject and animates the ants turn by turn.

Two views, v switches

map β€” the rooms at their coordinates, tunnels between them, S and E discs with the number of ants waiting or arrived, the rooms of the routes in use outlined in their route's colour, and every ant a coloured dot that glides along its tunnel. Drag to pan, wheel to zoom. When the coordinates carry no shape β€” the generator puts every room on the x == y diagonal β€” the rooms are laid out from the graph instead: one column per distance from ##start, rows ordered so that tunnels stay short, which turns a 4000-room colony into a readable flow from S to E.

lanes β€” one lane per route the ants actually took, shortest first, a dot per room, which is what makes the algorithm legible: the short routes stay saturated while the long ones carry a single wave. On the generator's maps this view opens first.

lem-in visu-hex   [lanes]                                  turn 34 / 69   playing   x2

#1  len 15  ants 55   S●●●●●●●●●●●●●●E
#2  len 17  ants 53   S●●●●●●●●●●●●●●●●E
#3  len 25  ants 45   S●●●●●●●●●●●●●●●●●●●●●●●●E
…
#9  len 36  ants 29   S○○○○○●●●●●●●●●●●●●●●●●●●●●●●●●●●●●○E

ants 390  rooms 2556  tunnels 3122  routes 9  turns 69   start 89  moving 220  arrived 81   valid

The bars around the view carry the rest:

title current turn / total, play state, speed
colony ants, rooms, tunnels, routes used, turns
progress ants still in ##start, on the way, arrived
validity one move per ant, through a tunnel, into a free room, never out of ##end, everybody home β€” or the first offending turn

Controls

Key Action
space play / pause
← β†’ one turn back / forward
+ - faster / slower
v map ↔ lanes
drag, wheel pan, zoom the map (wheel scrolls the lanes)
w a s d, z x, 0 pan, zoom, reset from the keyboard
PgUp PgDn scroll the lanes
r e restart / jump to the end
q Esc quit

Commands and comments come out on stdout as well, so the colony can steer the window β€” put a line like this in the map:

##visu speed=4 paused view=lanes

Room names of any length and alphabet, thousands of rooms, thousands of routes (the lanes scroll, and a lane wider than the window follows its ants), a million ants and invalid simulations all render. Every ant owns one canvas item that is moved rather than redrawn, the static map is rebuilt only when it pans or zooms, and the lanes only when they scroll, so a turn costs one coordinate update per ant. --dump DIR writes frames as PostScript without opening the window, which is how the drawing code is tested.


πŸ’‘ Key Learning Outcomes

  • Graph modelling: turning a text description into adjacency lists, and turning a vertex constraint into an edge constraint through node splitting
  • Network flow: residual graphs, reverse edges, min-cost augmenting paths, Johnson potentials, and the fact that unit capacities make the max flow equal a disjoint-route count
  • Flow decomposition: reading concrete routes back out of a flow, by following the forward edges that ended up saturated
  • Optimisation over a discrete choice: the best answer is not "use every route", it is the route count that minimises the last arrival
  • Buffered I/O: one read to take the input and one write to give the output, instead of a syscall per line
  • Defensive parsing: rejecting malformed input early and cleanly, through a single exit path that frees everything
  • Checking your own work: the visualizer replays the output against the rules instead of trusting the solver

βš™οΈ Technical Specifications

  • Language: C, compiled with cc -Wall -Wextra -Werror; bonus in Python 3 with tkinter
  • Dependencies: none beyond libc; lib/ holds the local libft
  • Input: standard input only, the program takes no arguments
  • Output: the map echoed back, a blank line, then one line per turn
  • Errors: ERROR on standard error, exit status 1
  • Graph: adjacency lists, 2 * rooms nodes and 2 * (rooms + 2 * tunnels) directed edges counting the residual reverses
  • Lookup: djb2 hash table, 1024 buckets, separate chaining
  • Min-cost max flow: O(F * (V + E)) here, since every capacity is 1 and F is bounded by the degree of ##start and ##end
  • Turn count: binary search between the shortest route length and that length plus the ant count
  • Buffers: 4096-byte initial size, doubling growth, for both input and output
  • Measured: the generator's --big and --big-superposition colonies solve in under 50 ms; 10 000 rooms on a grid or 60 000 tunnels in a quarter of a second, a 100 000-room corridor or 5 000 parallel routes in about one
  • Bonus: tkinter canvas, one item per ant moved in place, the static map rebuilt only on pan or zoom; 64 MB of simulation at most, positions rebuilt from at most 64 snapshots so stepping back never replays the whole run

πŸ”§ Requirements

  • A C compiler (gcc or clang) and make
  • A POSIX system β€” only read, write, malloc, free and exit are used
  • Nothing to install: the helper library lives in lib/ and is built by make
  • For the bonus, python3 3.8 or later with tkinter (part of Python; the Tk library comes as python3-tk on Debian/Ubuntu, tk on Arch, and with the python.org installer on macOS)

Note

lem-in is a small program with a large algorithm behind it: the one-ant-per-room rule turns a plain shortest-path exercise into a max-flow problem, and the answer is never a single route but the right set of routes with the right number of ants on each of them.

About

Program that parses an ant-farm graph and routes every ant from start to end in the fewest possible turns using max-flow on a node-split graph.

Resources

Code of conduct

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages