refactor flood fill to use a more optimal algorithm

This commit is contained in:
tali 2023-04-14 18:59:49 -04:00
parent 5f653db2c9
commit 5e069cf3ff
3 changed files with 60 additions and 26 deletions

1
Cargo.lock generated
View File

@ -405,6 +405,7 @@ dependencies = [
"criterion",
"hashbrown 0.13.2",
"mino",
"smallvec",
"tracing",
]

View File

@ -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]

View File

@ -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<Range<i16>> {
/// `(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<Item = i16> {
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() {