Find most unpopulated points in a coordinate system
Asked Answered
G

3

7

I have a coordinate system which basically represents a screen.
And I have an arbitrary amount of positions. E.g.:

population = [
    {x: 100.44, 200.54},
    {x: 123.45, 678.9},
    {x: 1300.23, 435.81},
    {x: 462.23, 468.37},
    {x: 956.58, 385.38},
];

I'm looking for an algorithm that finds the most unpopulated point.

The white mini circles represent the population and the red Xs mark points that appear very unpopulated to me:

screenshot

My goal is run an animation that randomly moves all these white mini circles into random directions and as soon as a circle has left the screen it should get teleported to the most unpopulated spot so that the amount of big empty spaces gets reduced.

I have tried to achieve that by calculating the sum of distances from every integer coordinate to every circle and then choosing the coordinate that has the highest distance sum. That alone seems to be pretty CPU intensive already, but I noticed that this algorithm causes the circles to teleport to the borders of my coordinate system. So I also added the sums of the distances from each integer coordinate to every border integer coordinate. At that point, the script essentially freezes. So this is definitely not the right approach.

I'm running out of ideas. I guess I don't need a perfect algorithm, but rather one with a healthy balance between precision and performance. In the end I want to be able to run that algorithm multiple times per second on 1920x1080 canvas with around 80 of these minicircles. Ideally the algorithm would have a parameter to adjust the accuracy and thus how much CPU time it uses.

This was my approach mentioned above. I commented out the lines that caused the script to just freeze:

let circles = [
    {x: 60.44, y: 190.54},
    {x: 103.45, y: 18.9},
    {x: 390.23, y: 135.81},
    {x: 302.23, y: 28.37},
    {x: 56.58, y: 85.38},
]

function getDistance(p1, p2) {
    return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
}
function drawCircle(ctx,x,y,r,c) {
    ctx.beginPath()
    ctx.arc(x, y, r, 0, 2 * Math.PI, false)
    ctx.fillStyle = c
    ctx.fill()
}


const canvas = document.getElementById('canvas')
const ctx = canvas.getContext("2d")

let highestDistanceSum = 0
let coordWithHighestDistanceSum
for (let x=0; x<canvas.width; x++) {
    for (let y=0; y<canvas.height; y++) {
        let canvasCoord = {x: x, y: y}
        let distanceSum = 0
        for (let circle of circles) {
            distanceSum += getDistance(canvasCoord, circle)
        }
        /*
        // Pretend as if every pixel on the border is a circle
        // Causes massive CPU usage
        for (let x2=0; x<canvas.width; x2++) {
            distanceSum += getDistance(canvasCoord, {x: x2, y: 0})
            distanceSum += getDistance(canvasCoord, {x: x2, y: canvas.height})
        }
        for (let y2=0; y<canvas.height; y2++) {
            distanceSum += getDistance(canvasCoord, {x: 0, y: y2})
            distanceSum += getDistance(canvasCoord, {x: canvas.width, y: y2})
        }
        */
            
        if (distanceSum > highestDistanceSum) {
            coordWithHighestDistanceSum = canvasCoord
            highestDistanceSum = distanceSum
        }
    }
}


for (let p of circles) {
    drawCircle(ctx, p.x, p.y, 3, 'black')
}

