实现一个暴力算法来检测自相交多边形

3
我最初实现了Hoey-Shamos算法,但是由于未来的可维护性太复杂(这不在我的控制范围内),而且它没有正确报告,因此我将使用优化过的暴力算法。
我的问题是:如何优化这段代码以使其可用?
目前,我的代码包含一个嵌套的for循环,对同一列表进行两次迭代。
编辑:将行转换为HashSet并使用两个foreach循环...扫描10,000个数据减少了约45秒。但还不够。
foreach (Line2D g in lines)
{                   
foreach (Line2D h in lines)
{
    if (g.intersectsLine(h))
    {
        return false;
    }
}                  

 } // end 'lines' for each loop

如果我强制我的“intersectsLine()”方法返回false(用于测试目的),它仍然需要8秒扫描10,000条记录(我有700,000条记录)。这太长了,所以我需要优化这段代码。
我尝试在将List与所有其他行进行比较后从中删除行,但是存在精度问题(不知道为什么),而且速度提高几乎无法察觉。
这是我的intersectsLine方法。我找到了一种替代方法here,但看起来它会因为所有的方法调用等等而变得更慢。计算斜率似乎对我来说不需要太多计算(如果我错了,请纠正我?)
public bool intersectsLine(Line2D comparedLine)
{

//tweakLine(comparedLine);
if (this.Equals(comparedLine) ||
    P2.Equals(comparedLine.P1) ||
    P1.Equals(comparedLine.P2))
{
    return false;
}

double firstLineSlopeX, firstLineSlopeY, secondLineSlopeX, secondLineSlopeY;

firstLineSlopeX = X2 - X1;
firstLineSlopeY = Y2 - Y1;

secondLineSlopeX = comparedLine.X2 - comparedLine.X1;
secondLineSlopeY = comparedLine.Y2 - comparedLine.Y1;

double s, t;
s = (-firstLineSlopeY * (X1 - comparedLine.X1) + firstLineSlopeX * (Y1 - comparedLine.Y1)) / (-secondLineSlopeX * firstLineSlopeY + firstLineSlopeX * secondLineSlopeY);
t = (secondLineSlopeX * (Y1 - comparedLine.Y1) - secondLineSlopeY * (X1 - comparedLine.X1)) / (-secondLineSlopeX * firstLineSlopeY + firstLineSlopeX * secondLineSlopeY);

if (s >= 0 && s <= 1 && t >= 0 && t <= 1)
{
    //console.WriteLine("s = {0}, t = {1}", s, t);
    //console.WriteLine("this: " + this);
    //console.WriteLine("other: " + comparedLine);
    return true;
}

return false; // No collision */

}

编辑:主要瓶颈在于大的多边形!我排除了超过100条线的多边形,它在5:10内运行了所有70万个多边形。这已经在可接受的范围内了!肯定有一种方法可以在运行此代码之前查看一个多边形是否值得计算吗?如果有必要,我可以访问XMin、Xmax、YMin和YMax属性。

进行了另一项测试,将多边形限制在每个不到1000条线。用时一小时左右。

我删除了对多边形的所有限制,现在已经运行了36个小时...仍然没有结果。

我有几个想法:

- 当我生成我的线哈希集时,还有另一个哈希集/列表,其中包含交点候选项。只有在存在可能相交的情况下,我才会将线添加到此列表中。但是如何消除/添加可能性?如果只有三条线可能与其他线相交,那么我将比较4000条线和3条线,而不是4000条。这本身就可能会有很大的差异。

- 如果同一点出现两次,除了第一个和最后一个点,省略运行嵌套的for循环。

编辑:


关于多边形的信息: 总共有700,000个

有超过四千个多边形具有1,000个或更多点

有2个多边形具有70,000个或更多点


我认为通过一些创意,可以将这个时间缩短到十五分钟左右。

