【问题标题】:Algorithm for sorting unordered list to tree structure将无序列表排序为树结构的算法
【发布时间】:2013-11-19 13:54:42
【问题描述】:

我正在寻找一种算法,将项目的无序列表排序为树结构,尽可能使用最少数量的“是子”比较操作。

我的具体案例的一些背景知识,但我想我只是在寻找一种我无法找到的通用排序算法(这是一个难以改进的搜索词)。

我有一个无序列表的轮廓,它们只是描述闭合多边形的坐标列表。我想创建一棵树来表示这些轮廓之间的关系,使得最外层是根,下一级的每个轮廓都是子级,依此类推。所以一个树结构,每个节点有零对多的子节点。

该算法的一个关键要求是确定一个轮廓是否是另一个轮廓的子轮廓的测试保持在最低限度,因为此操作非常昂贵。轮廓可以(并且经常)共享许多顶点,但不应相交。这些共享顶点通常出现在达到地图限制的地方 - 描绘一组同心半圆对着地图的直边。如果我需要在得到明确的答案之前运行大量的在线点,那么多点测试会很慢。

这是我到目前为止提出的算法。毫无疑问,这很天真,但它确实有效。可能有一些启发式方法可能会有所帮助 - 例如,一个轮廓可能只是另一个深度在一定范围内的轮廓的子级 - 但我想首先确定基本算法。第一个危险信号是它是指数型的。

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 倍以上。请参阅下面的图像,该图像现在在几秒钟内排序(v 之前的 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;
    }

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

根据我之前的想法,新的轮廓只能适合现有的叶子,这让我感到震惊,实际上这会更快 因为 poly in poly 测试在第一次边界检查时会失败,其他所有叶子 第一个解决方案涉及遍历分支以找到目标,根据定义,沿途的每个多边形都将通过边界检查,并涉及完整的多边形测试,直到没有发现了更多的叶子。

在 Jim 发表评论并重新检查代码之后 - 不幸的是,第二种解决方案不起作用。我想知道在分支之前查看树中较低的元素是否仍有一些优点,因为多聚测试应该很快失败,而且你知道如果你找到一个接受候选人的叶子,那里不再需要检查的多边形了..

迭代二重温

虽然轮廓并非只能适合叶子,但它们几乎总是这样 - 而且它们通常会适合排序列表中最近的前身轮廓。这个最终更新的代码是最快的,并且完全放弃了树遍历。它只是向后遍历最近较大的多边形并尝试每个多边形 - 来自其他分支的多边形可能会在边界检查中无法通过多边形中的多边形测试,并且发现围绕候选多边形的第一个多边形必须是直接父多边形,因为到列表的先前排序。此代码再次将排序降低到毫秒范围内,并且比树遍历快约 5 倍(对 poly-in-poly 测试也进行了显着的速度改进,这占了其余的加速)。根现在取自已排序的区域列表。请注意,我现在在列表中提供了一个我知道包含所有轮廓(所有轮廓的边界框)的根。

感谢您的帮助 - 特别是 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 秒 迭代 1(遍历分支找到叶子):9s 迭代 2(按尺寸升序向后遍历轮廓):25 毫秒(还有其他 pt-in-poly 改进)。

【问题讨论】:

  • 几乎每个严肃的排序实现都允许您提供自己的比较例程。你为什么不直接用这些,用“is child of”作为比较呢?
  • 我相信你的算法是 O(n^2)。您可能想尝试堆排序,它是 O(nlogn)。
  • @delnan - 我的结果需要在树结构中 - 任何特定轮廓可能有许多直接子级。您知道可以处理此问题的排序例程吗?
  • @Matt 列表排序后,您可以使用线性数量的“is direct child of”查询重新构建树。
  • @delnan - 我会在早上考虑更多,但是 - 想象一个大圆圈 (1),里面有 2 个小圆圈 (2,3),里面有 4 和 5分别。要将这些排序到一个列表中,您将得到 1、2、3,然后 4 在 2 内,但不在 3 内。但 3 不在 4 内 - 因此没有自然的线性排序可以满足排序算法。我错过了什么吗?

标签: algorithm sorting


【解决方案1】:

我不久前做了类似的事情,首先按区域排序。

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

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

(如果多边形可以共享边或顶点,那么这些关系可能是 &lt;=&gt;=。)

所以你应该做的第一件事是计算边界框并按边界框面积对多边形进行降序排序(所以最大的在前)。

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

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] 的孩子,等等. 您的 cmets 表明它并非总是,但经常如此。

鉴于此,也许您应该对您的实现进行简单的修改,以便保存最后一个多边形并首先检查它,而不是从树开始。所以你的循环看起来像这样:

    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 - 乍一看这看起来不错,因为它似乎将问题简化为线性解决方案。我确实使用边界框作为多边形中的第一个测试,但没有考虑按区域排序。我刚刚尝试了一个粗略的实现,但问题是某些多边形确实具有相同的边界框,如果初始排序将它们置于错误的顺序,则没有机会纠正此错误(树中已经存在的轮廓永远不会得到有机会成为后续 polys 的孩子)。我会看看解决这个(常见的)边缘情况有多难,然后回复你。
  • 如果形状都在彼此的内部,算法不会也变成 O(n²) 吗?换句话说,如果每个节点最终只有 1 个孩子?
  • @StriplingWarrior:是的。有退化的情况。然而,在我拥有的数据中,退化的情况从未发生过,这种方法很有帮助。
  • @Matt:如果 A 包含 B 并且它们都具有相同的边界框,那么这将是一个问题,但排序会将它们置于错误的顺序中。您可以让您的排序比较检查相等的边界框,并在这种情况下进行多边形测试,以便它们被正确排序。
  • 谢谢吉姆 - 我已经根据您的回答更新了我的问题,该解决方案显示了比我的原始算法显着的(现实世界)性能改进。
【解决方案2】:

递归方法在处理树时效果很好。在您的形状分布相当均匀的情况下,以下算法应该是 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:经过反思,我意识到这个解决方案只有在将对象从最大到最小添加到树中时才有效,以确保在尝试添加它们之前始终存在父节点子节点。此外,我还没有想到任何其他方法来减少对 isChildOf 的调用次数,但我已经想到了很多方法可以降低这些检查的成本——你是否花了很多时间思考朝那个方向?
  • “理想情况”可能有点强,但是如果您想到湖中的深度,那么您就会明白这一点。一个非常简单的地形可能有少量的长巢。我已经接受了 Jim 的解决方案,但我感谢您的宝贵时间!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-09-28
  • 2017-05-26
  • 2011-05-24
  • 2013-01-31
  • 1970-01-01
  • 1970-01-01
  • 2013-07-15
相关资源
最近更新 更多