无序列表转换为树形结构的算法

3
我正在寻找一种算法,将无序的项目列表排序为树状结构,并使用尽可能少的“子项”比较操作。
在我特定情况下的背景下,我想创建一个树形结构,表示这些轮廓之间的关系,使得最外层是根,下一个级别的每个轮廓都是子代,依此类推。因此,一个节点可以有零到多个子节点。
算法的一个关键要求是尽量减少测试以确定轮廓是否为另一个轮廓的子项,因为此操作非常昂贵。轮廓可能(并经常)共享许多顶点,但不应相交。这些共享顶点通常出现在达到地图限制的位置 - 想象一组半圆锥形放置在地图的直边上。如果我需要运行大量点在线路上之前才能得出明确的答案,则点在多边形上的测试速度很慢。
以下是我迄今为止想出的算法。它可能相当幼稚,但它有效。可能有一些启发式方法可以帮助 - 例如,轮廓只有在一定范围内的深度内才可能是另一个轮廓的子项 - 但我首先要掌握基本算法。第一个红旗是指它是指数级的。
for each candidate_contour in all_contours
    for each contour in all_contours
        // note already contains means "is direct or indirect child of"
        if contour == candidate_contour or contour already contains(candidate_contour)
            continue
        else
            list contours_to_check
            contours_to_check.add(candidate_contour)

            contour parent_contour = candidate_contour.parent
            while (parent_contour != null)
                contours_to_check.add(parent_contour)
                parent_contour = parent_contour.parent

            for each possible_move_candidate in contours_to_check (REVERSE ITERATION)
                if possible_move_candidate is within contour
                    // moving a contour moves the contour and all of its children
                    move possible_move_candidate to direct child of contour
                    break

这个方法能够运行 - 或者至少看起来是可以的 - 但是当轮廓数量非常大时(比如几百个,甚至可能几千个)会变得非常慢。

有没有更好的基本方法来解决这个问题?或者说,有没有已知的算法可以处理这种情况?正如之前提到的 - 在我的情况下,关键是尽量减少“是否在轮廓内”的比较次数。

编辑以添加基于Jim下面回答的解决方案 - 感谢Jim!

这是第一次迭代 - 它产生了很好的(10倍)改进。请参见下面的迭代2。与我的原始算法相比,这段代码快了>10倍,一旦轮廓集变得非常大。查看下面的图像,现在可以在几秒钟内排序(而不是之前的30多秒),并按顺序渲染。我认为还有改进的空间,例如,现在根据面积对原始列表进行了排序,那么每个新的候选对象都必须是树中的叶节点。难点在于确定要遍历哪些分支来测试现有的叶子节点 - 如果有许多分支/叶子,则检查顶部的分支可能仍然更快..这是需要考虑的一些内容!

    public static iso_area sort_iso_areas(List<iso_area> areas, iso_area root)
    {
        if (areas.Count == 0)
            return null;

        areas.Sort(new iso_comparer_descending());

        foreach (iso_area area in areas)
        {
            if (root.children.Count == 0)
                root.children.Add(area);
            else
            {
                bool found_child = false;
                foreach (iso_area child in root.children)
                {
                    // check if this iso_area is within the child
                    // if it is, follow the children down to find the insertion position
                    if (child.try_add_child(area))
                    {
                        found_child = true;
                        break;
                    }   
                }
                if (!found_child)
                    root.children.Add(area);
            }
        }
        return root;
    }

    // try and add the provided child to this area
    // if it fits, try adding to a subsequent child
    // keep trying until failure - then add to the previous child in which it fitted
    bool try_add_child(iso_area area)
    {
        if (within(area))
        {
            // do a recursive search through all children
            foreach (iso_area child in children)
            {
                if (child.try_add_child(area))
                    return true;
            }
            area.move_to(this);
            return true;
        }
        else
            return false;
    }

有序轮廓

迭代二 - 仅与现有叶子进行比较

继续我之前的想法,即新轮廓只能适合于现有叶子中,我认为实际上这将更快,因为在除了目标叶子以外的所有叶子中,多边形与多边形的测试会在第一次边界检查时失败。第一个解决方案涉及遍历分支以找到目标,其中每个定义上都通过边界检查的多边形都将通过完整的多边形内部测试,直至没有发现更多叶子。

根据Jim的评论和重新审查代码 - 不幸的是,第二个解决方案并没有起作用。我想知道在查看分支之前是否仍然有必要查看树中的低元素,因为多边形内部测试应该很快失败,并且您知道如果找到接受候选者的叶子,则不需要再检查其他多边形。