由于这是一个编程网站,所以有一些问题与您计划使用的数据类型有关:FP 还是整数? - chux - Reinstate Monica
你的意思我不太确定?我有一个对象内的函数,它返回真/假。如果多边形具有自相交线,则该函数将返回false。 - Evan Parsons
1
这不是一个完整的答案,但你可以使用四叉树(QuadTrees)(http://en.wikipedia.org/wiki/Quadtree)。这篇不错的文章http://blogs.msdn.com/b/kaelr/archive/2009/05/21/priorityquadtree.aspx有一个很酷的`PriorityQuatree<T>`实现。 - Simon Mourier
我想我需要进行一些修改,我会阅读一些资料,谢谢。 - Evan Parsons
1
你能提供一个样本/测试数据集吗? - RBarryYoung
显示剩余14条评论
3个回答

6
您当前的暴力算法是O(n^2)。仅针对您的两个70,000行多边形,这就是近十亿次操作,更不用说其他700,000个多边形了。显然,单纯的代码优化是不够的,因此您需要一种可以降低O(n^2)但不过于复杂的算法优化。
O(n^2)来自暴力算法中的嵌套循环,每个循环都由n限制,使其成为O(n*n)。最简单的改进方法是找到某种方式来减少内部循环,使其不受n的限制或依赖。因此,我们需要找到一种方法来排序或重新排序内部线段列表,以便只需扫描部分完整列表。
我采取的方法利用了这样一个事实:如果两个线段相交,则它们的X值范围必须重叠。当然,这并不意味着它们会相交,但如果它们的X范围不重叠,则它们不能相交,因此无需彼此检查。(Y值范围也是如此,但一次只能利用一个维度)。
这种方法的优点在于这些X范围可以用于排序线段的端点,这些端点又可以用作起始和停止检查相交的线段。
具体来说,我们定义一个自定义类(endpointEntry),表示线段两个点的高或低X值。这些端点都放在同一个列表结构中,然后根据它们的X值进行排序。
然后我们实现一个外部循环,就像暴力算法一样扫描整个列表。但是我们的内部循环有所不同。相反,我们从外循环线的高X值端点开始扫描已排序的端点列表,并在通过该线的低X值端点时结束。通过这种方式,我们只检查其X值范围与外循环线重叠的线段。
好的,这里是一个演示我的方法的c#类:
class CheckPolygon2
{
    // internal supporting classes
    class endpointEntry
    {
        public double XValue;
        public bool isHi;
        public Line2D line;
        public double hi;
        public double lo;
        public endpointEntry fLink;
        public endpointEntry bLink;
    }
    class endpointSorter : IComparer<endpointEntry>
    {
        public int Compare(endpointEntry c1, endpointEntry c2)
        {
            // sort values on XValue, descending
            if (c1.XValue > c2.XValue) { return -1; }
            else if (c1.XValue < c2.XValue) { return 1; }
            else // must be equal, make sure hi's sort before lo's
                if (c1.isHi && !c2.isHi) { return -1; }
                else if (!c1.isHi && c2.isHi) { return 1; }
                else { return 0; }
        }
    }

    public bool CheckForCrossing(List<Line2D> lines)
    {
        List<endpointEntry> pts = new List<endpointEntry>(2 * lines.Count);

        // Make endpoint objects from the lines so that we can sort all of the
        // lines endpoints.
        foreach (Line2D lin in lines)
        {
            // make the endpoint objects for this line
            endpointEntry hi, lo;
            if (lin.P1.X < lin.P2.X)
            {
                hi = new endpointEntry() { XValue = lin.P2.X, isHi = true, line = lin, hi = lin.P2.X, lo = lin.P1.X };
                lo = new endpointEntry() { XValue = lin.P1.X, isHi = false, line = lin, hi = lin.P1.X, lo = lin.P2.X };
            }
            else
            {
                hi = new endpointEntry() { XValue = lin.P1.X, isHi = true, line = lin, hi = lin.P1.X, lo = lin.P2.X };
                lo = new endpointEntry() { XValue = lin.P2.X, isHi = false, line = lin, hi = lin.P2.X, lo = lin.P1.X };
            }
            // add them to the sort-list
            pts.Add(hi);
            pts.Add(lo);
        }

        // sort the list
        pts.Sort(new endpointSorter());

        // sort the endpoint forward and backward links
        endpointEntry prev = null;
        foreach (endpointEntry pt in pts)
        {
            if (prev != null)
            {
                pt.bLink = prev;
                prev.fLink = pt;
            }
            prev = pt;
        }

        // NOW, we are ready to look for intersecting lines
        foreach (endpointEntry pt in pts)
        {
            // for every Hi endpoint ...
            if (pt.isHi)
            {
                // check every other line whose X-range is either wholly 
                // contained within our own, or that overlaps the high 
                // part of ours.  The other two cases of overlap (overlaps 
                // our low end, or wholly contains us) is covered by hi 
                // points above that scan down to check us.

                // scan down for each lo-endpoint below us checking each's 
                // line for intersection until we pass our own lo-X value
                for (endpointEntry pLo = pt.fLink; (pLo != null) && (pLo.XValue >= pt.lo); pLo = pLo.fLink)
                {
                    // is this a lo-endpoint?
                    if (!pLo.isHi)
                    {
                        // check its line for intersection
                        if (pt.line.intersectsLine(pLo.line))
                            return true;
                    }
                }
            }
        }

        return false;
    }
}

我不确定该算法的真正执行复杂度是多少,但我认为对于大多数非病态多边形来说,它会接近O(n*SQRT(n)),应该足够快。
内部循环逻辑的解释: 内部循环只是按照外部循环相同的排序顺序扫描endPoints列表。但它将从外部循环在列表中的当前位置(某条线的hiX点)开始扫描,并且仅在endPoint值低于该行相应loX值时停止扫描。
这里的棘手之处在于,它不能使用枚举器(外部循环的foreach(..in pts)),因为无法枚举列表的子列表,也无法基于另一个枚举的当前位置开始枚举。因此,我使用Forward和Backward链接(fLink和bLink)属性创建了一个双向链接列表结构,保留列表的排序顺序,但可以逐步扫描而无需枚举列表:
for (endpointEntry pLo = pt.fLink; (pLo != null) && (pLo.XValue >= pt.lo); pLo = pLo.fLink)

将其分解,旧式的for循环说明符有三个部分:初始化、条件和增量-减量。初始化表达式endpointEntry pLo = pt.fLink;pLo初始化为列表中当前点的前向链接。也就是说,列表中下一个点,按降序排序。
然后执行内部循环的主体。然后应用增量-减量pLo = pLo.fLink,它只是使用它的前向链接(pLo.fLink)将内部循环的当前点(pLo)设置为下一个较低的点,从而推进循环。
最后,在测试循环条件(pLo != null) && (pLo.XValue >= pt.lo)之后循环,只要新点不是null(这意味着我们到了列表的末尾),并且新点的XValue仍然大于或等于外部循环当前点的低X值,就会循环。第二个条件确保内部循环只查看与外部循环端点的线重叠的行。
现在对我来说更清楚的是,我可能可以通过将endPoints列表视为数组来避免整个fLink-bLink笨拙的问题:
  1. 用endPoints填充列表
  2. 按XValue排序列表
  3. 通过使用索引迭代器(i)而不是foreach枚举器,以降序遍历列表
  4. (A)通过使用第二个迭代器(j)在i处开始并在其下方通过时结束。
我认为那会简单得多。如果您想要,我可以发布一个简化版。

运行所有 700,000 个多边形总共只需要 15 分钟!这非常出色。我只需要检查它是否漏掉了任何一个。 - Evan Parsons
@EvanParsons 测试进行得如何?这个算法对你有用吗? - RBarryYoung
我99%确定它正在做我想要的事情。唯一的问题是我还没有完全理解它(尽管我有一个相当好的想法它在做什么),我打算明天花些时间确保我理解它。我对我的lineSegment交点方法和其他几个区域进行了一些调整,将时间缩短到了8:47。这基本上与我最初实现的Hoey-Shamos算法一样快。非常感谢! - Evan Parsons
好的,我认为我大致理解正在发生的事情。但是内部循环中发生了什么?我以前从未见过这样的for循环。 - Evan Parsons
@EvanParsons 我已经在我的回答末尾添加了一个解释。 - RBarryYoung

1
简单的优化,可以将不相交的多边形时间减少一半:
int count = lines.Count();
for (int l1idx = 0;       l1idx < count-1; l1idx++) 
for (int l2idx = l1idx+1; l2idx < count;   l2idx++)
{
  Line2D g = lines[l1idx];
  Line2D h = lines[l2idx];
  if (g.intersectsLine(h))
  {
    return false;
  }
}

不要在List上使用Count()。它是一个Linq扩展方法,会创建枚举器并逐个移动每个项目以增加计数器。对于700000,它将执行counter++ 700000次。List<>有一个字段Count,每个人都应该使用它。这是一个简单的值,Add(T)方法使用Count将新项插入到List的末尾。对于ValueTypes最好使用数组,以避免装箱/拆箱。 - Gleb Sevruk
2
@GlebSevruk 你的关于count的看法是错误的,linq扩展会返回属性值,如果它是ICollection类型。请参阅https://dev59.com/7msz5IYBdhLWcg3wVGPJ#7969468。你也错了关于泛型装箱逻辑,这对于Java是正确的,但不适用于C#。很少情况下需要使用数组,请参阅https://dev59.com/GHRC5IYBdhLWcg3wCMc6#434765。 - weston
谢谢指出。我仍在使用3.5,意识到“这只是在.NET 4中添加的”。现在我明白了装箱的问题,它只会发生在旧的.NET 2非类型化列表中。由于Ints存储在对象数组中,因此应该进行前后转换。存储纯值的List<T>唯一剩下的问题是动态调整大小,当您添加新元素时。最初,List<T>创建具有8或16(int[16])缓冲区的数组。达到此限制后,将分配新的两倍大小的数组(int[32]),并将数据移动到内存中。我看到开发人员对new List<T>(SIZE)几乎没有意识。 - Gleb Sevruk

1

需要检查两件事情:

  1. 闭合多边形由点的循环序列组成
    • 如果在此序列中有相同的点出现超过一次,则为自相交多边形。
    • 请注意,第一个和最后一个点可以是相同的(根据您的多边形表示方式不同),在这种情况下,此点必须存在两次以上。
  2. 检查多边形所有非相邻线段是否相交
    • 非相邻线段不共享任何点
    • 如果存在相交,则多边形自相交

不,我正在使用C++,而且在我的大部分工作中,第三方库由于许多原因都不是一个选项。顺便说一句,在我的程序中交叉检测已经足够快了。我认为你的评论更适合于问题,而不是答案。顺便说一句,这是对双重问题的跟进答案,为了清晰起见,请在这里查看https://dev59.com/KHbZa4cB1Zd3GeqPDkGZ?lq=1 - Spektre
是的,赶紧。我已经移动了注释。 - SalientBrain

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接