drawCircle(ctx, coordWithHighestDistanceSum.x, coordWithHighestDistanceSum.y, 5, 'red')
<canvas id="canvas" width="400" height="200" style="border:1px solid #d3d3d3;"></canvas>
Glynas answered 4/2, 2019 at 12:41 Comment(10)
You calculate the sum of distances to all other points. Would it not make more sense to calculate the sum of distances to a few of the closest points instead? Or even just use the distance to the one closest point?Sheldon
I think your problem isn't the code itself but the idea. So I suggest that you make an N number of circles maybe min(canvas.width, canvas.height)/10 with a rayon of min(canvas.width, canvas.height)/15 or something like that, then check the the circles that contain the fewest pointsBeem
You are trying to get a uniform density of points on the screen. So instead of calculating distances from coordinates, just count the number of points in each square. And choose any random point in the square with the least number of white points. The smaller your squares the better will your visual look. Additionally, you can also calculate the number of points in each square by adding/substracting as the particles cross boundaries instead of recalculating everytime to get even more efficiency.Lipstick
Since you're only looking for approximations, one thing that might speed you along is to measure only the taxicab distance (difference in x coordinates plus difference in y coordinates) rather than including the squaring and square root operations necessary for Euclidean distance.Offing
Good points by, evgeni fotia and gautam1168. A non-optimal solver might do well enough and have much better performance. Although it's not exactly an answer to the question as it stands, it might be the best approach to use for Forivin's program.Sheldon
@Sheldon But to only calculate the distance to the closest point, I'd still have to check the distance to all points because how else would I know which ones are closest?Glynas
@Lipstick That actually sounds like a pretty smart approach.Glynas
@Glynas yes, that's right, you still have to calculate distance to all. It's not in order to improve performance, but in order to change behavior of the algorithm.Sheldon
Search google for "Maximal rectangle algorithm". It will find you the largest rectangle that contains no point. An implementation gave me result around 0.1sec for 1920x1080px with 2000 points.Frisby
Have you seen Mitchell's Best Candidate algorithm? bl.ocks.org/mbostock/1893974Featherbrain
G
5

(Since it's white dot on black canvas, I'll call the white points star for easy distinction)

First of all your solution doesn't seem to fit your criteria. You don't need the point with largest sum of distance to all star. You need the point with furthest distance to its nearest star.

To elaborate, let's say for example a situation like this, where there's one star at the center and a large number of stars some distance away from the center:

enter image description here

The "largest distance sum" method would probably give a point somewhere in the red circle (which is too close or even overlap the center star), while what you want would more like something in the green circle:

enter image description here

With that in mind:

  1. First of all we should divide the screen into reasonably-sized squares, we will build a distance map from those squares to find the one that has largest distance from any star.

The "reasonably-sized" part is quite important performance-wise. You used the 1920x1080 resolution, which is way too fine-grained in my opinion. To get a visually pleasing enough result, a resolution of 48x30 or even 32x20 would be more than enough.

  1. To actually build the distance map, we can simply use a Breadth-first Search. We convert all stars' position to grid coordinate, and use those as BFS's starting positions.

The result would be something like this:

enter image description here

There's still one big problem here: the red-est square is at the bottom edge!

Turn out edge and corner squares have a "cheating" advantage over center coords, since there's no star toward one side (or even 3 sides, for corner squares). So it is very likely that a corner and edge square will have largest distance to any star.

It is not very visually pleasing for your, um, art piece? So we can cheat a bit by filtering out results that are within a certain padding. Luckily BFS' results are sorted by default, so we can just iterate over the result until we find one that fit within the desired area.

Below is the full code with comment. Even with distance map visible the whole process take 20ms, which should be enough for a webgl piece (which run @ max 30fps ~ 33ms/frame)

