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(出于测试目的),扫描 10,000 条记录仍然需要 8 秒(我有 700,000 条记录)。这太长了,所以我需要优化这段代码。

在将列表与所有其他行进行比较后,我尝试从列表中删除行,但是存在准确性问题(不知道为什么)并且速度增加几乎不明显。

这是我的 intersectsLine 方法。我在这里找到了一种替代方法,但看起来所有方法调用和诸如此类的东西都会变慢。在我看来,计算斜率似乎不需要太多的计算(如果我错了,请纠正我?)

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 运行了所有 700,000 多个多边形。在可接受的范围内!在运行此代码之前,肯定有一种方法可以查看多边形是否值得计算?如果有帮助,我可以访问 XMin、Xmax、YMin 和 YMax 属性吗?

进行另一项测试,将多边形限制在每个 1000 线以下。花了一个多小时。

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

我有几个想法:

-当我生成我的行哈希集时,有另一个哈希集/列表有交叉的候选者。如果有机会相交,我只会在此列表中添加行。但是我如何消除/增加可能性?如果只有三条线可能与其他线相交,我会将 4000 条线与 3 条而不是 4000 条进行比较。仅此一项就可以产生巨大的差异。

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

编辑:


有关多边形的信息:总计 700,000

有超过 4000 个具有 1000 个或更多点的多边形

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


我认为只要有一点创造力,就有可能把它缩短到十五分钟左右。

4

3 回答 3

6

您当前的蛮力算法是 O(n^2)。就你的两个 70,000 线多边形而言,这几乎是 100 亿次操作的一部分,更不用说其他 700,000 个多边形了。显然,仅仅进行代码优化是不够的,因此您需要某种算法优化,以降低 O(n^2) 的时间,而不会过于复杂。

O(n^2) 来自蛮力算法中的嵌套循环,每个循环都以 为界n,使其成为 O(n*n)。改善这一点的最简单方法是找到某种方法来减少内部循环,使其不受n. 因此,我们需要找到某种方式来对内部行列表进行排序或重新排序以检查外部行,以便只需要扫描完整列表的一部分。

我采用的方法利用了这样一个事实,即如果两条线段相交,那么它们的 X 值范围必须相互重叠。请注意,这并不意味着它们确实相交,但如果它们的 X 范围不重叠,则它们不能相交,因此无需相互检查它们。(Y 值范围也是如此,但您一次只能利用一个维度)。

这种方法的优点是这些 X 范围可用于对线的端点进行排序,而这些端点又可以用作要检查相交的线的起点和终点。

所以具体来说,我们要做的是定义一个自定义类 ( endpointEntry),它表示线的两个点的 High 或 Low X 值。这些端点都被放入同一个 List 结构中,然后根据它们的 X 值进行排序。

然后我们实现一个外循环,我们扫描整个列表,就像在蛮力算法中一样。然而,我们的内部循环有很大不同。与其重新扫描整个列表以检查是否相交,我们宁愿从外循环线的高 X 值端点开始向下扫描已排序的端点列表,并在我们通过同一行的低 X 值端点下方时结束它。这样,我们只检查 X 值范围与外循环线重叠的线。

好的,这是展示我的方法的 ac# 类:

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)),这应该足够快。


内循环逻辑说明:

内循环只是按照与外循环相同的排序顺序扫描端点列表。但它会从外循环当前在列表中的位置开始扫描(这是某行的 hiX 点),并且只会扫描直到 endPoint 值低于同一行的相应 loX 值。

这里棘手的是,这不能用 Enumerator (foreach(..in pts)外部循环的)来完成,因为无法枚举列表的子列表,也无法根据另一个枚举当前位置开始枚举。因此,我所做的是使用 Forward 和 Backward Links(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) 设置为使用它的前向链接 ( pLo.fLink) 的下一个较低点,从而推进循环。

最后,它在测试循环条件后(pLo != null) && (pLo.XValue >= pt.lo)循环,只要新点不为空(这意味着我们在列表的末尾)并且只要新点XValue仍然大于或等于外循环当前点的低 X 值。第二个条件确保内循环只查看与外循环端点线重叠的线。


现在对我来说更清楚的是,我可能可以通过将 endPoints List 视为一个数组来解决整个 fLink-bLink 的笨拙问题:

  1. 用端点填充列表
  2. 按 XValue 对列表进行排序
  3. i使用索引迭代器 ( ),而不是foreach枚举器,按降序遍历列表
  4. (A) 使用第二个迭代器 ( j) 遍历列表的内循环,从下面开始i并结束pt.Lo

我认为会简单得多。如果您愿意,我可以发布这样的简化版本。

于 2013-10-04T03:09:27.150 回答
1

有两件事要检查:

  1. 封闭多边形由点的循环序列组成
    • 如果此序列中的同一点不止一次,则它是自相交多边形。
    • 请注意,第一个点和最后一个点可以相同(在您的多边形表示上有所不同),在这种情况下,这个点必须有更多的棕褐色两次。
  2. 检查多边形的所有不相邻的线是否相交
    • 不相邻的线不共享任何点
    • 如果有相交,则多边形是自相交的
于 2013-09-16T14:29:03.693 回答
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;
  }
}
于 2013-10-04T11:32:46.743 回答