我正在寻找一种算法,用于将无序的项目列表排序到树结构中,使用最小数量的" is child"比较操作尽可能。
关于我的具体案例的一些背景,但我想我只是在寻找一种我无法找到的通用排序算法(这是一个难以改进的硬搜索术语)。
我有一个无序的轮廓列表,它只是描述闭合多边形的坐标列表。我想创建一个表示这些轮廓之间关系的树,这样最外层是根,每个轮廓在下一级作为子级,依此类推。所以每个节点都有一个零到多个孩子的树结构。
该算法的一个关键要求是,确定轮廓是否是另一个轮廓的子项的测试保持最小,因为此操作非常昂贵。轮廓可以(并且经常)共享许多顶点,但不应相交。这些共享顶点通常出现在达到地图限制的地方 - 在地图的直边上绘制一组同心半圆。如果我需要在得到明确的答案之前经历许多点上线,那么聚合点测试很慢。
这是我到目前为止提出的算法。毫无疑问,这很天真,但它确实有效。可能有一些启发式方法可能会有所帮助 - 例如,轮廓只可能是另一个轮廓的孩子,其深度在一定范围内 - 但我想首先确定基本算法。第一个红旗是它呈指数级。
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!
这是第一次迭代 - 产生了良好的(10x)改进。请参阅下面的迭代2。 该代码与我的原始算法相比是>一旦轮廓设置变得非常大,就会快10倍。请参阅下面的图片,该图片现在在几秒钟内排序(v&#39; 30多秒前),并按顺序呈现。我认为通过一些额外的启发式方法还有进一步改进的空间 - 例如,既然原始列表是根据区域排序的,那么每个新候选者必须是树中某处的叶节点。困难在于确定要遍历哪些分支以测试现有的叶子 - 如果有许多分支/叶子,那么通过检查顶部的分支来缩短搜索空间可能仍然更快......更需要考虑的事情!< / p>
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的评论和重新检查代码之后 - 不幸的是,第二个解决方案无效。我想知道在分支之前查看树中的较低元素是否仍然有一些优点,因为poly-in-poly测试应该快速失败,并且你知道如果你找到一个接受候选者的叶子,没有更多的需要检查的多元化..
重访迭代
虽然不是轮廓可以仅适合叶子的情况,但它们几乎总是这样做 - 并且它们通常也适合于有序列表中的最近的前任轮廓。这个最终更新的代码是最快的,并完全抛弃树遍历。它只是向后走过最近的较大的多边形并尝试每个 - 来自其他分支的多边形可能会在边界检查中失败多边形多边形测试,并且发现围绕候选多边形的第一个多边形必须是直接的父多边形,到期到列表的先前排序。该代码再次将分类降低到毫秒范围,并且比树遍历快约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 迭代1(遍历分支找到叶子): 9s 迭代2(以递增的大小顺序向后走过轮廓):25ms(还有其他pt-in-poly改进)。
答案 0 :(得分: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]
的子项。 {1}}等等。您的评论表明它不是总是的情况,但它经常发生。
考虑到这一点,也许你应该对你的实现做一个简单的修改,这样你就可以保存最后一个多边形并先检查它,而不是从树开始。所以你的循环看起来像这样:
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;
如果数据与你说的一样,那么这种优化应该会有所帮助,因为它消除了一堆在树中的搜索。
答案 1 :(得分: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
}