0

I want to know how to change this algorithm to return new array of updated things instead of changing it in global map array. It's using JS implementation of Flood Fill algorithm.

This is the array I have:

var map = [
    [0, 0, 0, 1, 1],
    [0, 0, 0, 1, 0],
    [0, 0, 1, 1, 0],
    [0, 0, 0, 1, 0],
    [0, 0, 0, 0, 0],
    [0, 1, 1, 1, 1]
];

And this is the result I want to get out of calling my floodFill function:

result = [
    [0, 2, 2],
    [0, 2, 0],
    [2, 2, 0],
    [0, 2, 0]
];

Here is the whole source(that's just changing connected ones in original array to twos):

var map = [
    [0, 0, 0, 1, 1],
    [0, 0, 0, 1, 0],
    [0, 0, 1, 1, 0],
    [0, 0, 0, 1, 0],
    [0, 0, 0, 0, 0],
    [0, 1, 1, 1, 1]
];

function floodFill(data, x, y, newValue) {
    // get target value
    var target = data[x][y];

    function flow(x,y) {
        // bounds check what we were passed
        if (x >= 0 && x < data.length && y >= 0 && y < data[x].length) {
            if (data[x][y] === target) {
                data[x][y] = newValue;
                flow(x-1, y);
                flow(x+1, y);
                flow(x, y-1);
                flow(x, y+1);
            }
        }
    }

    flow(x,y);
}

var result = floodFill(map, 1, 3, 2);
/*
result = [
    [0, 2, 2],
    [0, 2, 0],
    [2, 2, 0],
    [0, 2, 0]
]
*/
Hp840
  • 21
  • 2

2 Answers2

0

You need to clone the 2d array first. You can perform a so-called deep-clone of data, copying it into a new 2d array called newData. Then you can mutate this copy.

function floodFill(data, x0, y0, newValue) {
    var minX = x0, maxX = x0;
    var minY = y0, maxY = y0;

    // perform a deep clone, copying data to newData
    var newData = [];
    for (var i = 0; i < data.length; i++)
        newData[i] = data[i].slice();

    // from now on we make modifications to newData, not data
    var target = newData[x0][y0];
    function flow(x,y) {
        if (x >= 0 && x < newData.length && y >= 0 && y < newData[x].length) {
            if (newData[x][y] === target) {
                minX = Math.min(x, minX);
                maxX = Math.max(x, maxX);
                minY = Math.min(y, minY);
                maxY = Math.max(y, maxY);

                newData[x][y] = newValue;
                flow(x-1, y);
                flow(x+1, y);
                flow(x, y-1);
                flow(x, y+1);
            }
        }
    }
    flow(x0,y0);

    // shrink array (cut out a square)
    var result = [];
    for (var i = minX; i < maxX + 1; i++)
        result[i] = newData[i].slice(minY, maxY + 1);
    return result;
}

To get rid of the zeros around the result, you need to keep track of the minimum and maximum x and y values that the flood fill managed to reach, then cut out a square of [minX..maxX] x [minY..maxY], from the result.

James Lawson
  • 8,150
  • 48
  • 47
  • This just returns whole updated array, the problem here is that I want to get rid of zeros around, as you can see in last commented lines of my example – Hp840 Jun 30 '17 at 06:04
  • You're right. I've adjusted my algorithm to get rid of the zeros around the result. Now the algorithm (i) does not modify original grid (ii) cuts out zeros – James Lawson Jun 30 '17 at 06:44
  • By the way, this will result in infinite recursion as-is. – Patrick Roberts Jun 30 '17 at 06:44
  • @PatrickRoberts I think the previous version was missing an if statement. Please check updated version. – James Lawson Jun 30 '17 at 06:46
  • @JamesLawson oh nevermind. It only results in infinite recursion when the `target` and `newValue` are the same, but that's still a problem with your algorithm. You also have all your `[x][y]` reversed. It's row-major order, not column-major order. The output `result` is wrong. – Patrick Roberts Jun 30 '17 at 06:51
  • @PatrickRoberts The output of `floodFill(map, 1, 3, 2)` for my algorithm is `result = [ [ 0, 2, 2 ], [ 0, 2, 0 ], [ 2, 2, 0 ], [ 0, 2, 0 ] ]` which what the OP asked for -- so I don't think it's wrong. Also, why is using column-major wrong? Using row-major or column-major doesn't matter. You can still perform a floodFill with either. The OP chose column-major and to be consistent, so did I. Perhaps column-major is easier for his/her application ... maybe indexing by x first makes more sense ... we don't actually know, but to assume one way is better seems a bit much here. – James Lawson Jun 30 '17 at 18:48
  • @JamesLawson OP chose row-major order, based on how they declared their array, but erroneously indexed in column-major. – Patrick Roberts Jun 30 '17 at 18:58
0

Here's a stack-based, iterative, out-of-place flood fill algorithm that returns the area of interest copied as a 2D sub-array after applying the flood fill with newValue:

function floodFill (data, x, y, newValue) {
  const target = data[y][x]
  const stack = []
  const fill = []
  const directions = {
    0: { x: -1, y:  0 },
    1: { x:  1, y:  0 },
    2: { x:  0, y: -1 },
    3: { x:  0, y:  1 }
  }
  const { X = 0, Y = 1, DIR = 2 } = {}
  let [minX, maxX, minY, maxY] = [x, x, y, y]
  
  function isChecked (x, y) {
    const hash = `${x},${y}`
    const checked = isChecked[hash]
    
    isChecked[hash] = true
    
    return checked
  }
  
  // validates spot as target
  function isValid (x, y) {
    return (
      x >= 0 && x < data[0].length &&
      y >= 0 && y < data.length &&
      data[y][x] === target &&
      !isChecked(x, y)
    )
  }

  // start with x, y  
  stack.push([x, y, [0, 1, 2, 3]])

  // continue flood fill while stack is not empty
  while (stack.length > 0) {
    let top = stack.pop()

    // if there are directions left to traverse
    if (top[DIR].length > 0) {
      // get next direction
      let dir = top[DIR].pop()
      let delta = directions[dir]
      // remember this spot before traversing
      stack.push(top)
      // calculate next spot
      top = [top[X] + delta.x, top[Y] + delta.y, [0, 1, 2, 3]]

      // if next spot doesn't match target value, skip it
      if (!isValid(...top)) continue

      // traverse to next spot
      stack.push(top)
    } else {
      // we're done with this spot
      // expand area of interest
      minX = Math.min(minX, top[X])
      maxX = Math.max(maxX, top[X])
      minY = Math.min(minY, top[Y])
      maxY = Math.max(maxY, top[Y])

      // and remember it for filling the copied sub-array later
      fill.push([top[X], top[Y]])
    }
  }

  // now that flood fill is done, get result sub-array and fill it
  const result = []

  // generate result sub-array by copying data
  for (let i = minY; i <= maxY; i++) {
    result.push(data[i].slice(minX, maxX + 1))
  }
  // fill each remembered spot with newValue
  for (let i = 0; i < fill.length; i++) {
    let [gx, gy] = fill[i]
    let [rx, ry] = [gx - minX, gy - minY]

    result[ry][rx] = newValue
  }

  return result
}

const map = [
  [0, 1, 1, 1, 1],
  [0, 1, 0, 1, 0],
  [0, 1, 1, 1, 0],
  [0, 0, 0, 1, 0],
  [0, 0, 0, 0, 0],
  [0, 1, 1, 1, 1]
]

let result = floodFill(map, 3, 1, 2)

console.log(stringify(map))
console.log(stringify(result))

// for console.log(), ignore this
function stringify(data) {
  return data.map(row => row.join(', ')).join('\n')
}

The reason isChecked() has to be used is to avoid infinite iteration by ensuring any single spot is not a valid candidate more than once.

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153