【问题标题】:C# - Speeding up a List comparison that uses nested for-loopsC# - 加速使用嵌套 for 循环的列表比较
【发布时间】:2017-12-18 02:14:18
【问题描述】:

我目前正在开发一个测试程序,以确保主板上的孔不会彼此太靠近或不会重叠。

为了做到这一点,我将所有孔的 X、Y 坐标和半径保存在称为holeInfo 的对象和列表holeInfoList 中的对象中。

我目前正在使用嵌套的 for 循环来遍历所有孔,并使用基本数学公式来检查孔之间的距离。

这是我使用的函数:

public void checkHoleConditions()
{
    for (int i = 0; i < holeInfoList.Count - 1; i++)
    {
        errorType = new List<string>();

        for (int j = i + 1; j < holeInfoList.Count; j++)
        {
            holeMinSpaceFailed = false;

            if (failsHoleConditions(holeInfoList[i], holeInfoList[j])) 
            {
                if (holeMinSpaceFailed)
                {
                    errorType.Add("X: " + holeInfoList[j].holeXCoordinate + " Y: " + holeInfoList[j].holeYCoordinate + "R: " + holeInfoList[j].holeDiameter + " too close.");
                }
                else
                {
                    errorType.Add("X: " + holeInfoList[j].holeXCoordinate + " Y: " + holeInfoList[j].holeYCoordinate + "R: " + holeInfoList[j].holeDiameter + " overlap.");
                }

                invalidHole = new InvalidHole(holeInfoList[i], errorType);
                invalidHoleList.Add(invalidHole);
                Console.WriteLine("Hole Error Detected");
            }
            else
            {
                Console.WriteLine("Hole Check Successful");
            }
        }
    }
}

public bool failsHoleConditions(HoleInfo holeOne, HoleInfo holeTwo)
    {
        float holeOneXCoordinate = holeOne.holeXCoordinate;
        float holeOneYCoordinate = holeOne.holeYCoordinate;
        float holeOneRadius = holeOne.holeDiameter / 2;

        float holeTwoXCoordinate = holeTwo.holeXCoordinate;
        float holeTwoYCoordinate = holeTwo.holeYCoordinate;
        float holeTwoRadius = holeTwo.holeDiameter / 2;

        float holeXDifferenceSquared = (float)Math.Pow((holeOneXCoordinate - holeTwoXCoordinate), 2);
        float holeYDifferenceSquared = (float)Math.Pow((holeOneYCoordinate - holeTwoYCoordinate), 2);
        float distanceBetweenCenters = (float)Math.Sqrt(holeXDifferenceSquared + holeYDifferenceSquared);

        float distanceBetweenHoles = distanceBetweenCenters - (holeOneRadius + holeTwoRadius);

        if (distanceBetweenHoles <= 0)
        {
            return true;
        }
        else if (distanceBetweenHoles <= minimumSpace)
        {
            holeMinSpaceFailed = true;
            return true;
        }
        return false;
    }

目前,我的程序在大约 2.5 分钟内完成了 254 个列表对象的测试。考虑到这是一个测试程序,254洞2.5分钟就够长了。

我可以实施哪些算法来加快测试运行速度?

【问题讨论】:

  • 这似乎更适合代码审查?
  • 并行化您的代码(例如 Parallel.For),也许可以尝试使用 stringbuilder 而不是 string,不确定在这种情况下什么会更快。
  • @VladimirArustamian 我不确定 stringbuilder 会在哪里提供帮助?此外,您必须小心并行运行嵌套循环。
  • 您当前将向控制台写入数千行。在某些情况下,控制台可能成为瓶颈。你试过删除它吗?接下来,您是否对代码进行了分析以找出速度慢的地方? failsHoleConditions 是做什么的?
  • @JonSkeet failedHoleCondition 只是使用一些基本的数学公式计算中心之间的距离,然后根据 if-else if 语句运行它们。如果您愿意,我也可以发布代码。至于控制台,后来我开始在控制台上写,在我看到这个过程花费了很多时间后,看看程序是否真的还在工作。

标签: c# algorithm list object for-loop


【解决方案1】:

此解决方案的灵感来自物理引擎,以及负责碰撞检测的优化。

我并不声称完全知道它是如何工作的,为了获得更好的解决方案,您应该为此尝试研究 ODE 或 Bullet Dynamics。

基本上解决方案是将对象(孔)分成岛屿,并且仅将每个对象的位置与同一岛屿中的对象进行比较。如果您无法想出正确分离岛屿的方法,您可以这样做:

