Principe de résolution
Pour résoudre ce problème j’ai choisi de procéder en suivant les étapes suivantes :
- Parser le flux envoyé dans la connexion TCP pour extraire la taille de la grille et le base64 de l’image.
- Traiter l’image avec opencv pour ne garder que la partie grille et passer l’image en binaire.
- Construire un graphe connexe en mémoire
- Le nombre de sommet correspond au nombre de case de la grille
- Les arêtes entre les sommets sont ajoutés dans le graphe si on peut tracer une ligne sur l’image binaire entre deux cases adjacentes sans que cette ligne ne croise une bordure de la case (les bordures ont été légèrement épaissis lors du traitement opencv).
- Utiliser l’algorithme de Karger pour trouver une coupe du graphe connexe afin de le séparer en deux graphes distincts. L’algorithme s’arrête dès qu’il trouve une coupe de trois arrêtes.
- Formater la liste des arrêtes de la coupe pour correspondre aux coordonnées de la grille et envoyer la réponse dans la connexion TCP.
Code de la solution
Le code pour résoudre ce problème a été écrit en Rust et séparé dans plusieurs fichiers dont le contenu va être indiqué dans les parties ci-dessous.
Client pour intéragir avec le service
Le fichier client.rs gère les échanges avec le service docker via la connexion TCP :
use std::{io::{Read, Write}, net::TcpStream, time::Duration};
use regex::Regex;
use crate::{FarmError, Result};
const BUFFER_SIZE: usize = 8192;
const BEGIN_IMAGE: &str = "------------------------ BEGIN MAZE ------------------------";
const END_IMAGE: &str = "------------------------- END MAZE -------------------------";
pub struct FarmClient {
stream: TcpStream,
buffer: [u8; BUFFER_SIZE],
}
impl FarmClient {
pub fn new(host: &str, port: u16) -> Result<Self> {
let stream = TcpStream::connect(format!("{host}:{port}"))?;
stream.set_read_timeout(Some(Duration::from_secs(5)))?;
let mut client = Self { stream, buffer: [0u8; BUFFER_SIZE] };
println!("{}", client.read_lines()?);
Ok(client)
}
pub fn read_lines(&mut self) -> Result<String> {
let mut lines = String::new();
loop {
let size = self.stream.read(&mut self.buffer)?;
if size > 0 {
let text = str::from_utf8(&self.buffer[..size])?;
lines.push_str(&text);
}
if size < BUFFER_SIZE {
println!("{}", lines);
return Ok(lines);
}
}
}
pub fn read_flag(&mut self) -> Result<String> {
let mut lines = String::new();
loop {
let text = self.read_lines()?;
lines.push_str(&text);
if text.contains("FCSC") {
break;
}
}
Ok(lines)
}
pub fn read_maze(&mut self) -> Result<(String, (usize, usize))> {
let mut lines = String::new();
loop {
let text = self.read_lines()?.replace("\n", "");
lines.push_str(&text);
if text.ends_with(">>> ") {
break;
}
if text.ends_with("Bye bye.") {
return Err(Box::new(FarmError::ReadTCPStreamError));
}
}
let (before, text) = lines
.split_once(BEGIN_IMAGE)
.ok_or(Box::new(FarmError::ImageReadError))?;
let (img, _) = text
.split_once(END_IMAGE)
.ok_or(Box::new(FarmError::ImageReadError))?;
let dimensions = Self::parse_tore_dimensions(before)
.ok_or(Box::new(FarmError::ReadToreDimensionsError))?;
Ok((img.to_string(), dimensions))
}
fn parse_tore_dimensions(text: &str) -> Option<(usize, usize)> {
let size_regex = Regex::new(r"Generating a (\d+)x(\d+) tore").unwrap();
size_regex.captures(&text).map(|dimensions|
(dimensions[1].parse().unwrap_or(0), dimensions[2].parse().unwrap_or(0)))
}
pub fn send_message(&mut self, text: &str) -> Result<()> {
writeln!(self.stream, "{text}")?;
Ok(())
}
}
Traitement de l’image
L’image en base64 est chargée puis retraité avec opencv dans le fichier preprocessing.rs pour pouvoir détecter les passages entre les cases en projetant une ligne entre deux cases adjacentes et en détectant si la ligne rentre en colision avec une bordure de la case ou non.
use opencv::{core, imgcodecs, imgproc, prelude::*};
use base64::prelude::*;
use crate::Result;
fn load_base64_png(base64_img: &str) -> Result<Mat> {
let decoded_bytes = BASE64_STANDARD.decode(base64_img.trim())?;
let mat = Mat::from_slice(&decoded_bytes)?;
let img = imgcodecs::imdecode(&mat, imgcodecs::IMREAD_COLOR)?;
Ok(img)
}
fn binarize(img: &Mat, blur: Option<core::Size>) -> Result<Mat> {
let mut gray = Mat::default();
imgproc::cvt_color(&img, &mut gray, imgproc::COLOR_BGR2GRAY, 0)?;
let blurred_or_not = if let Some(blur_size) = blur {
let mut blurred = Mat::default();
imgproc::gaussian_blur(&gray, &mut blurred, blur_size, 0.0, 0.0, core::BORDER_DEFAULT)?;
blurred
} else {
gray
};
let mut binary = Mat::default();
imgproc::threshold(&blurred_or_not, &mut binary, 0.0, 255.0, imgproc::THRESH_BINARY | imgproc::THRESH_OTSU)?;
Ok(binary)
}
fn crop_grid(img: &Mat) -> Result<Mat> {
let border = 63;
let width = img.cols() - border * 2;
let y = img.rows() - width - border;
let rect = core::Rect::new(border, y, width, width);
let cropped = Mat::roi(img, rect)?;
Ok(cropped.try_clone()?)
}
fn prepare_grid(img: &Mat) -> Result<Mat> {
let binary_img = binarize(&img, None)?;
let grid_img = crop_grid(&binary_img)?;
// Erode
let kernel = imgproc::get_structuring_element(imgproc::MORPH_RECT, core::Size::new(3, 3), core::Point::new(-1, -1))?;
let mut eroded = Mat::default();
imgproc::erode(&grid_img, &mut eroded, &kernel, core::Point::new(-1, -1), 1, core::BORDER_CONSTANT, imgproc::morphology_default_border_value()?)?;
Ok(eroded)
}
pub fn load_grid(base64_img: &str) -> Result<Mat> {
let img = load_base64_png(&base64_img)?;
prepare_grid(&img)
}
fn bresenham_line(start: &core::Point, end: &core::Point) -> Vec<core::Point> {
let mut points = Vec::new();
let mut x0 = start.x;
let mut y0 = start.y;
let x1 = end.x;
let y1 = end.y;
let dx = (x1 - x0).abs();
let dy = -(y1 - y0).abs();
let sx = if x0 < x1 { 1 } else { -1 };
let sy = if y0 < y1 { 1 } else { -1 };
let mut err = dx + dy;
while x0 != x1 || y0 != y1 {
points.push(core::Point::new(x0, y0));
let e2 = 2 * err;
if e2 >= dy {
err += dy;
x0 += sx;
}
if e2 <= dx {
err += dx;
y0 += sy;
}
}
points
}
pub fn line_has_intersection(img: &Mat, start_line: &core::Point, end_line: &core::Point) -> bool {
let line = bresenham_line(start_line, end_line);
for point in &line {
if *img.at_2d::<u8>(point.y, point.x).unwrap_or(&0) == 0 {
return true;
}
}
false
}
Chargement du graphe et calcul de la coupe
Les fichiers graph.rs et farm.rs servent à représenter la grille sous forme d’un graphe connexe puis de trouvé une coupe qui ne nécessite la suppression que de trois arrêtes pour séparer le graphe. L’algorithme de Karger est utilisé pour trouver la coupe du graphe. Il a été implémenté en utilisant une structure de données Union-Find.
graph.rs
use rand::seq::SliceRandom;
pub type Edge = (usize, usize, u8);
struct UnionFind {
partitions: Vec<usize>,
size: usize
}
impl UnionFind {
fn new(size: usize) -> Self {
let mut partitions = vec![0; size];
for i in 0..size {
partitions[i] = i;
}
Self { partitions, size }
}
fn union(&mut self, idx1: usize, idx2: usize) {
self.partitions[idx2] = idx1;
self.size -= 1;
}
fn find(&mut self, idx: usize) -> usize {
let tmp_idx = self.partitions[idx];
if tmp_idx == idx {
return idx;
}
let res_idx = self.find(tmp_idx);
// compress path
self.partitions[idx] = res_idx;
res_idx
}
}
#[derive(Clone)]
pub struct Graph<T> {
nodes: Vec<T>,
edges: Vec<Edge>,
}
impl <T> Graph<T> {
pub fn new(nodes: Vec<T>, edges_opt: Option<Vec<Edge>>) -> Self {
Self { nodes, edges: edges_opt.unwrap_or(vec![]) }
}
pub fn add_edges(&mut self, edges: &[Edge]) {
self.edges.extend_from_slice(edges);
}
pub fn nodes(&self) -> &[T] {
&self.nodes
}
pub fn node(&self, idx: usize) -> &T {
&self.nodes[idx]
}
pub fn edges(&self) -> &[Edge] {
&self.edges
}
pub fn karger_min_cut(&self, expected_min_cut: Option<usize>) -> Option<Vec<Edge>> {
let mut res = None;
let mut min_cut_size = usize::MAX;
let nodes_count = self.nodes.len() as u32;
let retry_number = nodes_count * nodes_count / 2;
for _ in 0..retry_number {
if let Some(min_cut) = self.karger_min_cut_union_find() && min_cut.len() < min_cut_size {
min_cut_size = min_cut.len();
res = Some(min_cut);
}
if let Some(expected_min_cut_size) = expected_min_cut && expected_min_cut_size == min_cut_size {
break;
}
}
res
}
fn karger_min_cut_union_find(&self) -> Option<Vec<Edge>> {
let mut edges = self.edges.clone();
edges.shuffle(&mut rand::rng());
let mut clusters = UnionFind::new(self.nodes.len());
while clusters.size > 2 && let Some(edge) = edges.pop() {
let src = clusters.find(edge.0);
let dest = clusters.find(edge.1);
if src != dest {
clusters.union(src, dest);
}
}
if clusters.size != 2 {
return None;
}
let mut min_cut = vec![];
for edge in self.edges() {
if clusters.find(edge.0) != clusters.find(edge.1) {
min_cut.push(*edge);
}
}
Some(min_cut)
}
}
farm.rs
use opencv::{core::Point, prelude::*};
use crate::{Result, graph::Graph, preprocessing::{self, line_has_intersection}};
#[derive(Clone)]
struct FarmPlot {
x: usize,
y: usize,
center: Point,
}
impl FarmPlot {
fn new(x: usize, y: usize, center: Point) -> Self {
Self { x, y, center }
}
}
pub struct Farm {
img: Mat,
offset_x: i32,
offset_y: i32,
start_x: i32,
start_y: i32,
rows: usize,
colums: usize,
graph: Graph<FarmPlot>,
}
impl Farm {
pub fn new(base64_img: &str, rows: usize, colums: usize) -> Result<Self> {
let img = preprocessing::load_grid(base64_img)?;
let offset_x = img.cols() / colums as i32;
let offset_y = img.rows() / rows as i32;
let start_x = offset_x / 2;
let start_y = offset_y / 2;
let mut plots = Vec::with_capacity(rows*colums);
for y in 0..rows {
for x in 0..colums {
let plot_center = Point::new(start_x + offset_x * x as i32, start_y + offset_y * y as i32);
plots.push(FarmPlot::new(x, y, plot_center));
}
}
Ok(Self { img, offset_x, offset_y, start_x, start_y, rows, colums,
graph: Graph::new(plots, None)})
}
fn get_node_idx(&self, x: usize, y: usize) -> usize {
x + self.colums * y
}
fn get_neighbor_node_idx(&self, x: usize, y: usize, relative_position: &(i32, i32)) -> (usize, bool) {
let n_x = match relative_position.0 {
-1 if x < 1 => (self.colums - 1, true),
1 if x == self.colums - 1 => (0, true),
_ => ((relative_position.0 + x as i32) as usize, false),
};
let n_y = match relative_position.1 {
-1 if y < 1 => (self.rows - 1, true),
1 if y == self.rows - 1 => (0, true),
_ => ((relative_position.1 + y as i32) as usize, false),
};
(self.get_node_idx(n_x.0, n_y.0), n_x.1 || n_y.1)
}
fn generate_border_point(&self, plot: &FarmPlot, relative_position: &(i32, i32)) -> Option<Point> {
match relative_position {
(1, -1) => Some(Point::new(self.start_x * 2 + self.offset_x * plot.x as i32, self.offset_y * plot.y as i32)),
(1, 0) => Some(Point::new(self.start_x * 2 + self.offset_x * plot.x as i32, self.start_y + self.offset_y * plot.y as i32)),
(0, 1) => Some(Point::new(self.start_x + self.offset_x * plot.x as i32, self.start_y * 2 + self.offset_y * plot.y as i32)),
(1, 1) => Some(Point::new(self.start_x * 2 + self.offset_x * plot.x as i32, self.start_y * 2 + self.offset_y * plot.y as i32)),
_ => None
}
}
pub fn init_graph(&mut self) {
let adjacency_positions = vec![
(1, -1),
(1, 0),
(0, 1), (1, 1)
];
let mut edges = vec![];
for plot in self.graph.nodes() {
for position in &adjacency_positions {
let (other_node_idx, border) = self.get_neighbor_node_idx(plot.x, plot.y, position);
let other_plot = self.graph.node(other_node_idx);
let destination_point = if border {
self.generate_border_point(plot, position).unwrap()
} else {
other_plot.center
};
if !line_has_intersection(&self.img, &plot.center, &destination_point) {
let node_idx = self.get_node_idx(plot.x, plot.y);
edges.push((node_idx, other_node_idx, 1));
}
}
}
self.graph.add_edges(&edges);
}
pub fn disconnect_plots_min_cut(&self) -> Option<Vec<((usize, usize), (usize, usize))>> {
if let Some(res) = self.graph.karger_min_cut(Some(3)) && res.len() == 3 {
return Some(res.iter()
.map(|idx|
((self.graph.node(idx.0).x, self.graph.node(idx.0).y),
(self.graph.node(idx.1).x, self.graph.node(idx.1).y)))
.collect());
}
None
}
}
Ordonnancement du traitement
Le fichier lib.rs exécute les traitements et itère sur les 100 grilles qui sont envoyées par le service docker puis lit la connexion TCP une dernière fois pour récupérer le flag.
use std::{error::Error, time::Instant};
use crate::{client::FarmClient, farm::Farm};
pub(crate) mod client;
pub(crate) mod preprocessing;
pub(crate) mod graph;
pub(crate) mod farm;
pub type Result<T> = std::result::Result<T, Box<dyn Error + Send + Sync>>;
#[derive(Debug, strum_macros::Display)]
enum FarmError {
ImageReadError,
ReadTCPStreamError,
ReadToreDimensionsError,
DisconnectPlotsError,
}
impl Error for FarmError {}
pub fn resolve_mazes(host: &str, port: u16) -> Result<()> {
let mut client = FarmClient::new(host, port)?;
client.send_message("")?;
for _ in 0..100 {
let start = Instant::now();
let (base64_img, (rows, columns)) = client.read_maze()?;
let mut farm = Farm::new(&base64_img, rows, columns).unwrap();
farm.init_graph();
if let Some(removed_edges) = farm.disconnect_plots_min_cut() {
client.send_message(&format!("{:?}", removed_edges))?;
let duration = start.elapsed();
println!("Solution {}x{} : {:?} found in {:?}", rows, columns, removed_edges, duration)
} else {
return Err(Box::new(FarmError::DisconnectPlotsError));
}
}
client.read_flag()?;
Ok(())
}
Build et lancement du programme
Les fichiers main.rs et Cargo.toml sont ci-dessous et le programme peut être compilé avec la commande cargo build --release.
main.rs
use centaure_parks::resolve_mazes;
fn main() {
resolve_mazes("localhost", 4000).unwrap();
}
Cargo.toml
[package]
name = "centaure-parks"
version = "0.1.0"
edition = "2024"
[dependencies]
base64 = "0.22.1"
opencv = "0.98.1"
rand = "0.9.2"
regex = "1.12.2"
strum_macros = "0.27.2"
Solution
Commande pour lancer le programme (avec un petit grep pour filtrer la sortie) : time ./target/release/centaure-parks | egrep "FCSC|Solution"
$ time ./target/release/centaure-parks | egrep "FCSC{|Solution"
Solution 5x5 : [((0, 1), (1, 2)), ((0, 4), (1, 3)), ((3, 4), (4, 0))] found in 78.679306ms
Solution 5x5 : [((1, 1), (2, 1)), ((4, 3), (0, 3)), ((4, 4), (0, 4))] found in 68.515984ms
Solution 5x5 : [((1, 1), (2, 0)), ((0, 3), (1, 3)), ((0, 4), (1, 4))] found in 69.101076ms
Solution 5x5 : [((3, 2), (4, 3)), ((4, 2), (0, 3)), ((4, 4), (4, 0))] found in 73.186317ms
Solution 5x5 : [((0, 3), (1, 2)), ((1, 4), (2, 4)), ((3, 4), (4, 0))] found in 66.709502ms
Solution 8x8 : [((2, 2), (3, 1)), ((7, 5), (0, 6)), ((3, 6), (4, 7))] found in 88.030565ms
Solution 8x8 : [((1, 3), (2, 2)), ((4, 4), (5, 4)), ((0, 6), (1, 5))] found in 91.123931ms
Solution 8x8 : [((4, 4), (5, 3)), ((2, 6), (3, 5)), ((2, 7), (3, 6))] found in 88.973856ms
Solution 8x8 : [((4, 3), (5, 3)), ((4, 4), (5, 4)), ((4, 6), (5, 5))] found in 88.757771ms
Solution 8x8 : [((7, 1), (0, 1)), ((1, 6), (2, 5)), ((0, 7), (1, 0))] found in 89.209154ms
Solution 10x10 : [((8, 2), (9, 2)), ((7, 8), (8, 7)), ((2, 9), (3, 0))] found in 130.166183ms
Solution 10x10 : [((1, 2), (2, 2)), ((5, 9), (6, 8)), ((7, 9), (8, 8))] found in 109.031221ms
Solution 10x10 : [((1, 4), (2, 3)), ((3, 6), (4, 5)), ((3, 8), (4, 9))] found in 107.999323ms
Solution 10x10 : [((0, 0), (1, 1)), ((5, 5), (5, 6)), ((9, 9), (0, 8))] found in 109.351407ms
Solution 10x10 : [((2, 2), (2, 3)), ((6, 4), (7, 5)), ((6, 6), (7, 7))] found in 108.672734ms
Solution 10x10 : [((9, 1), (0, 2)), ((8, 6), (9, 5)), ((0, 7), (1, 6))] found in 107.553367ms
Solution 10x10 : [((3, 0), (4, 9)), ((6, 0), (7, 9)), ((1, 5), (2, 4))] found in 108.6063ms
Solution 10x10 : [((5, 2), (5, 3)), ((3, 7), (4, 8)), ((8, 7), (9, 6))] found in 109.239628ms
Solution 10x10 : [((7, 0), (8, 9)), ((7, 1), (8, 0)), ((4, 6), (5, 5))] found in 108.295707ms
Solution 10x10 : [((5, 1), (6, 0)), ((4, 6), (5, 7)), ((1, 8), (2, 8))] found in 108.527555ms
Solution 11x11 : [((10, 3), (0, 3)), ((4, 6), (5, 5)), ((10, 9), (0, 10))] found in 119.697951ms
Solution 11x11 : [((0, 2), (1, 1)), ((5, 3), (6, 2)), ((5, 4), (6, 4))] found in 120.440308ms
Solution 11x11 : [((9, 1), (10, 0)), ((4, 5), (5, 4)), ((9, 7), (10, 8))] found in 119.457567ms
Solution 11x11 : [((1, 8), (2, 7)), ((10, 8), (0, 7)), ((8, 9), (8, 10))] found in 117.502332ms
Solution 11x11 : [((3, 7), (4, 8)), ((8, 8), (9, 7)), ((8, 9), (9, 8))] found in 118.700045ms
Solution 11x11 : [((4, 5), (5, 6)), ((5, 5), (6, 4)), ((7, 9), (8, 8))] found in 118.745756ms
Solution 11x11 : [((0, 1), (1, 0)), ((0, 2), (1, 1)), ((10, 3), (0, 4))] found in 118.867086ms
Solution 11x11 : [((3, 3), (4, 2)), ((0, 7), (1, 8)), ((10, 10), (10, 0))] found in 117.597407ms
Solution 11x11 : [((3, 3), (4, 3)), ((2, 10), (2, 0)), ((9, 10), (10, 9))] found in 117.716152ms
Solution 11x11 : [((0, 1), (1, 0)), ((9, 7), (10, 6)), ((0, 10), (1, 9))] found in 116.024861ms
Solution 12x12 : [((2, 3), (3, 2)), ((2, 4), (3, 3)), ((2, 11), (2, 0))] found in 128.616553ms
Solution 12x12 : [((3, 6), (4, 5)), ((4, 8), (5, 9)), ((2, 9), (3, 10))] found in 131.320645ms
Solution 12x12 : [((1, 5), (2, 4)), ((0, 10), (1, 10)), ((9, 11), (9, 0))] found in 131.364586ms
Solution 12x12 : [((8, 1), (9, 1)), ((8, 2), (9, 3)), ((0, 10), (1, 11))] found in 130.344786ms
Solution 12x12 : [((3, 2), (4, 2)), ((0, 3), (0, 4)), ((10, 5), (11, 6))] found in 130.186285ms
Solution 12x12 : [((2, 7), (3, 8)), ((0, 8), (0, 9)), ((1, 8), (2, 8))] found in 131.010988ms
Solution 12x12 : [((7, 1), (8, 0)), ((5, 5), (5, 6)), ((10, 5), (10, 6))] found in 130.242803ms
Solution 12x12 : [((7, 3), (8, 3)), ((1, 6), (2, 5)), ((1, 11), (1, 0))] found in 128.840095ms
Solution 12x12 : [((6, 5), (7, 6)), ((9, 7), (10, 6)), ((2, 9), (3, 8))] found in 129.955829ms
Solution 12x12 : [((0, 3), (0, 4)), ((7, 4), (8, 4)), ((9, 5), (10, 4))] found in 129.625615ms
Solution 15x15 : [((4, 6), (5, 5)), ((3, 14), (4, 0)), ((7, 14), (7, 0))] found in 172.172088ms
Solution 15x15 : [((0, 4), (1, 4)), ((14, 8), (14, 9)), ((9, 12), (10, 12))] found in 173.801034ms
Solution 15x15 : [((0, 2), (0, 3)), ((2, 8), (3, 9)), ((4, 13), (4, 14))] found in 175.597662ms
Solution 15x15 : [((5, 1), (5, 2)), ((5, 9), (6, 8)), ((14, 14), (0, 14))] found in 172.327956ms
Solution 15x15 : [((14, 1), (0, 1)), ((12, 6), (13, 5)), ((7, 8), (8, 8))] found in 171.370221ms
Solution 15x15 : [((11, 3), (12, 4)), ((4, 10), (4, 11)), ((8, 10), (9, 11))] found in 176.011301ms
Solution 15x15 : [((1, 7), (2, 7)), ((0, 8), (1, 9)), ((14, 8), (0, 9))] found in 172.635648ms
Solution 15x15 : [((6, 2), (6, 3)), ((5, 10), (6, 9)), ((12, 12), (13, 13))] found in 173.574605ms
Solution 15x15 : [((2, 0), (3, 14)), ((5, 8), (6, 9)), ((4, 14), (4, 0))] found in 171.656596ms
Solution 15x15 : [((6, 11), (7, 10)), ((7, 12), (8, 11)), ((10, 14), (11, 0))] found in 173.00582ms
Solution 16x16 : [((8, 6), (9, 7)), ((15, 8), (0, 8)), ((6, 13), (7, 12))] found in 188.389891ms
Solution 16x16 : [((11, 0), (12, 1)), ((15, 1), (0, 0)), ((3, 6), (4, 5))] found in 193.297474ms
Solution 16x16 : [((2, 0), (3, 15)), ((14, 0), (15, 1)), ((7, 4), (7, 5))] found in 190.485802ms
Solution 16x16 : [((7, 0), (8, 0)), ((6, 2), (7, 3)), ((2, 12), (3, 11))] found in 190.713209ms
Solution 16x16 : [((11, 10), (12, 11)), ((6, 14), (6, 15)), ((10, 14), (11, 15))] found in 189.998905ms
Solution 16x16 : [((2, 5), (3, 4)), ((9, 7), (10, 7)), ((1, 8), (1, 9))] found in 188.302805ms
Solution 16x16 : [((1, 0), (2, 15)), ((5, 4), (5, 5)), ((5, 12), (6, 11))] found in 187.096684ms
Solution 16x16 : [((5, 3), (5, 4)), ((2, 10), (3, 11)), ((7, 14), (8, 13))] found in 191.710811ms
Solution 16x16 : [((13, 1), (14, 1)), ((6, 12), (7, 13)), ((0, 14), (1, 15))] found in 188.913148ms
Solution 16x16 : [((0, 2), (1, 1)), ((9, 14), (10, 13)), ((10, 15), (11, 0))] found in 189.862862ms
Solution 18x18 : [((4, 2), (5, 2)), ((12, 5), (12, 6)), ((4, 9), (5, 10))] found in 224.074389ms
Solution 18x18 : [((16, 7), (17, 8)), ((13, 10), (13, 11)), ((17, 17), (17, 0))] found in 228.678233ms
Solution 18x18 : [((11, 3), (12, 2)), ((2, 13), (3, 12)), ((0, 15), (1, 16))] found in 224.600439ms
Solution 18x18 : [((17, 3), (0, 2)), ((17, 8), (17, 9)), ((12, 16), (13, 17))] found in 224.839076ms
Solution 18x18 : [((6, 1), (7, 0)), ((8, 11), (9, 12)), ((14, 11), (14, 12))] found in 227.467694ms
Solution 18x18 : [((11, 0), (12, 1)), ((10, 2), (11, 1)), ((10, 12), (11, 12))] found in 226.549033ms
Solution 18x18 : [((10, 1), (11, 1)), ((11, 4), (12, 3)), ((17, 15), (17, 16))] found in 226.428988ms
Solution 18x18 : [((5, 5), (6, 6)), ((12, 9), (13, 8)), ((14, 12), (15, 11))] found in 224.405223ms
Solution 18x18 : [((1, 4), (2, 5)), ((13, 12), (13, 13)), ((16, 14), (17, 15))] found in 226.619025ms
Solution 18x18 : [((0, 6), (0, 7)), ((3, 6), (4, 5)), ((7, 7), (8, 8))] found in 224.439282ms
Solution 20x20 : [((13, 2), (13, 3)), ((6, 16), (7, 17)), ((2, 18), (3, 19))] found in 264.347951ms
Solution 20x20 : [((0, 0), (1, 1)), ((9, 10), (10, 9)), ((19, 18), (19, 19))] found in 265.772198ms
Solution 20x20 : [((5, 0), (6, 1)), ((8, 9), (9, 10)), ((2, 15), (3, 14))] found in 274.411237ms
Solution 20x20 : [((0, 8), (1, 7)), ((6, 13), (7, 12)), ((9, 15), (10, 14))] found in 268.954963ms
Solution 20x20 : [((0, 13), (1, 14)), ((0, 14), (0, 15)), ((4, 18), (5, 19))] found in 268.995356ms
Solution 20x20 : [((17, 5), (17, 6)), ((17, 7), (18, 8)), ((17, 17), (18, 16))] found in 264.308968ms
Solution 20x20 : [((0, 0), (0, 1)), ((10, 8), (10, 9)), ((13, 14), (14, 13))] found in 267.883278ms
Solution 20x20 : [((2, 11), (3, 12)), ((2, 12), (2, 13)), ((5, 18), (6, 17))] found in 262.114591ms
Solution 20x20 : [((13, 7), (14, 8)), ((8, 10), (9, 9)), ((13, 19), (14, 0))] found in 265.618902ms
Solution 20x20 : [((8, 7), (9, 7)), ((1, 11), (1, 12)), ((18, 19), (19, 18))] found in 267.489444ms
Solution 22x22 : [((0, 4), (1, 5)), ((19, 5), (20, 4)), ((6, 19), (7, 18))] found in 310.171159ms
Solution 22x22 : [((8, 2), (8, 3)), ((18, 17), (19, 18)), ((6, 20), (7, 19))] found in 311.013708ms
Solution 22x22 : [((18, 4), (19, 5)), ((16, 9), (17, 9)), ((21, 16), (0, 17))] found in 309.334494ms
Solution 22x22 : [((14, 8), (15, 9)), ((12, 12), (13, 13)), ((4, 18), (4, 19))] found in 313.065376ms
Solution 22x22 : [((19, 14), (20, 14)), ((21, 20), (0, 19)), ((6, 21), (7, 20))] found in 308.505905ms
Solution 22x22 : [((20, 2), (21, 3)), ((15, 4), (16, 4)), ((6, 14), (6, 15))] found in 312.598244ms
Solution 22x22 : [((17, 9), (18, 8)), ((17, 12), (18, 11)), ((17, 14), (18, 13))] found in 311.316036ms
Solution 22x22 : [((5, 0), (5, 1)), ((14, 0), (15, 1)), ((21, 7), (21, 8))] found in 312.269791ms
Solution 22x22 : [((10, 1), (11, 0)), ((11, 9), (11, 10)), ((1, 19), (2, 18))] found in 312.084957ms
Solution 22x22 : [((3, 1), (4, 2)), ((15, 3), (16, 4)), ((12, 21), (13, 20))] found in 314.289588ms
Solution 25x25 : [((16, 10), (17, 11)), ((7, 18), (8, 17)), ((0, 20), (1, 21))] found in 385.887212ms
Solution 25x25 : [((11, 3), (12, 4)), ((5, 6), (6, 7)), ((3, 7), (4, 8))] found in 385.079971ms
Solution 25x25 : [((8, 4), (9, 3)), ((4, 17), (4, 18)), ((1, 24), (2, 24))] found in 382.665797ms
Solution 25x25 : [((17, 10), (17, 11)), ((8, 21), (9, 22)), ((6, 24), (7, 0))] found in 385.28435ms
Solution 25x25 : [((24, 18), (0, 17)), ((18, 19), (19, 18)), ((11, 22), (12, 21))] found in 380.496836ms
Solution 30x30 : [((12, 7), (13, 6)), ((16, 17), (17, 18)), ((7, 26), (8, 26))] found in 525.010214ms
Solution 30x30 : [((28, 13), (29, 13)), ((27, 14), (28, 14)), ((12, 23), (13, 22))] found in 537.698897ms
Solution 30x30 : [((2, 1), (3, 0)), ((23, 17), (24, 16)), ((15, 27), (16, 26))] found in 535.699594ms
Solution 30x30 : [((17, 2), (18, 2)), ((17, 26), (18, 25)), ((18, 27), (19, 26))] found in 536.358668ms
Solution 30x30 : [((8, 2), (9, 2)), ((8, 18), (9, 19)), ((5, 28), (6, 27))] found in 540.985969ms
Congratulations! Here is your flag: FCSC{1eda491c7c2e2f530234cbfde411ebfa7a1d12e2ac9de43e1c8b0ea3aa351487}
real 0m20,834s
user 0m1,083s
sys 0m0,180s