迭代二重新审视

虽然情况并非轮廓只能适合于叶子,但几乎总是如此 - 而且它们通常适合于轮廓的有序列表中的最近前身。这个最终更新的代码是最快的,完全放弃了树遍历。它只是向后遍历最近的较大多边形并尝试每个 - 来自其他分支的多边形可能会在边界检查时失败多边形内部测试,而找到围绕候选多边形的第一个多边形必须是直接父级,由于列表的先前排序。该代码将排序带入毫秒范围内,并且比树遍历快约5倍(对多边形内部测试进行了显着的速度改进,其占其余速度提升的原因)。现在从区域的排序列表中取出根。请注意,我现在在列表中提供一个包含所有轮廓(所有轮廓的边界框)的根。

感谢您的帮助 - 特别是Jim - 帮助我思考这个问题。关键实际上是将轮廓排序为列表,其中保证不会有轮廓是列表中后面轮廓的子级。

public static iso_area sort_iso_areas(List<iso_area> areas)
    {
        if (areas.Count == 0)
            return null;

        areas.Sort(new iso_comparer_descending());

        for (int i = 0; i < areas.Count; ++i)
        {
            for (int j = i - 1; j >= 0; --j)
            {
                if (areas[j].try_add_child(areas[i]))
                    break;    
            }              
        }
        return areas[0];
    }

我的原始尝试:133 s 第一次迭代(遍历分支查找叶节点):9s 第二次迭代(按升序大小向后遍历轮廓):25毫秒(同时还有其他点在多边形内的改进)。


几乎所有严肃的排序实现都允许您提供自己的比较例程。为什么不使用这些,以“是子代”作为比较? - user395760
我认为你的算法是O(n^2)。你可能想尝试堆排序,它是O(nlogn)的。 - sdasdadas
1
@马特 一旦列表排序完成,您可以使用线性数量的“是直接子项”的查询重新构建树。 - user395760
@delnan - 我会在明天再考虑这个问题,但是请想象一个大圆圈(1),里面有两个小圆圈(2,3),它们内部分别有4和5。要将它们排序成一个列表,你会得到1、2、3,然后4在2中,但不在3中。但是3不在4中 - 所以没有自然的线性顺序可以满足排序算法。我有什么遗漏吗? - Matt
我不理解仅叶子节点的逻辑。您发布的代码不应该有效。在第一种情况下,您没有将第一个节点添加到叶子列表中,因此该节点的任何子节点都将添加到根级别。更重要的是,您发布的代码会在包含单个子节点时立即从叶子列表中删除节点。因此,没有节点可以有多个子节点?根据您发布的图片,这似乎不正确。您确定您的迭代2生成与迭代1相同的树吗? - Jim Mischel
显示剩余2条评论
2个回答

2

我一段时间前也做过类似的事情,首先按区域排序。

如果多边形B包含在多边形A中,则多边形A的边界框必须大于多边形B的边界框。更重要的是,如果您将边界框指定为((x1, y1), (x2, y2)),则:

A.x1 < B.x1
A.y1 < B.y1
A.x2 > B.x2
A.y2 > B.y2

如果多边形可以共享边缘或顶点,则这些关系可能是<=>=。因此,您应该首先计算边界框并按照边界框面积倒序排序多边形(以便最大的多边形排在第一位)。

创建一个基本上是多边形及其子项列表的结构:

PolygonNode
{
    Polygon poly
    PolygonNode[] Children
}

所以你首先按照边界框面积进行排序,降序排列,并初始化一个空的PolygonNode结构列表:

Polygon[] sortedPolygons
PolygonNode[] theTree