This solution will also take care of rare cases where several star move out of bound at the same frame. In that case just fetch several different coords from BFS's result.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body style="margin: 0; padding: 0;">
  <canvas id="canvas" style="display: block;"></canvas>
  <script>
    // higher GRID_WIDTH = better result, more calculation time
    // We will caculate gridHeight later based on window size
    const GRID_WIDTH = 48;
    const GRID_PADDING = 3;

    const heatMapColors = [
      '#ffffff',
      '#ffdddd',
      '#ffbbbb',
      '#ff9999',
      '#ff7777',
      '#ff5555',
      '#ff3333',
      '#ff0000'
    ]

    const init = () => {
      var circles = [];
      for (var i = 0; i < 90; i++) {
        circles.push({
          x: Math.random() * window.innerWidth,
          y: Math.random() * window.innerHeight
        });
      }

      const canvas = document.getElementById('canvas')
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      const ctx = canvas.getContext("2d");

      const cellSize = window.innerWidth / GRID_WIDTH;
      const gridHeight = Math.ceil(canvas.height / cellSize);

      update(ctx, circles, GRID_WIDTH, gridHeight, cellSize);
    }

    const update = (ctx, circles, gridWidth, gridHeight, cellSize) => {
      const start = new Date();

      // Perform a BFS from all stars to find distance of each rect from closest star
      // After BFS visitedCoords will be an array of all grid rect, with distance-from-star (weight) sorted in ascending order
      var bfsFrontier = getGridCoordOfStars(circles, cellSize).map(coord => ({ ...coord, weight: 0 }));
      var visitedCoords = [...bfsFrontier];

      while (bfsFrontier.length > 0) {
        const current = bfsFrontier.shift();
        const neighbors = getNeighbors(current, gridWidth, gridHeight);

        for (let neighbor of neighbors) {
          if (visitedCoords.findIndex(weightedCoord => coordsEqual(weightedCoord, neighbor)) === -1) {
            visitedCoords.push(neighbor);
            bfsFrontier.push(neighbor);
          }
        }
      }

      // Visualize heatmap
      for (let coord of visitedCoords) {
        drawRect(ctx, coord.x * cellSize, coord.y * cellSize, cellSize, cellSize, heatMapColors[Math.min(coord.weight, heatMapColors.length - 1)]);
      }

      const emptiestCoord = getLastCoordWithinPadding(visitedCoords, gridWidth, gridHeight, GRID_PADDING);
      const emptiestPosition = {
        x: (emptiestCoord.x + 0.5) * cellSize,
        y: (emptiestCoord.y + 0.5) * cellSize
      }

      drawCircle(ctx, emptiestPosition.x, emptiestPosition.y, 5, 'yellow');
      for (let p of circles) {
        drawCircle(ctx, p.x, p.y, 3, 'black')
      }

      console.log(`Processing time: ${new Date().getTime() - start.getTime()} ms`);
    }

    const drawCircle = (ctx, x, y, r, c) => {
      ctx.beginPath()
      ctx.arc(x, y, r, 0, 2 * Math.PI, false)
      ctx.fillStyle = c
      ctx.fill()
    }

    const drawRect = (ctx, x, y, width, height, c) => {
      ctx.beginPath();
      ctx.rect(x, y, width, height);
      ctx.fillStyle = c;
      ctx.fill();
    }

    // Convert star position to grid coordinate
    // Don't need to worry about duplication, BFS still work with duplicates
    const getGridCoordOfStars = (stars, cellSize) =>
      stars.map(star => ({
        x: Math.floor(star.x / cellSize),
        y: Math.floor(star.y / cellSize)
      }))

    const coordsEqual = (coord1, coord2) => coord1.x === coord2.x && coord1.y === coord2.y;

    const getNeighbors = (weightedCoord, gridWidth, gridHeight) => {
      var result = [];
      if (weightedCoord.x > 0) result.push({ x: weightedCoord.x - 1, y: weightedCoord.y, weight: weightedCoord.weight + 1 })
      if (weightedCoord.x < gridWidth - 1) result.push({ x: weightedCoord.x + 1, y: weightedCoord.y, weight: weightedCoord.weight + 1 })

      if (weightedCoord.y > 0) result.push({ x: weightedCoord.x, y: weightedCoord.y - 1, weight: weightedCoord.weight + 1 })
      if (weightedCoord.y < gridHeight - 1) result.push({ x: weightedCoord.x, y: weightedCoord.y + 1, weight: weightedCoord.weight + 1 })

      return result;
    }

    // loop through a BFS result from bottom to top and return first occurence inside padding
    const getLastCoordWithinPadding = (coords, gridWidth, gridHeight, padding) => {
      for (let i = coords.length - 1; i > 0; i--) {
        const coord = coords[i];
        if (
          coord.x >= padding
          && coord.x < gridWidth - padding - 1
          && coord.y >= padding
          && coord.y < gridHeight - padding - 1
        ) return coord;
      }

      // This does not happen with current logic, but I leave it here to catch future code changes
      return coords[coords.length - 1];
    }

    init();
  </script>
</body>

</html>

Edit:

I've just read @ArneHugo 's answer, and I see adding the border together with the stars as BFS starting positions will work as well. It is slightly slower but give more pleasing result.

