【问题标题】:Graham scan issue at high amount of points格雷厄姆扫描问题在很多点
【发布时间】:2014-08-07 19:02:00
【问题描述】:

当我的列表有很多点时,我在 graham 扫描算法中遇到了问题,但每次都可以在点数很少的情况下正常工作。我做了一些截图:

工作:(300 分)

不工作(5000 分)

角度计算:

public static double angle(MyVector3D vec1, MyVector3D vec2)
{
    return Math.Atan2(vec2.Y - vec1.Y, vec2.X - vec1.X) * 180 / Math.PI;

}

按极角排序点取决于最大 Y 点:

//bubblesort
    private void sortList()
    {
        double temp = 0.0;
        MyVector3D tempVector = new MyVector3D();
        for (int i = 1; i < points.Count; i++)
        {
            for (int j = 1; j < points.Count - 1; j++)
            {
                if (angles[j] < angles[j + 1])
                {
                    temp = angles[j + 1];
                    tempVector = points[j + 1];
                    angles[j + 1] = angles[j];
                    points[j + 1] = points[j];
                    angles[j] = temp;
                    points[j] = tempVector;
                }
            }   
        }

逆时针方法:

private double ccw(MyVector3D vec1, MyVector3D vec2, MyVector3D vec3)
{
    // ccwTest = ((vec2.X - vec1.X) * (vec3.Y - vec1.Y)) - ((vec2.Y - vec1.Y) * (vec3.X - vec1.X));
    return ((vec2.X - vec1.X) * (vec3.Y - vec1.Y)) - ((vec2.Y - vec1.Y) * (vec3.X - vec1.X));
}

格雷厄姆扫描算法:

for (int i = 2; i < points.Count; i++)
    {
        while (ccw(points[M - 1], points[M], points[i]) > 0)
        {
            if (M > 1)
            {
                points.RemoveAt(M);
                M -= 1;
                i--;
            }
            else if (i == points.Count - 1)
            {
                break;
            }

            else
            {
                i += 1;
            }
        }
        //goodPoints.Add(points[M]);
        //listBoxInfos.Items.Add("g" + (int)points[M].X + "," + (int)points[M].Y + "," + 0);
        //listBoxInfos.Items.Add("ccw" + ccwTest);
        M += 1;

    }

我真的不知道为什么我的程序在 800+ 点上爆炸...很难调试,因为算法在 300,400,500... 点上运行得很好。

Ty 获取信息。

【问题讨论】:

  • 冒泡排序中的points 是什么? for (...) 循环中不应该以“0”而不是“1”开头吗?
  • 这是 cavas 上的点列表(每个点都带有 x,y,z(0) 坐标)。角度列表是最大 Y 点与其他点之间的计算角度列表。当我对角度进行排序时,我也会对点列表进行排序,然后在格雷厄姆扫描开始之前用最大 Y 点和点 [最大 Y 点的索引)替换点 [0]。
  • 查看 wikipedia 伪代码,我发现您使用的是 ccw>0 而不是 ccw
  • 当 ccw == 0 时,while 循环不执行,这意味着该点保留在列表中并且是凸包的一部分。
  • 让 points[M-1] 与 points[i] 处于相同的坐标。然后知道: ccw = ((vec2.X - vec1.X) * (vec3.Y - vec1.Y)) - ((vec2.Y - vec1.Y) * (vec3.X - vec1.X)) 有点[M-1]对应vec1,points[i]对应vec3。用 vec1 替换 vec3 我们得到: ccw = ((vec2.X - vec1.X) * (vec1.Y - vec1.Y)) - ((vec2.Y - vec1.Y) * (vec1.X - vec1.X )) 我们可以清楚地看到这将是 == 0。既然您确认如果 ccw == 0 点将保留在凸包中,那么这将使您的包的一部分成为两条完全重叠的线,除非我弄错了某处。

标签: c# wpf algorithm computational-geometry


【解决方案1】:

维基百科上的算法坏了。它不处理多个点彼此共线和最小点的情况。例如,以下测试用例将失败:

        Vector3[] points3 = new Vector3[] 
        {
            new Vector3( 1, 1, 0),
            new Vector3( 5, 5, 0),
            new Vector3( 3, 3, 0),
            new Vector3( 2, 2, 0),
            new Vector3( 1, 1, 0),
            new Vector3( 1, 10, 0),

        };

问题是,当沿点行进时,如果该点位于船体的最后两个点之间,则可能需要丢弃当前点,而不是延长船体或替换船体上的最后一个点. (只有当这些点也与最小点共线时才会发生这种情况,否则前面的角度排序会阻止这种双反。)显示的伪代码中没有逻辑。

我还认为 Wikipedia 算法可能处理浮点舍入错误很糟糕。特别是检查 ccw

这是清理维基百科算法的尝试。我不得不摆脱(含糊不清的)“哨点”,因为如果所有点都水平对齐,它将基本上随机选择:

    public static IList<Vector3> GrahamScanCompute(IList<Vector3> initialPoints)
    {
        if (initialPoints.Count < 2)
            return initialPoints.ToList();

        // Find point with minimum y; if more than one, minimize x also.
        int iMin = Enumerable.Range(0, initialPoints.Count).Aggregate((jMin, jCur) =>
        {
            if (initialPoints[jCur].Y < initialPoints[jMin].Y)
                return jCur;
            if (initialPoints[jCur].Y > initialPoints[jMin].Y)
                return jMin;
            if (initialPoints[jCur].X < initialPoints[jMin].X)
                return jCur;
            return jMin;
        });
        // Sort them by polar angle from iMin, 
        var sortQuery = Enumerable.Range(0, initialPoints.Count)
            .Where((i) => (i != iMin)) // Skip the min point
            .Select((i) => new KeyValuePair<double, Vector3>(Math.Atan2(initialPoints[i].Y - initialPoints[iMin].Y, initialPoints[i].X - initialPoints[iMin].X), initialPoints[i]))
            .OrderBy((pair) => pair.Key)
            .Select((pair) => pair.Value);
        List<Vector3> points = new List<Vector3>(initialPoints.Count);
        points.Add(initialPoints[iMin]);     // Add minimum point
        points.AddRange(sortQuery);          // Add the sorted points.

        int M = 0;
        for (int i = 1, N = points.Count; i < N; i++)
        {
            bool keepNewPoint = true;
            if (M == 0)
            {
                // Find at least one point not coincident with points[0]
                keepNewPoint = !NearlyEqual(points[0], points[i]);
            }
            else
            {
                while (true)
                {
                    var flag = WhichToRemoveFromBoundary(points[M - 1], points[M], points[i]);
                    if (flag == RemovalFlag.None)
                        break;
                    else if (flag == RemovalFlag.MidPoint)
                    {
                        if (M > 0)
                            M--;
                        if (M == 0)
                            break;
                    }
                    else if (flag == RemovalFlag.EndPoint)
                    {
                        keepNewPoint = false;
                        break;
                    }
                    else
                        throw new Exception("Unknown RemovalFlag");
                }
            }
            if (keepNewPoint)
            {
                M++;
                Swap(points, M, i);
            }
        }
        // points[M] is now the last point in the boundary.  Remove the remainder.
        points.RemoveRange(M + 1, points.Count - M - 1);
        return points;
    }

    static void Swap<T>(IList<T> list, int i, int j)
    {
        if (i != j)
        {
            T temp = list[i];
            list[i] = list[j];
            list[j] = temp;
        }
    }

    public static double RelativeTolerance { get { return 1e-10; } }

    public static bool NearlyEqual(Vector3 a, Vector3 b)
    {
        return NearlyEqual(a.X, b.X) && NearlyEqual(a.Y, b.Y);
    }

    public static bool NearlyEqual(double a, double b)
    {
        return NearlyEqual(a, b, RelativeTolerance);
    }

    public static bool NearlyEqual(double a, double b, double epsilon)
    {
        // See here: http://floating-point-gui.de/errors/comparison/
        if (a == b)
        { // shortcut, handles infinities
            return true;
        }

        double absA = Math.Abs(a);
        double absB = Math.Abs(b);
        double diff = Math.Abs(a - b);
        double sum = absA + absB;
        if (diff < 4*double.Epsilon || sum < 4*double.Epsilon)
            // a or b is zero or both are extremely close to it
            // relative error is less meaningful here
            return true;

        // use relative error
        return diff / (absA + absB) < epsilon;
    }

    static double CCW(Vector3 p1, Vector3 p2, Vector3 p3)
    {
        // Compute (p2 - p1) X (p3 - p1)
        double cross1 = (p2.X - p1.X) * (p3.Y - p1.Y);
        double cross2 = (p2.Y - p1.Y) * (p3.X - p1.X);
        if (NearlyEqual(cross1, cross2))
            return 0;
        return cross1 - cross2;
    }

    enum RemovalFlag
    {
        None,
        MidPoint,
        EndPoint
    };

    static RemovalFlag WhichToRemoveFromBoundary(Vector3 p1, Vector3 p2, Vector3 p3)
    {
        var cross = CCW(p1, p2, p3);
        if (cross < 0)
            // Remove p2
            return RemovalFlag.MidPoint;
        if (cross > 0)
            // Remove none.
            return RemovalFlag.None;
        // Check for being reversed using the dot product off the difference vectors.
        var dotp = (p3.X - p2.X) * (p2.X - p1.X) + (p3.Y - p2.Y) * (p2.Y - p1.Y);
        if (NearlyEqual(dotp, 0.0))
            // Remove p2
            return RemovalFlag.MidPoint;
        if (dotp < 0)
            // Remove p3
            return RemovalFlag.EndPoint;
        else
            // Remove p2
            return RemovalFlag.MidPoint;
    }

顺便说一下,你的算法在几个地方是 n 平方的:

  1. 冒泡排序的顺序为 O(N^2)
  2. 在找到船体时移除点而不是将它们交换到列表的前面可能是 O(N^2),因为所有后续点都必须向下移动。

如果它解决了你的问题,请告诉我,我已经测试了一点,但不完全。

【讨论】:

  • 经过一些测试,您的代码似乎在某些方面失败了。考虑输入点:0]: {X = 11.581625 Y = -110.983437} [1]: {X = 11.1816254 Y = -108.983437} [2]: {X = 11.88781 Y = -113.115852} [3]: {X = 11.587204 Y = -111.015938} [4]: {X = 12.1884336 Y = -115.215759} [5]: {X = 11.88781 Y = -113.115845} [6]: {X = 12.5794077 Y = -116.863365} [7]: {X = 12.1794081 Y = -115.163368} [8]: {X = 13.0785418 Y = -118.855026} [9]: {X = 12.5785418 Y = -116.855026} [10]: {X = 13.534234 Y = -119.732178} [11] {X = 13.034234 Y = -118.732178}
  • 使用上面的代码扫描后上面的一组点的输出我们得到: [0]: {X = 13.534234 Y = -119.732178} [1]: {X = 11.1816254 Y = -108.983437 } [2]: {X = 12.1794081 Y = -115.163368} [3]: {X = 12.1884336 Y = -115.215759} [4]: {X = 12.5794077 Y = -116.863365} [5]: {X = 13.034234 Y = -118.732178} [6]: {X = 13.0785418 Y = -118.855026}
  • 另一个似乎失败的测试用例,考虑输入:[0]: {X = 10.4182844 Y = -111.21611} [1]: {X = 10.0190592 Y = -109.21595} [2]: { X = 10.712142 Y = -113.283806} [3]: {X = 10.4127483 Y = -111.183716} [4]: {X = 11.0115175 Y = -115.383896} [5]: {X = 10.712141 Y = -113.2838} [6] : {X = 11.4204569 Y = -117.136063} [7]: {X = 11.0213022 Y = -115.435867} [8]: {X = 11.9213 Y = -119.144341} [9]: {X = 11.4223957 Y = -117.144066} [ 10]:{X = 12.4652023 Y = -120.266693} [11]:{X = 11.9662571 Y = -119.266167}
  • 以上点扫描后的输出如下: [0]: {X = 12.4652023 Y = -120.266693} [1]: {X = 10.0190592 Y = -109.21595} [2]: {X = 11.0115175 Y = -115.383896} [3]: {X = 11.0213022 Y = -115.435867} [4]: {X = 11.4204569 Y = -117.136063} [5]: {X = 11.4223957 Y = -117.144066} [6 ]: {X = 11.9213 Y = -119.144341} [7]: {X = 11.9662571 Y = -119.266167}
  • 下面这个截图是一个视觉表示,Top Row 坐标是在扫描之前。底行坐标是在扫描之后。 earnestdev.com/universal/tessellation/…
