从二维列表中查找与最多圆相交的线段

Find line segment that intersects the most circles from a list in 2D

我有一个圆列表 (x, y, radius) 和一条线原点 (x, y),我希望能够确定一条线段,该线段与尽可能多的圆与其中一个点相交作为线的原点,这条线是完全直的。这都是二维的。

圆有固定的半径并且可以重叠。线与圆相交没有最小距离限制。

一张我想要的图片:

伪代码很好。

更新:

在评论和回答的一些帮助和想法的帮助下,我已经想出了如何做到这一点,不过它可能需要一些优化。

我创建了两个 codepen,其中一个有试图使角度居中的代码,但更耗费资源,而另一个则没有。

无居中: https://codepen.io/joshiegemfinder/pen/gOXzXOq

居中: https://codepen.io/joshiegemfinder/pen/abVGLRJ

我将使用 javascript 编写代码示例,因为这可能是最容易理解的语言

要找到与最多圆相交的线段,您必须像这样取每个圆的两条切线(通过线原点):

let diffX = circle.x - origin.x
let diffY = circle.y - origin.y
let dist = Math.sqrt(diffX ** 2 + diffY**2)
let asine = Math.asin(circle.radius / dist)
let angle = Math.atan2(diffY, diffX)

let tangent1 = angle - asine
let tangent2 = angle + sinAngle

let pos1 = {x: circle.x + circle.radius * Math.sin(tangent1), y: circle.y + circle.radius * -Math.cos(tangent1)}
let pos2 = {x: circle.x + circle.radius * -Math.sin(tangent2), y: circle.y + circle.radius * Math.cos(tangent2)}

let dist1 = Math.sqrt((pos1.x - origin.x)**2 + (pos1.x - origin.y)**2)
let dist2 = Math.sqrt((pos2.x - origin.x)**2 + (pos2.x - origin.y)**2)

return {pos1: pos1, pos2: pos2, angle1: Math.atan2(pos1.y - origin.y, pos1.x - origin.x), angle2: Math.atan2(pos2.y - origin.y, pos2.x - origin.x), dist1: dist1, dist2: dist2, circle: circle, dist: dist}

这会产生一个“间隔”(由@Stef 命名),如果线段足够长,任何小于角度 1 但大于角度 2 (angle1 <= angleX <= angle2) 的角度都将与圆相交

然后,对于每个交叉点,您检查两个角度与其他间隔的对比,检查角度是否在间隔范围内,如果是,则增加其计数,对所有间隔重复此操作,(您还需要存储这个区间相交的最大距离)当你得到交叉点的计数时,保存交叉点的角度和数量,并每隔一个间隔重复一次(不幸的是,这是一个嵌套在另一个循环中的循环),将变量替换为如果相交超过当前存储的间隔,您的角度、距离和交点将被计算在内。

最终,您将得到与最多圆相交的角度,以及与它相交的最远圆的距离,现在可以通过 sin 和 cos 计算线段。

为什么只检查切线?难道不能有一个更远的圆因为它在切线内而不会被检测到吗?不,不可能,这是因为还将检查更远的圆的切线,这意味着它会被检测到。

最终代码(可能有错别字):

function getSegment(circles, origin) {
    //Get all of the intervals
    let intervals = getIntervals(circles, origin)
    
    let maxDist = 0
    let maxIntersections = 0
    let angle = 0
    
    for(interval of intervals) {
        let data1 = getIntersectionData(interval.angle1, intervals)
        let intersections1 = data1.count
        let distance1 = data1.dist
        //if this angle intersects more than the current amount, store this angle's data instead
        //(or this angle intersects the same, but is shorter, feel free to remove this)
        if(intersections1 > maxIntersections || (intersections1 == maxIntersections && distance1 <= maxDist)) {
            maxIntersections = intersections1
            maxDist = distance1
            angle = interval.angle1
        }
        
        let data2 = getIntersectionData(interval.angle2, intervals)
        let intersections2 = data2.count
        let distance2 = data2.dist
        if(intersections2 > maxIntersections || (intersections2 == maxIntersections && distance2 <= maxDist)) {
            maxIntersections = intersections2
            maxDist = distance2
            angle = interval.angle2
        }
    }
    
    let x2 = origin.x + Math.cos(angle) * maxDist
    let y2 = origin.y + Math.sin(angle) * maxDist
    
    return {x1: origin.x, y1: origin.y, x2: x2, y2: y2}
}

function getInterval(circle, origin) {
    let diffX = circle.x - origin.x
    let diffY = circle.y - origin.y
    let dist = Math.sqrt(diffX ** 2 + diffY**2)
    //for asine, you might need to add a catch for when the origin is inside the circle, for JavaScript it just returns NaN and no error is thrown
    let asine = Math.asin(circle.radius / dist)
    let angle = Math.atan2(diffY, diffX)

    let tangent1 = angle - asine
    let tangent2 = angle + asine

    let pos1 = {x: circle.x + circle.radius * Math.sin(tangent1), y: circle.y + circle.radius * -Math.cos(tangent1)}
    let pos2 = {x: circle.x + circle.radius * -Math.sin(tangent2), y: circle.y + circle.radius * Math.cos(tangent2)}

    let dist1 = Math.sqrt((pos1.x - origin.x)**2 + (pos1.x - origin.y)**2)
    let dist2 = Math.sqrt((pos2.x - origin.x)**2 + (pos2.x - origin.y)**2)

    return {pos1: pos1, pos2: pos2, angle1: Math.atan2(pos1.y - origin.y, pos1.x - origin.x), angle2: Math.atan2(pos2.y - origin.y, pos2.x - origin.x), dist1: dist1, dist2: dist2, circle: circle, dist: dist}
}

function getIntervals(circles, origin) {
    //Initialize the array of intervals
    let intervals = []
    
    //Append the interval of each circle to an array
    for(let circle of circles) {
        intervals.push(getInterval(circle))
    }
    
    //Sort the array clockwise by interval.angle1 (Not neccesary or useful)
    intervals.sort((a, b) => a.angle1 - b.angle1)
    
    return intervals
}

function getIntersectionData(angle, intervals) {
    let count = 0
    let dist = 0
    //for each interval:
    for(let inter of intervals) {
        //if the angle is inside the interval (if it'll intersect the circle)
        flag = inter.angle1 <= angle && inter.angle2 >= angle
        //or if the origin is inside the circle (i talked about this at getInterval())
        if((inter.angle1 == NaN || inter.angle2 == NaN) || (flag)) {
            //increase the number of intersections
            count++
            //and if the inteval IS valid, update the distance if it's bigger than the current one
            //(basically get the interval with the largest distance from origin
            if(flag) {
                dist = Math.max(dist, inter.dist)
            }
        }
    }
    return {count: count, dist: dist}
}

按照@Stef 的建议,计算从直线原点到圆的所有切线的角度(在四个象限上)。按三角旋转顺序用 +1 和 -1 标记角度,并将它们递增排序。忽略原点周围的圆圈。

现在形成±1个标签的前缀和,考虑产生最大值的angular区间


要获得圆的角度,请计算圆心的极坐标并加上正负半孔径,其正弦值是圆半径与距离之比 center-origin。