我正在寻找一种算法来将项目的无序列表排序为树结构,尽可能使用最少数量的“是子”比较操作。
关于我的具体案例的一些背景知识,但我想我只是在寻找一种我无法找到的通用排序算法(这是一个很难改进的搜索词)。
我有一个无序列表的轮廓,它们只是描述闭合多边形的坐标列表。我想创建一棵树来表示这些轮廓之间的关系,这样最外面的就是根,下一层的每个轮廓都是子级,依此类推。因此,每个节点具有零对多子节点的树结构。
该算法的一个关键要求是确定一个轮廓是否是另一个轮廓的子项的测试保持在最低限度,因为此操作非常昂贵。轮廓可以(并且经常)共享许多顶点,但不应相交。这些共享顶点通常出现在达到地图限制的地方 - 描绘一组同心半圆对着地图的直边。如果我需要在得到明确的答案之前运行大量的在线点,那么多点测试会很慢。
这是我到目前为止提出的算法。毫无疑问,这很天真,但它确实有效。可能有一些启发式方法可能会有所帮助-例如,轮廓仅可能是具有一定范围内深度的另一个轮廓的子代-但我想先确定基本算法。第一个危险信号是它是指数的。
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
所以这行得通 - 或者至少看起来 - 但它变得非常缓慢,轮廓数量非常多(想想 - 几百,可能几千)。
有没有一种从根本上更好的方法来做到这一点,或者确实 - 是否有已知的算法可以解决这个问题?如前所述 - 在我的案例中,关键是将“是轮廓内”的比较保持在最低限度。
编辑以根据下面吉姆的回答添加解决方案 - 谢谢吉姆!
这是第一次迭代——它产生了很好的(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 测试在第一次边界检查时会失败,而不是目标叶子。第一个解决方案涉及遍历分支以查找目标,根据定义,沿途的每个多边形都将通过边界检查,并涉及完整的多边形测试,直到找不到更多叶子为止。
在吉姆的评论和重新检查代码之后 - 不幸的是,第二个解决方案不起作用。我想知道在分支之前查看树中较低的元素是否仍有一些优点,因为多聚测试应该很快失败,而且你知道如果你找到一个接受候选者的叶子,那里不再需要检查的多边形了。
迭代二重温
虽然不是轮廓只能适合叶子,这是他们几乎总是这样做的情况 - 而且它们通常会适合轮廓有序列表中最近的前身。这个最终更新的代码是最快的,并且完全放弃了树遍历。它只是向后遍历最近较大的多边形并尝试每个多边形 - 来自其他分支的多边形可能会在边界检查中无法通过多边形中的多边形测试,并且发现围绕候选多边形的第一个多边形必须是直接父多边形,因为到列表的先前排序。此代码再次将排序降低到毫秒范围内,并且比树遍历快约 5 倍(对 poly-in-poly 测试也进行了显着的速度改进,这占了其余的加速)。根现在取自已排序的区域列表。知道包含所有轮廓(所有的边界框)。
感谢您的帮助——尤其是吉姆——帮助我思考这个问题。关键实际上是将轮廓原始排序到一个列表中,其中保证没有轮廓可以是列表中稍后轮廓的子级。
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(以升序大小顺序向后遍历轮廓):25 毫秒(还有其他 pt-in-poly 改进)。