逻辑将策略性地将项目放置在具有最小重叠连接的容器中

时间:2013-08-29 12:31:06

标签: java javascript algorithm layout

这更像是一个算法问题。我有一个页面,使用javaScript通过绘制从源到目标的箭头连接显示项目和项目与其他项目的关系(想想jsPlumb)。每个项目可以有0个或更多连接。我面临的挑战是以最佳方式战略性地将div /圈放置在容器中。

  • 最佳:最少连接数(连接两个圆圈的箭头)重叠

视觉示例:下图是未经优化的显示版本,已将圆圈随机放置在容器内。

enter image description here

请注意,在上图中,连接(箭头)重叠的数量不必要地高。下面的图片是一个优化的解决方案,在这个小例子中,圆圈位于更好的位置,导致连接没有重叠:

enter image description here

放置物品的容器尺寸为1020x800。存在大量圆圈的地方总会有重叠,因此我们的想法是尽量减少连接重叠的数量。我希望能够做到这一点的例子,因为我发现阅读算法文章有点令人生畏:(。

3 个答案:

答案 0 :(得分:7)

方法1

用于布局图的一类非常好的算法是基于仿真的算法。在这些算法中,您可以将图形建模为具有物理属性的物理对象。

在这种情况下,想象图形的节点是相互排斥的球,而边缘是弹簧或橡胶,将图形保持在一起。节点彼此越接近,排斥力越强,例如,它们的距离是平方的,每个弹簧的张力与其长度成正比。排斥力将使节点尽可能远离其他节点,并且图形将解开。当然,你必须稍微试验系数才能获得最佳效果(但我保证 - 这很有趣)。

这种方法的主要优点是:

  1. 易于编码 - 嵌套循环计算每个节点 - 节点对之间的力并更新节点位置
  2. 适用于各种图形平面或非平面
  3. 很有趣的实验
  4. 如果你让它互动,例如允许用户用鼠标移动节点 - 它吸引了人们,每个人都想“玩图”

这种方法的缺点是:

  1. 它可能会陷入局部能量最小值(摇晃或帮助手动帮助)
  2. 它不是非常快(但可以做出漂亮的动画)
  3. 可以使用类似的方法来布局/解开结。

    示例代码

    <html>
    <head>
    </head>
    <body>
      <canvas id="canvas" width="800" height="600" style="border:1px solid black"/>   
      <script>
        window.requestAnimFrame = (function(callback) {
           return window.requestAnimationFrame || window.webkitRequestAnimationFrame || 
                  window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
                  function(callback) {
                     window.setTimeout(callback, 1000 / 120);
                  };
        })();
    
        var width = 800;
        var height = 600;
    
        function addEdge(nodeA, nodeB) {
          if (nodeA.edges.indexOf(nodeB) == -1) {
            nodeA.edges[nodeA.edges.length] = nodeB;
            nodeB.edges[nodeB.edges.length] = nodeA;
          }
        }
    
        function createGraph(count) {
          var graph = new Array();
          for (var i = 0; i < count; i++) {
            var node = new Object();
            node.x = Math.floor((Math.random() * width));  
            node.y = Math.floor((Math.random() * height));
            node.edges = new Array();        
            graph[i] = node;  
            if (i > 0) 
              addEdge(graph[i], graph[i - 1]);        
          }
    
          for (var i = 0; i < count / 2; i++) {
            var a = Math.floor((Math.random() * count));  
            var b = Math.floor((Math.random() * count));  
            addEdge(graph[a], graph[b]);
          }
          return graph;
        }
    
        function drawEdges(ctx, node) {
          for (var i = 0; i < node.edges.length; i++) {
            var otherNode = node.edges[i];
            ctx.beginPath();
            ctx.moveTo(node.x, node.y);
            ctx.lineTo(otherNode.x, otherNode.y);
            ctx.stroke();
          }
        }
    
        function drawNode(ctx, node) {
          ctx.beginPath();
          ctx.arc(node.x, node.y, 30, 0, 2 * Math.PI, false);
          ctx.fillStyle = 'green';
          ctx.fill();
          ctx.lineWidth = 5;
          ctx.strokeStyle = '#003300';
          ctx.stroke();
        }    
    
        function drawGraph(ctx, graph) {
          ctx.fillStyle = 'white';
          ctx.fillRect(0, 0, width, height);
          for (var i = 0; i < graph.length; i++)         
            drawEdges(ctx, graph[i]);      
          for (var i = 0; i < graph.length; i++)         
            drawNode(ctx, graph[i]);      
        }
    
        function distanceSqr(dx, dy) { 
          return dx * dx + dy * dy; 
        }
    
        function force(nodeA, nodeB, distanceFn) {
          var dx = nodeA.x - nodeB.x;
          var dy = nodeA.y - nodeB.y;
          var angle = Math.atan2(dy, dx);
          var ds = distanceFn(distanceSqr(dx, dy));
          return { x: Math.cos(angle) * ds, y: Math.sin(angle) * ds };
        }
    
        function repelForce(distanceSqr) {
          return 5000.0 / distanceSqr;
        }
    
        function attractForce(distanceSqr) {
          return -distanceSqr / 20000.0;
        }
    
        function gravityForce(distanceSqr) {
          return -Math.sqrt(distanceSqr) / 1000.0;
        }
    
    
        function calculateForces(graph) {
          var forces = new Array();  
          for (var i = 0; i < graph.length; i++) {
            forces[i] = { x: 0.0, y: 0.0 };
    
            // repelling between nodes:
            for (var j = 0; j < graph.length; j++) {
              if (i == j)
                continue;
              var f = force(graph[i], graph[j], repelForce);
              forces[i].x += f.x;
              forces[i].y += f.y;
            }
    
            // attraction between connected nodes:
            for (var j = 0; j < graph[i].edges.length; j++) {
              var f = force(graph[i], graph[i].edges[j], attractForce);
              forces[i].x += f.x;
              forces[i].y += f.y;          
            }          
    
            // gravity:
            var center = { x: 400, y: 300 };
            var f = force(graph[i], center, gravityForce);
            forces[i].x += f.x;
            forces[i].y += f.y;           
          }  
          return forces;
        }
    
        function updateNodePositions(graph) {
          var forces = calculateForces(graph);
          for (var i = 0; i < graph.length; i++) {
            graph[i].x += forces[i].x;      
            graph[i].y += forces[i].y;           
          }  
        }    
    
        function animate(graph) {    
          var ctx = document.getElementById("canvas").getContext("2d");
          for (var i = 0; i < 20; i++)
            updateNodePositions(graph);
          drawGraph(ctx, graph);
          requestAnimFrame(function() { animate(graph); });
        }
    
        animate(createGraph(8));
      </script>
    </body>
    </html>
    

    您可以看到此代码的工作原理here。刷新页面以获取不同的图形。 当然,有时它找不到全局最小值并且有更多的交叉边缘 - 所以如果结果不满足你,你可以添加随机抖动。

    方法2

    这个问题类似于PCB设计中的布线问题。如果您对方法1提供的简单易用的解决方案不满意,您可以使用自动布线方法改进解决方案。例如。您可以将节点放在网格上,然后使用A *算法查找连接它们的最短路径。

    1. 使用方法1查找次优的初始解决方案(可选)。
    2. 删除所有边缘。将节点放在网格上(将其坐标向上舍入)。网格必须具有足够的分辨率,以便没有两个节点重叠。
    3. 按升序近似长度对边缘进行排序(使用欧几里德或曼哈顿指标)。
    4. 对于每个边缘使用A *算法来查找连接节点的最短路径。作为成本函数,不仅要使用与源节点的距离,还要为步进到之前路由的任何边缘已经占用的任何网格点添加足够的大额惩罚。
    5. 将上一步中找到的路径上的网格点标记为“已拍摄”,因此所有下一条边将有利于不踩踏/与此路径相交的路径。
    6. 上述算法是一种贪婪的启发式算法,遗憾的是它并不能保证最优解,因为结果取决于路由边缘的顺序。您可以通过移除跨越另一条边的随机边缘并重新路由它来进一步改进解决方案。

      步骤1.是可选的,使图形布局更规则,使平均连接距离变小,但不应影响交叉点的数量(如果网格有足够的分辨率)。

答案 1 :(得分:2)

看起来像是简单的闭合多边形提取给我。试试这个:

  1. 忘记连接的方向 删除所有冗余连接(双向连接是重复的)
  2. 找到所有闭环

    起始点始终是具有2个以上连接的容器(或者只是1个),因此循环通过未使用的相邻容器,直到返回起点(设置此路径为已使用)或直到到达端点(仅1个连接,也设置为使用此路径或直到到达十字路口(连接&gt; 2,也将此路径设置为已使用)。

  3. 重复,直到容器之间没有未使用的行。

  4. 之后,您将图表分解为不相交的部分。

    现在将它们连接在一起,因此没有连接相交。共享连接在内部,非共享连接在外部。开环(带端点)可以在任何地方。

    我希望这会有所帮助

答案 2 :(得分:0)

我认为基于模拟的算法是最好的选择,但是,因为你的目标是最小化重叠弧而不是优化节点的分布,你应该在弧之间(而不是节点之间)应用排斥力并使用节点作为弹簧。

迭代:

  1. 对于图中的每个弧,计算其中心点(将起点与终点平均)
  2. 每对弧线在它们的中心之间施加排斥(弧的两个极端相应地移动)
  3. 对于图中的每个节点计算其新位置作为连接弧的平均值并更新弧的相关端点
  4. 您还可以添加收缩阶段,吸引到图表中间的节点(所有节点坐标的平均值)。

    在达到某个稳定性阈值时停止迭代。