【问题标题】:algorithm to randomly & efficiently place 100 circles without any overlap?随机有效地放置100个圆圈而没有任何重叠的算法?
【发布时间】:2016-07-01 18:21:51
【问题描述】:

我正在尝试编写一个脚本,将 100 个不同大小的圆圈放在舞台上。我在下面概述了简洁的要求。

鉴于以下情况:

var stage;       // contains a "width" and "height" property.
var circle;      // the circle class. contains x, y, radius & a unique id property.
var circleArray; // contains 100 circle instances

要求:

  • 编写一个函数,在舞台上放置 100 个不同半径的圆。
  • 展示位置必须是随机的,但分布均匀(无聚集)。
  • 展示位置必须是高性能的 - 这将在移动网络浏览器上执行。
  • 圆圈不得与其他圆圈相交/重叠。
  • circle.x >= 0 必须为真。
  • circle.y >= 0 && circle.y <= stage.height 必须为真。
  • 圆可以具有以下任何半径大小(在创建时分配):
    • 150
    • 120
    • 90
    • 80
    • 65

我目前的尝试是一种蛮力方法,它不能有效地运行。如果我尝试插入超过 10 个圆圈,浏览器就会挂起。以下是我当前的实现,我完全可以放弃它以支持更高性能/更好的实现。

这是一个现场演示(注意:没有实际的绘图代码,只是逻辑,但它仍然会锁定浏览器,所以请注意!!)http://jsbin.com/muhiziduxu/2/edit?js,console

function adjustForOverlap (circleArray) {
  // a reference to the circle that is invoking this function.
  var _this = this;

  // remove this circle from the array we are iterating over.
  var arr = circleArray.filter(function (circle){
    return circle.id !== _this.id;
  });

  // while repeat == true, the circle may be overlapping something.
  var repeat = true;
  while(repeat) {
    var hasOverlap = false;
    for (var i=0; i<arr.length; i++) {
      var other = arr[i];
      var dx = _self.x - other.x;
      var dy = _self.y - other.y;
      var rr = _self.radius + other.radius;
      if (dx * dx + dy * dy < rr * rr) {
        // if here, then an overlap was detected.
        hit = true;
        break;
      }
    }
    // if hit is false, the circle didn't overlap anything, so break.
    if (hit === false) {
      repeat = false;
      break;
    } else {
      // an overlap was detected, so randomize position.
      _self.x = Math.random() * (stage.width*2);
      _self.y = Math.random() * stage.height;
    }
  }
}

【问题讨论】:

  • 其实如果空间很小,半径不能为零,可能没有办法解决。这将永远迭代。
  • @Oriol 垂直空间是有限的(即 0 - 500),但是可以根据需要使用尽可能多的水平空间(stage.width * n)

标签: javascript algorithm heuristics


【解决方案1】:

lots of efficient collision detection algorithms。它们中的许多通过将空间划分为单元格并通过有效查找单元格中的其他对象来维护单独的数据结构来工作。基本步骤是:

  1. 为您的新圈子确定一个随机位置
  2. 确定它所在的单元格
  3. 在每个单元格中查找碰撞
  4. 如果发生碰撞,请转到 1。
  5. 否则,将新圆圈添加到它重叠的每个单元格中。

您可以为单元格数据结构使用简单的方形网格(即二维数组),或类似quadtree 之类的其他东西。在某些情况下,您还可以通过先尝试廉价但粗略的碰撞检查(边界框是否重叠)来获得一些额外的速度,如果返回 true,请尝试稍微昂贵且精确的检查。

更新

对于四叉树,请查看d3-quadtree,它应该会给你一个很好的实现,并附有示例。

对于(非常快速、未经测试的)二维数组实现:

function Grid(radius, width, height) {
    // I'm not sure offhand how to find the optimum grid size.
    // Let's use a radius as a starting point
    this.gridX = Math.ceil(width / radius);
    this.gridY = Math.ceil(height / radius);
    // Determine cell size
    this.cellWidth = width / this.gridX;
    this.cellHeight = height / this.gridY;
    // Create the grid structure
    this.grid = [];
    for (var i = 0; i < gridY; i++) {
        // grid row
        this.grid[i] = [];
        for (var j = 0; j < gridX; j++) {
            // Grid cell, holds refs to all circles
            this.grid[i][j] = []; 
        }
    }
}

Grid.prototype = {
    // Return all cells the circle intersects. Each cell is an array
    getCells: function(circle) {
        var cells = [];
        var grid = this.grid;
        // For simplicity, just intersect the bounding boxes
        var gridX1Index = Math.floor(
            (circle.x - circle.radius) / this.cellWidth
        );
        var gridX2Index = Math.ceil(
            (circle.x + circle.radius) / this.cellWidth
        );
        var gridY1Index = Math.floor(
            (circle.y - circle.radius) / this.cellHeight
        );
        var gridY2Index = Math.ceil(
            (circle.y + circle.radius) / this.cellHeight
        );
        for (var i = gridY1Index; i < gridY2Index; i++) {
            for (var j = gridX1Index; j < gridX2Index; j++) {
                // Add cell to list
                cells.push(grid[i][j]);
            }
        }
        return cells;
    },
    add: function(circle) {
        this.getCells(circle).forEach(function(cell) {
            cell.push(circle);
        });
    },
    hasCollisions: function(circle) {
        return this.getCells(circle).some(function(cell) {
            return cell.some(function(other) {
                return this.collides(circle, other);
            }, this);
        }, this);
    },
    collides: function (circle, other) {
        if (circle === other) {
          return false;
        }
        var dx = circle.x - other.x;
        var dy = circle.y - other.y;
        var rr = circle.radius + other.radius;
        return (dx * dx + dy * dy < rr * rr);
    }
};

var g = new Grid(150, 1000, 800);
g.add({x: 100, y: 100, radius: 50});
g.hasCollisions({x: 100, y:80, radius: 100});

这是一个功能齐全的示例:http://jsbin.com/cojoxoxufu/1/edit?js,output

请注意,这只显示 30 个圆圈。看起来问题通常无法通过您当前的半径、宽度和高度来解决。设置为在放弃和接受碰撞之前为每个圆圈查找多达 500 个位置。

【讨论】:

  • 四叉树看起来有点复杂,但我很想看看你描述的二维数组方法的实现。
  • 添加了一个示例实现。玩得开心:)
  • 这真的很酷!!!尽管我似乎陷入了一个错误 - 添加我的圈子后,当我检查 hasCollisions() 时,它会在 getCells() 内引发错误,特别是 cells.push(this.grid[i][j]) 中的 [j] 对于某些迭代似乎是未定义的。目前正在尝试调试,但如果您有任何见解,我会全力以赴 :) 非常感谢您的帮助
  • 我不是肯定的,但似乎单元格宽度/单元格高度没有得到正确计算。即我通过半径为 150,但得到的单元格宽度/高度为 148/120。这是否意味着我的舞台根本不够大?
  • 那是因为它使用Math.ceil 作为单元格的数量。使用Math.floor 会给你更大的单元格,这可能会更好(它会确保一个圆圈会跨越更少的单元格)。但无论哪种方式,除非您的高度和宽度完全可以被半径整除,否则您将无法获得准确的单元格大小。
猜你喜欢
  • 1970-01-01
  • 2015-02-28
  • 1970-01-01
  • 1970-01-01
  • 2018-02-01
  • 2018-03-24
  • 1970-01-01
  • 1970-01-01
  • 2021-03-16
相关资源
最近更新 更多