From 5e069cf3ffa3e0be553f1166600bd2ab70c1e954 Mon Sep 17 00:00:00 2001 From: tali Date: Fri, 14 Apr 2023 18:59:49 -0400 Subject: [PATCH] refactor flood fill to use a more optimal algorithm --- Cargo.lock | 1 + fish/Cargo.toml | 1 + fish/src/eval/downstacking.rs | 84 ++++++++++++++++++++++++----------- 3 files changed, 60 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2c4c3e..83cb3dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,6 +405,7 @@ dependencies = [ "criterion", "hashbrown 0.13.2", "mino", + "smallvec", "tracing", ] diff --git a/fish/Cargo.toml b/fish/Cargo.toml index 8b8cf91..872eca7 100644 --- a/fish/Cargo.toml +++ b/fish/Cargo.toml @@ -10,6 +10,7 @@ mino = { path = "../mino" } ahash = "0.8" bumpalo = "3.12" hashbrown = "0.13" +smallvec = "1.10" tracing = { version = "0.1", default_features = false } [dev-dependencies] diff --git a/fish/src/eval/downstacking.rs b/fish/src/eval/downstacking.rs index 9cba368..f177991 100644 --- a/fish/src/eval/downstacking.rs +++ b/fish/src/eval/downstacking.rs @@ -1,8 +1,9 @@ #![allow(dead_code)] use core::ops::Range; -use mino::matrix::EMPTY_ROW; +use mino::matrix::{EMPTY_ROW, FULL_ROW}; use mino::{Mat, MatBuf}; +use smallvec::SmallVec; /// This is the algorithm that mystery and I developed. It computes the "minimum downstack /// estimate", which approximates the minimum pieces to downstack a given board state. It @@ -100,7 +101,8 @@ fn residue(mat: &Mat) -> Option> { /// `(x,y,area)` where `(x,y)` is first point in the region filled in, and `area` is the /// number of empty cells filled. fn flood_fill(rows: &mut [u16]) -> Option<(i16, i16, u32)> { - fn init(rows: &[u16]) -> Option<(i16, i16)> { + // find an empty cell in the matrix somewhere + fn find_empty(rows: &[u16]) -> Option<(i16, i16)> { for (y, row) in rows.iter().enumerate() { // trailing_ones() finds some unoccipied cell in `row`. if the row is full // then this return 16 (since there are 16 bits in the row representation @@ -113,37 +115,67 @@ fn flood_fill(rows: &mut [u16]) -> Option<(i16, i16, u32)> { None } - fn flood(rows: &mut [u16], x: i16, y: i16) -> u32 { - // test if (x,y) is OOB - if x < 0 || y < 0 || (y as usize) >= rows.len() { - return 0; - } - - // try to fill in (x,y) - let idx = y as usize; - let mask = 1 << x; - if rows[idx] & mask != 0 { - return 0; - } - rows[idx] |= mask; - - // FIXME: improve this: - // - the max recursion depth is small (~400?) so we should use an explicit stack - // - scan filling could be more efficient than plain recursion - 1 + flood(rows, x - 1, y) - + flood(rows, x + 1, y) - + flood(rows, x, y - 1) - + flood(rows, x, y + 1) + // given a column in the row that is empty, find the left and rightmost columns that + // are also empty and adjacent to to it. + fn find_span(row: u16, x: i16) -> (i16, i16) { + let left = row & !(FULL_ROW << x); + let right = row & !((1 << x) - 1); + let lx = 16 - left.leading_zeros() as i16; + let rx = right.trailing_zeros() as i16; + (lx, rx) } - let (x0, y0) = init(rows)?; - Some((x0, y0, flood(rows, x0, y0))) + // list the new columns that should be passed on the stack for the given row and range + fn scan(row: u16, lx: i16, rx: i16) -> impl Iterator { + let mut added = false; + (lx..rx).filter_map(move |x| { + if row & (1 << x) != 0 { + added = false; + None + } else if added { + None + } else { + added = true; + Some(x) + } + }) + } + + let mut area = 0; + let (x0, y0) = find_empty(rows)?; + + type Stack = SmallVec<[(i16, i16); 4]>; + let mut stack = Stack::new(); + stack.push((x0, y0)); + + while let Some((x, y)) = stack.pop() { + let row = &mut rows[y as usize]; + + let (lx, rx) = find_span(*row, x); + area += (rx - lx) as u32; + *row |= (FULL_ROW << lx) & !(FULL_ROW << rx); + + if y > 0 { + for x in scan(rows[(y - 1) as usize], lx, rx) { + stack.push((x, y - 1)); + } + } + + if ((y + 1) as usize) < rows.len() { + for x in scan(rows[(y + 1) as usize], lx, rx) { + stack.push((x, y + 1)); + } + } + } + + Some((x0, y0, area)) } #[cfg(test)] mod test { use super::*; - use mino::{mat, matrix::EMPTY_ROW}; + + use mino::mat; #[test] fn test_flood_fill() {