Here's another version that implement their idea:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body style="margin: 0; padding: 0;">
  <canvas id="canvas" style="display: block;"></canvas>
  <script>
    const GRID_WIDTH = 48; // We will caculate gridHeight based on window size

    const heatMapColors = [
      '#ffffff',
      '#ffdddd',
      '#ffbbbb',
      '#ff9999',
      '#ff7777',
      '#ff5555',
      '#ff3333',
      '#ff0000'
    ]

    const init = () => {
      var circles = [];
      for (var i = 0; i < 90; i++) {
        circles.push({
          x: Math.random() * window.innerWidth,
          y: Math.random() * window.innerHeight
        });
      }

      const canvas = document.getElementById('canvas')
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      const ctx = canvas.getContext("2d");

      const cellSize = window.innerWidth / GRID_WIDTH;
      const gridHeight = Math.ceil(canvas.height / cellSize); // calculate gridHeight

      // cache border coords array since it's never changed
      const borderCoords = getBorderCoords(GRID_WIDTH, gridHeight);

      update(ctx, circles, GRID_WIDTH, gridHeight, cellSize, borderCoords);
    }

    const update = (ctx, circles, gridWidth, gridHeight, cellSize, borderCoords) => {
      const start = new Date();

      // Perform a BFS from all stars to find distance of each rect from closest star
      // After BFS visitedCoords will be an array of all grid rect, with distance-from-star (weight) sorted in ascending order

      var bfsFrontier = borderCoords.concat(
        getGridCoordOfStars(circles, cellSize).map(coord => ({ ...coord, weight: 0 }))
      );

      var visitedCoords = [...bfsFrontier];

      while (bfsFrontier.length > 0) {
        const current = bfsFrontier.shift();
        const neighbors = getNeighbors(current, gridWidth, gridHeight);

        for (let neighbor of neighbors) {
          if (visitedCoords.findIndex(weightedCoord => coordsEqual(weightedCoord, neighbor)) === -1) {
            visitedCoords.push(neighbor);
            bfsFrontier.push(neighbor);
          }
        }
      }

      // Visualize heatmap
      for (let coord of visitedCoords) {
        drawRect(ctx, coord.x * cellSize, coord.y * cellSize, cellSize, cellSize, heatMapColors[Math.min(coord.weight, heatMapColors.length - 1)]);
      }

      const emptiestCoord = visitedCoords[visitedCoords.length - 1];
      const emptiestPosition = {
        x: (emptiestCoord.x + 0.5) * cellSize,
        y: (emptiestCoord.y + 0.5) * cellSize
      }

      drawCircle(ctx, emptiestPosition.x, emptiestPosition.y, 5, 'yellow');
      for (let p of circles) {
        drawCircle(ctx, p.x, p.y, 3, 'black')
      }

      console.log(`Processing time: ${new Date().getTime() - start.getTime()} ms`);
    }

    const drawCircle = (ctx, x, y, r, c) => {
      ctx.beginPath()
      ctx.arc(x, y, r, 0, 2 * Math.PI, false)
      ctx.fillStyle = c
      ctx.fill()
    }

    const drawRect = (ctx, x, y, width, height, c) => {
      ctx.beginPath();
      ctx.rect(x, y, width, height);
      ctx.fillStyle = c;
      ctx.fill();
    }

    const getBorderCoords = (gridWidth, gridHeight) => {
      var borderCoords = [];
      for (var x = 0; x < gridWidth; x++) {
        for (var y = 0; y < gridHeight; y++) {
          if (x === 0 || y === 0 || x === gridWidth - 1 || y === gridHeight - 1) borderCoords.push({ x, y, weight: 0 })
        }
      }

      return borderCoords;
    }

    // Convert star position to grid coordinate and filter out duplicates
    const getGridCoordOfStars = (stars, cellSize) => stars.map(star => ({
      x: Math.floor(star.x / cellSize),
      y: Math.floor(star.y / cellSize)
    }))

    const uniqueCoord = (arr) => arr.filter((candidate, index) => arr.findIndex(item => coordsEqual(item, candidate)) === index);

    const coordsEqual = (coord1, coord2) => coord1.x === coord2.x && coord1.y === coord2.y;

    const getNeighbors = (weightedCoord, gridWidth, gridHeight) => {
      var result = [];
      if (weightedCoord.x > 0) result.push({ x: weightedCoord.x - 1, y: weightedCoord.y, weight: weightedCoord.weight + 1 })
      if (weightedCoord.x < gridWidth - 1) result.push({ x: weightedCoord.x + 1, y: weightedCoord.y, weight: weightedCoord.weight + 1 })

      if (weightedCoord.y > 0) result.push({ x: weightedCoord.x, y: weightedCoord.y - 1, weight: weightedCoord.weight + 1 })
      if (weightedCoord.y < gridHeight - 1) result.push({ x: weightedCoord.x, y: weightedCoord.y + 1, weight: weightedCoord.weight + 1 })

      return result;
    }

    init();
  </script>