假设我们在一个正方形区域上有 125 个对象,大小为 5 x 5。您可以将其划分为 25 个主要正方形岛,然后是 8 个交叉岛(长岛,沿着主要岛的边缘。这些岛中最小的一边应该是您要计算的最小距离)。岛屿可以重叠。您必须解析整个列表一次才能进行拆分。这意味着到目前为止,我们总共循环了 125 个项目 - O(n)

接下来,对于每个岛(总共 33 个,O(n^(2/3)),通过使用相同的嵌套循环找到比它们必须更近的对象。每个岛的总复杂度为 O((n / n^(2/3))^2) = O(n^(2/3))。乘以岛屿的数量,我们得到这个算法的总复杂度 = O(n^(4/3)),它小于最初提出的 O(n^2)

希望这是有道理的。如果你愿意,我可以写一个 Python 演示。只是代码量比较大。

编辑:
或者您可以只使用 2D 物理引擎并将对象绘制为直径等于孔之间最小距离的圆,然后让它检测碰撞。或者从那里获取相关代码(如果许可证允许),因为整个物理引擎对于这项任务来说有点过头了。
https://github.com/VelcroPhysics/VelcroPhysics

编辑 2:

254 个列表对象

我以为你正在解析 254 个不同的板。我强调的这个解决方案只有在计算量很大时才有意义。

【讨论】:

  • 我认为 254 孔不需要这个。我写了一个在 1 秒内完成 100 万次的测试用例。
  • 但是对于更大的数字,您确实需要它。请参阅我的“编辑 2”。
【解决方案2】:

如果 failedHoleConditions 为真,holeMinSpaceFailed 将永远不会为真
不要绕过这样的逻辑

x * xMath.Pow 快​​

不需要 Sqrt - 只需平方 minimumSpace

类似的东西。 运行 100 万 300 毫秒。您的代码有问题需要几分钟。

static byte HoleTooClose(HoleInfo x, HoleInfo y, float minDistance)
{
    float holeSize = (x.Diameter + y.Diameter) / 2;
    float deltaX = y.X - x.X;
    float deltaY = y.Y - x.Y;

    float distanceSquared = deltaX * deltaX + deltaY * deltaY - holeSize * holeSize;

    if (distanceSquared <= 0)
    {
        return 0;
    }

    float minDistanceSquared = minDistance * minDistance;
    if (distanceSquared <= minDistanceSquared)
    {
        return 1;
    }

    return 2;
}

internal struct HoleInfo
{
    public float Diameter { get; internal set; }
    public float X { get; internal set; }
    public float Y { get; internal set; }
    public HoleInfo (float x, float y, float diameter)
    {
        X = x;
        Y = y;
        Diameter = diameter;
    }
}


static bool DistanceTooClose(System.Windows.Point x, System.Windows.Point y, Double minDistance)
{
    double deltaX = Math.Abs(y.X - x.X);
    double deltaY = Math.Abs(y.Y - x.Y);
    double distanceSquared = deltaX * deltaX + deltaY * deltaY;
    //double distance = Math.Sqrt(distanceSquared);
    Double minDistanceSquared = minDistance * minDistance;
    return (distanceSquared <= minDistanceSquared);
}

【讨论】:

  • 修复了逻辑问题。另外,如果我只是平方 minimumSpace,在计算 distanceBetweenHoles 时会不会引起一些问题,因为我要从中减去半径来检查它是否小于零?
  • 在平方之前从 x 和 y 距离中减去半径。为什么一开始就将半径除以二?
  • @BoeseB 被划分的变量实际上是直径,我最近发现了输入的类型,但从来没有改变过名称。我现在就这样做。
  • @BerkeJimmyBakkaloğlu 请参阅此gist 以了解此答案和 cmets 中建议的更改
【解决方案3】:

如果您需要针对所有人进行测试,一种优化可以是将它们放在 nxn 矩阵中,而不是列表中。然后,并行执行验证。矩阵越大,拥有的核心越多越好。并行也可以在列表上执行,但我不能 100% 确定 .Net 是否会确定等待每个线程结束,因为算法是顺序的。

如果您的方法使用欧几里得距离,您可以尝试先根据此距离对列表进行排序。 sqr(x)+sqr(y) 相对于 0 可以是先排序的分数。我认为.Net 可以轻松处理这种排序。稍后,只需运行您的算法,直到第一个允许的电路。然后,您知道必须接受其余部分,因此您将只对列表的第一个元素执行

【讨论】:

    猜你喜欢
    • 2016-06-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多