【解决方案2】:

据此:http://en.wikipedia.org/wiki/Graham_scan 和其他人至少有 2 个与您的格雷厄姆扫描算法实施有关的问题,我认为您对较低的数字“走运”了:

1) 你在你的外层 for 和你的 else 中都增加 i,也就是说,一般来说,你每隔一个点就跳过测试。

2) 我不相信您删除失败点的方法,是的,该点不在“这里”的船体上,但可能是船体上更远的一个点,您需要将这些点向下交换或使用基于堆栈的方法。

【讨论】:

  • Ty tolanj 寻求建议。我会尝试解决这些问题并在明天再次粘贴代码。
【解决方案3】:

我想这超出了 cmets 部分的范围,所以我将对此做出回答:

让 points[M-1] 与 points[i] 处于相同的坐标。

然后知道:ccw = ((vec2.X - vec1.X) * (vec3.Y - vec1.Y)) - ((vec2.Y - vec1.Y) * (vec3.X - vec1.X) ),points[M-1]对应vec1,points[i]对应vec3。

用 vec1 替换 vec3 我们得到: ccw = ((vec2.X - vec1.X) * (vec1.Y - vec1.Y)) - ((vec2.Y - vec1.Y) * (vec1.X - vec1.X)) 我们可以清楚地看到这将是 == 0。既然你确认如果 ccw == 0 点将保留在凸包中,那么这将使你的包的一部分成为两条完全重叠的线,除非我我在某个地方搞错了。


谢谢。我检查了算法,你是对的。问题是 当 ccw==0 时,但主要问题是如何消除点 ccw==0 不是凸包的一部分,并保留那些, 因为同一向量中的 3 个点可能是凸包的一部分,也可能不是。 知道如何解决这个问题吗?

(您可能想看看 dbc 的代码,但这是我的答案)

我认为您不想在凸包中保留与 ccw==0 的任何链接。 当 ccw==0 时,您的向量之间有 180 度或 0 度角。

0deg 的情况就像我在回答开头所说的那样(这些点实际上不需要重叠,这样更容易演示)。

至于 180 度的情况,您需要编写一些额外的代码。我将引用 dbc:

可能需要丢弃当前点,而不是两者之一 延长船体或更换船体上的最后一点,如果 点在船体的最后两点之间

如果您想让测试更容易,您还可以提取所有凸包点的位置并仅使用这些点进行回放。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-10-29
    • 1970-01-01
    • 1970-01-01
    • 2017-01-25
    • 1970-01-01
    • 2012-01-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多