</body>

</html>
Gerda answered 7/2, 2019 at 14:28 Comment(0)
S
3

Original solution (with edits)

Here is an example, though I think it might be more interesting if you have more circles.

  • I only count the distance to a few of the closest circles
  • As per your request, I've made the borders of the canvas repel new circles, so you're much less likely to get new circles on the border. This is done by counting in the distances to the edges ([canvas.width - coord.x, coord.x - 0, canvas.height - coord.y, coord.y -0]), along with the distances to each of the existing circles.

(I've also changed into a more functional style, because I found that to be easier, though that is not necessary.)

const numberOfCirclesToGetDistanceTo = 2

let circles = [
    {x: 60.44, y: 190.54},
    {x: 103.45, y: 18.9},
    {x: 390.23, y: 135.81},
    {x: 302.23, y: 28.37},
    {x: 56.58, y: 85.38},
]

function getDistance(p1, p2) {
    return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
}
function drawCircle(ctx,x,y,r,c) {
    ctx.beginPath()
    ctx.arc(x, y, r, 0, 2 * Math.PI, false)
    ctx.fillStyle = c
    ctx.fill()
}


const canvas = document.getElementById('canvas')
const ctx = canvas.getContext("2d")

function getCoordWithHighestDistanceSum() {
    const xList = Array(canvas.width).fill().map((_, index) => index)
    const yList = Array(canvas.height).fill().map((_, index) => index)
    const coords = xList.flatMap(x => yList.reduce((coords, y) => coords.concat({ x, y }), []))

    const ascending = (a, b) => a - b
    const sumTotal = (sum, next) => sum + next

    const coordsWithDistance = coords.map(coord => {
        const distances = [
            ...circles.map(circle => getDistance(coord, circle)),
            ...[canvas.width - coord.x, coord.x - 0, canvas.height - coord.y, coord.y -0],
        ]

        return {
            coord,
            dist: distances
                .sort(ascending)
                .slice(0, numberOfCirclesToGetDistanceTo)
                .reduce(sumTotal, 0)
        }
    })

    return coordsWithDistance
        .sort((a, b) => b.dist - a.dist)
        [0].coord
}

const coordWithHighestDistanceSum = getCoordWithHighestDistanceSum()

for (let p of circles) {
    drawCircle(ctx, p.x, p.y, 3, 'black')
}

drawCircle(ctx, coordWithHighestDistanceSum.x, coordWithHighestDistanceSum.y, 5, 'red')
<canvas id="canvas" width="400" height="200" style="border:1px solid #d3d3d3;"></canvas>

Interactive version

Here is a more interactive version so you can test how it works. As you'll see, most of the time, the least densely populated area is along the edge when the other circles are generated randomly. Additionally, the more circles you count in when checking the distance, the more you tend to go towards the edges and the corners when picking the least densely populated area.

The logic for finding least populated area is the same as in the original solution.

let circles = []
let coordWithHighestDistanceSum = void 0

const canvas = document.getElementById('canvas')
const ctx = canvas.getContext("2d")

const xList = Array(canvas.width).fill().map((_, index) => index)
const yList = Array(canvas.height).fill().map((_, index) => index)
const coords = xList.flatMap(x => yList.reduce((coords, y) => coords.concat({ x, y }), []))

function render() {
    ctx.clearRect(0, 0, canvas.width, canvas.height)

    function drawCircle(ctx,x,y,r,c) {
        ctx.beginPath()
        ctx.arc(x, y, r, 0, 2 * Math.PI, false)
        ctx.fillStyle = c
        ctx.fill()
    }

    circles.forEach(circle => drawCircle(ctx, circle.x, circle.y, 3, 'black'))

    if (coordWithHighestDistanceSum) {
        drawCircle(ctx, coordWithHighestDistanceSum.x, coordWithHighestDistanceSum.y, 5, 'red')
    }
}

function generateCircles() {
    const nofCircles = Number(document.getElementById('nofCircles').value)

    const randomCoord = () => coords[Math.floor(Math.random() * coords.length)]

    circles = Array(nofCircles).fill().map(randomCoord)
    findLeastPopulatedCoordinate()

    render()
}

function findLeastPopulatedCoordinate() {
    const nofCirclesToSumDistanceTo = Number(document.getElementById('nofCirclesToSumDistanceTo').value)

    const ascending = (a, b) => a - b
    const sumTotal = (sum, next) => sum + next

    function getDistance(p1, p2) {
        return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
    }

    coordWithHighestDistanceSum = coords
        .map(coord => ({
            coord,
            dist: []
                .concat(circles.map(circle => getDistance(coord, circle)))
                .concat([canvas.width - coord.x, coord.x - 0, canvas.height - coord.y, coord.y -0])
                .sort(ascending)
                .slice(0, nofCirclesToSumDistanceTo)
                .reduce(sumTotal, 0)
        }))
        .sort((a, b) => b.dist - a.dist)
        [0].coord

    render()
}

generateCircles()
findLeastPopulatedCoordinate()
<div>
    <label>Number of circles</label>
    <input type="number" id="nofCircles" value="30" />
</div>
<div>
    <label>Number of circles to sum distance to</label>
    <input type="number" id="nofCirclesToSumDistanceTo" value="1" />
</div>
<button onclick="generateCircles()">Generate circles</button>
<button onclick="findLeastPopulatedCoordinate()">Find least populated coordinate</button>
<canvas id="canvas" width="400" height="200" style="border:1px solid #d3d3d3;"></canvas>
Sheldon answered 4/2, 2019 at 13:7 Comment(4)
Thank you for your answer, this seems to work okay, but it always picks a spot that is on the very edge. What I wanted is to find the least populated point, pretending that the borders are populated as well or something like that, so that I get a result similar to my screenshot/drawing: i.sstatic.net/6LM6X.jpgGlynas
I see! I've updated my answer now so the borders repel new circles, just like the existing circles do.Sheldon
Thanks, this works, but unfortunately it freezes my browser for a minute when I set the resolution to 1920x1080 and use 80 circles. My goal was actually to use this algorithm for an animation where it would be called multiple times per second.Glynas
Ok, I'm not surprised this is too computationally heavy for that. You should look at the comments from evgeni fotia and gautam1168 then. I'd suggest dividing the screen into rectangles of e.g. approximately 100 by 100 pixels, pick the rectangle with fewest circles and place the new circle randomly anywhere in that rectangle. It would be quick, and might feel like it does pretty much the same thing.Sheldon
F
1

You can think of the canvas as a matrix having 1080×1920 columns and rows, initialized with 1s representing blank area and 0s in xth column, yth row represent the point at that coordinate. You now need to find the maximum empty rectangle in a binary matrix.

This Dr. Dobb's article contains one of the fastest algorithms for the problem. You can find a JavaScript implementation on the internet or implement it yourself.

You can also consider finding the maximum empty square.

var canvas = document.querySelector("#canvas-1");
var rctx = canvas.getContext("2d");
var ncols = canvas.width;
var nrows = canvas.height;
var npoints = +canvas.dataset.points;
var matrix = Array(nrows).fill(0).map(function() {
  return Array(ncols).fill(1);
});
var i, x, y, t0, t1, maxrect, maxsquare;

/*
 * For consistency with algorithms, the matrix is initialized with 1s 
 * representing the blank area and the points are represented with 0s
 */
for (i = 0; i < npoints; i++) {
  x = Math.floor(Math.random() * ncols);
  y = Math.floor(Math.random() * nrows);
  matrix[y][x] = 0;
}

t0 = new Date();
maxrect = maximalRectangle(matrix);
t1 = new Date();
console.log("Rectangle found in %dms", t1 - t0);

t0 = new Date();
maxsquare = maximalSquare(matrix);
t1 = new Date();
console.log("Square found in %dms", t1 - t0);

/*
 * Render the results
 */
rctx.fillStyle = "rgba(255,0,0,.5)";
rctx.fillRect(maxrect.left, maxrect.top, maxrect.right - maxrect.left + 1, maxrect.bottom - maxrect.top + 1);

rctx.fillStyle = "rgba(0,0,255,.5)";
rctx.fillRect(maxsquare.left, maxsquare.top, maxsquare.right - maxsquare.left + 1, maxsquare.bottom - maxsquare.top + 1);

rctx.fillStyle = "rgb(255,255,255)";
for (y = 0; y < nrows; y++) {
  for (x = 0; x < ncols; x++) {
    if (matrix[y][x] === 0) {
      rctx.fillRect(x, y, 1, 1);
    }
  }
}

/*
 * implementation of this answer:
 * https://mcmap.net/q/261214/-puzzle-find-largest-rectangle-maximal-rectangle-problem
 */
function maximalRectangle(matrix) {
  var best_area = 0;
  var best_rect = {};
  var M = matrix[0].length;
  var N = matrix.length;
  var c = Array(M + 1).fill(0);
  var s = [];
  var m, n, open_width, area, prev;
  for (n = 0; n < N; n++) {
    for (m = 0; m < M; m++) {
      if (matrix[n][m] === 0) {
        c[m] = 0;
      } else {
        c[m]++;
      }
    }
    open_width = 0;
    for (m = 0; m < M + 1; m++) {
      if (c[m] > open_width) {
        s.push({
          m: m,
          w: open_width
        });
        open_width = c[m];
      } else if (c[m] < open_width) {
        do {
          prev = s.pop();
          area = open_width * (m - prev.m);
          if (area > best_area) {
            best_area = area;
            best_rect.left = prev.m;
            best_rect.right = m - 1;
            best_rect.top = n - open_width + 1;
            best_rect.bottom = n;
          }
          open_width = prev.w;
        } while (c[m] < open_width);
        open_width = c[m];
        if (open_width != 0) {
          s.push(prev);
        }
      }
    }
  }
  return {
    area: best_area,
    left: best_rect.left,
    top: best_rect.top,
    right: best_rect.right,
    bottom: best_rect.bottom
  };
}

/*
 * (possibly buggy) implementation of this answer:
 * https://mcmap.net/q/382730/-dynamic-programming-largest-square-block
 */
function maximalSquare(matrix) {
  var best_length = 0;
  var best_square = {};
  var M = matrix[0].length;
  var N = matrix.length;
  var c = Array(M + 1).fill(0);
  var n, m, temp, prev = 0;
  for (n = 1; n <= N; n++) {
    for (m = 1; m <= M; m++) {
      temp = c[m];
      if (matrix[n - 1][m - 1] === 1) {
        c[m] = Math.min(Math.min(c[m - 1], prev), c[m]) + 1;
        if (best_length < c[m]) {
          best_length = c[m];
          best_square.left = m - best_length;
          best_square.right = m - 1;
          best_square.top = n - best_length;
          best_square.bottom = n - 1;
        }
      } else {
        c[m] = 0;
      }
      prev = temp;
    }
  }
  return {
    area: best_length * best_length,
    left: best_square.left,
    top: best_square.top,
    right: best_square.right,
    bottom: best_square.bottom
  };
}
<canvas id="canvas-1" width="1920" height="1080" data-points="80" style="background-color: #000;"></canvas>
Frisby answered 10/2, 2019 at 19:47 Comment(4)
Interesting, thanks for the answer. Are you sure I need the maximum rectangle and not the maximum circle?Glynas
It is up to you to decide. Finding a circle inscribed inside a rectangle is trivial.Frisby
But couldn't there be a bigger spot that the rectangle wouldn't have fit in? I'm thinking about something like that: i.snag.gy/t7dOqb.jpgGlynas
I see. No, unfortunately it will not handle circular gaps.Frisby

© 2022 - 2024 — McMap. All rights reserved.