现在,从具有最大面积的多边形开始,即sortedPolygons的第一个成员,检查它是否是theTree的任何顶级成员的子项。如果不是,则将该多边形添加到theTree中。这里的边界框很有帮助,因为如果边界框测试失败,您就不必进行完整的多边形内部测试。
如果它是节点的子项,则检查它是否是该节点的任何子项的子项,并沿着子项链向下跟随,直到找到插入点。
对于sortedPolygons中的每个多边形都重复以上步骤。
最坏情况下,该算法是O(n ^ 2),如果没有父/子关系,就会发生这种情况。但是假设有许多嵌套的父/子关系,则搜索空间会很快缩小。
通过按位置对theTree列表和子节点列表进行排序,您可能可以加快速度。然后,您可以使用二进制搜索更快地定位多边形的潜在父级。这样做会稍微复杂一些,但如果您有很多顶级多边形,这可能是值得的。但我不会在第一次尝试中添加该优化。使用顺序搜索概述的版本很可能已经足够快了。
编辑
理解数据的性质是有帮助的。当我编写原始响应时,我没有意识到您的典型情况是,给定多边形的排序列表,正常情况是p [i]p [i-1] 的子项,p [i-2]的子项等等。您的评论表明这不总是情况,但通常是如此。
鉴于这一点,也许您应该对实现进行简单修改,以便首先保存最后一个多边形并检查它,而不是从树开始。因此,您的循环看起来像这样:
    iso_area last_area = null;    // <============
    foreach (iso_area area in areas)
    {
        if (root.children.Count == 0)
            root.children.Add(area);
        else if (!last_area.try_add_child(area))  // <=======
        {
            bool found_child = false;
            foreach (iso_area child in root.children)
            {
                // check if this iso_area is within the child
                // if it is, follow the children down to find the insertion position
                if (child.try_add_child(area))
                {
                    found_child = true;
                    break;
                }   
            }
            if (!found_child)
                root.children.Add(area);
        }
        last_area = area;      // <============
    }
    return root;

如果数据如你所说,那么这种优化应该会有很大帮助,因为它消除了一堆树搜索。

谢谢Jim - 乍一看这看起来不错,因为它似乎将问题简化为线性解决方案。我已经在poly-in-poly中使用了边界框作为第一个测试,但没有考虑按面积排序。我刚刚尝试了一个粗略的实现,但问题是有些多边形确实具有相同的边界框,如果初始排序将它们放在错误的顺序中,则无法纠正此错误(树中已经存在的轮廓永远不会成为后续多边形的子级)。我将看看如何解决这个(常见的)边缘情况,并回复您。 - Matt
如果所有形状都在彼此内部,算法是否也会变成O(n²)?换句话说,如果每个节点最终只有一个子节点呢? - StriplingWarrior
@Matt:如果A包含B并且它们都有相同的边界框,但排序将它们放在错误的顺序中,那将是一个问题。您可以让排序比较检查是否具有相等的边界框,并在这种情况下进行多边形内部测试,以使它们被正确排序。 - Jim Mischel
谢谢Jim - 我已经根据您的答案更新了我的问题,基于此提供了解决方案,相比我的原始算法,它展示了显著(现实世界)的性能提升。 - Matt
@Jim - 这是个好想法,我自己也有类似的想法(还没有尝试过)。但在这个例子中,很可能不是最后一个插入的节点(其他轮廓中有许多相似大小的轮廓)。"非常经常但并非总是"的情况是候选区域将适合于叶节点。这个事实,再加上非常快速的多边形测试(边界检查)与不相交的叶子节点,意味着首先检查所有叶子节点,然后如果失败就回到原始算法,可能会有一个好的案例。现在我只需要正确地实现它 :) - Matt
显示剩余4条评论

1

在处理树形结构时,采用递归方法效果很好。如果你的形状分布相对均匀,下面的算法应该是O(N log(N))的。但如果你的形状全部集中在一个像隧道一样的长通道中,最坏情况下它会变成O(N²)。

boolean tryAddToNode(Node possibleParent, Node toAdd)
{
    if not toAdd.isChildOf(possibleParent)
        return false
    for each child in possibleParent.children
        if(tryAddToNode(child, toAdd))
             return true
    // not a child of any of my children, but
    // it is a child of me.
    possibleParent.children.add(toAdd)
    return true
}

谢谢Stripling - 我会在明天实现并测试这个方法。我唯一能看到的潜在问题是,“一个长隧道状的分布”是(理想情况下的)中心案例 - 所以它可能不会比我的当前方法更快。我会告诉你结果的。 - Matt
@Matt:所以你的理想情况是将所有的形状像一堆巴布什卡娃娃一样放在一起吗?我今晚会再考虑一下,看看能否提出任何优化建议。你可以提供更多信息吗?你的坐标有界限吗?它们的精度程度如何? - StriplingWarrior
@Matt:反思后,我意识到这个解决方案只有在从大到小添加对象到树中时才能起作用,以确保在尝试添加其子节点之前始终存在父节点。此外,我还没有想到任何其他减少isChildOf调用次数的方法,但我想到了很多可能降低这些检查成本的方法——你是否花费了很多时间思考这个方向? - StriplingWarrior
“理想情况”可能有点过于强烈,但如果你想到湖泊的深度,那么你就会明白了。一个非常简单的地形可能只有少量的长巢穴。我选择了Jim的解决方案,但我很感谢你的时间! - Matt

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