【发布时间】:2014-09-20 23:35:21
【问题描述】:
我试图将二叉树拆分为 k 个大小相似的部分(通过删除 k-1 条边)。有没有解决这个问题的有效算法?或者它是NP难的?任何指向论文、问题定义等的指针?
-- 评估分区质量的一个合理指标可能是最大和最小分区之间的大小差距;另一个指标可能是使最小的分区具有尽可能多的顶点。
【问题讨论】:
标签: algorithm tree binary-tree partition np-hard
我试图将二叉树拆分为 k 个大小相似的部分(通过删除 k-1 条边)。有没有解决这个问题的有效算法?或者它是NP难的?任何指向论文、问题定义等的指针?
-- 评估分区质量的一个合理指标可能是最大和最小分区之间的大小差距;另一个指标可能是使最小的分区具有尽可能多的顶点。
【问题讨论】:
标签: algorithm tree binary-tree partition np-hard
这是一个多项式确定性解:
假设树是有根的,并且有两个固定值:MIN 和 MAX - 一个组件的最小和最大允许大小。
然后可以使用动态规划来检查是否存在分区,使得每个组件大小在MIN和MAX之间:
假设f(node, cuts_count, current_count) 是true 当且仅当有一种方法可以在node 的子树中精确切割cuts_count 以便current_count 顶点连接到node所以条件 2) 成立。
叶子的基本情况是:f(leaf, 1, 0)(从父节点到叶子的边)是true当且仅当MIN <= 1和MAX >= 1f(leaf, 0, 1)(不剪它)总是true(对于cuts_count 和current_count 的所有其他值,它是false)。
要为node(不是叶子)计算f,可以使用以下算法:
//Combine all possible children states.
for cuts_left in 0..k
for cuts_right in 0..k
for cnt_left in 0..left_subtree_size
for cnt_right in 0..right_subtree_size
if f(left_child, cuts_left, cnt_left) is true and
f(right_child, cuts_right, cnt_right) is true and then
f(node, cuts_left + cuts_right, cnt_left + cnt_right + 1) = true
//Cut an edge from this node to its parent.
for cuts in 0..k-1
for cnt in 0..node's_subtree_size
if f(node, cuts, node's_subtree_size) is true and MIN <= cnt <= MAX:
f(node, cuts + 1, 0) = true
这个伪代码所做的是结合节点子节点的所有可能状态来计算该节点的所有可达状态(第一组 for 循环),然后通过切割该节点与其父节点之间的边来生成其余可达状态(第二组for循环)(状态表示(node, cuts_count, current_count)元组。如果f(state)是true,我称之为可达)。
有两个孩子的节点就是这样,有一个孩子的情况可以类似的处理。
最后,如果f(root, k, 0) 是true,则可以找到对条件 2) 进行分层的分区,否则就不可能。我们需要“假装”我们在这里做了k 切割,因为当我们为根计算f(以避免角落案例)。
该算法的空间复杂度(对于固定的MIN和MAX)为O(n^2 * k)(n为节点数),时间复杂度为O(k^2 * n^2)。看起来复杂度实际上是O(k^2 * n^3),但并非如此,因为节点左右子树中顶点数的乘积恰好是节点对的数量,因此它们的最小共同祖先是该节点。但是节点对的总数是O(n^2)(并且每一对只有一个最小共同祖先)。因此,所有节点左右子树大小的乘积之和为O(n^2)。
可以简单地尝试所有可能的MIN 和MAX 值并选择最佳值,但它可以做得更快。关键的观察是,如果有MIN 和MAX 的解决方案,则总是有MIN 和MAX + 1 的解决方案。因此,可以遍历MIN(n / k 不同的值)的所有可能值,并应用二分搜索来找到最小的MAX,这给出了一个有效的解决方案(log n 迭代)。所以整体时间复杂度为O(n^2 * k^2 * n / k * log n) = O(n^3 * k * log n)。但是,如果您想最大化MIN(而不是最小化MAX 和MIN 之间的差异),您可以简单地使用此算法并在任何地方忽略MAX 值(通过将其值设置为n)。然后不需要对MAX 进行二分搜索,但可以改为对MIN 进行二分搜索并获得O(n^2 * k^2 * log n) 解决方案。
要重建分区本身,可以从f(root, k, 0) 开始并应用我们用来计算f 的步骤,但这次是相反的方向(从根到叶)。还可以保存有关如何获取每个状态的值的信息(结合了哪些子状态或边缘被切割之前的状态是什么)(并在f的初始计算期间适当更新它)然后使用这些数据重建分区(如果我对这一步的解释看起来不是很清楚,阅读一篇关于动态编程的文章并重建答案可能会有所帮助)。
所以,在二叉树上,这个问题有一个多项式解决方案(即使对于任意图来说它是 NP-hard)。
【讨论】:
我可以建议非常快速的解决方案,以使最小的部分具有尽可能多的顶点。
假设我们猜测最小部分的大小 S 并想检查它是否正确。 首先我想说几句:
如果树的总大小大于 S,则至少有一个子树大于 S,并且该子树的所有子树都较小。 (检查两个最大的就足够了。)
如果有某种方法可以分割树,其中最小部分的大小 >= S,并且我们有子树 T,其所有子树的所有子树都小于 S,我们可以允许不删除 T 内的边。 (因为任何这样的删除都会创建一个小于 S 的分区)
如果有某种方法可以拆分最小部分的大小 >= S 的树,并且我们有一些大小 >= S 的子树 T,内部没有删除的边但不是部分之一,我们可以拆分以其他方式的树,其中子树 T 将是部分本身之一,并且所有部分将不小于 S。(只需将一些额外的顶点从原始部分移动到任何其他部分,其他部分不会变小。)
所以这里有一个算法来检查我们是否可以将树分成不小于 S 的 k 个部分。
找到所有合适的顶点(大小 >= S 的子树的根和两个子树的大小
当列表不为空且零件数量小于 K 时,从列表中取出一个顶点并将其从树上切断。比更新父顶点的子树的大小,如果其中一个变得合适,则添加到列表中。 你甚至不需要更新所有的父顶点,只有在你首先发现哪个新的子树大小大于 S 时,父顶点还不适合添加到列表中,可以稍后更新。
您可能需要重新构造树以恢复分配给顶点的原始子树大小。
现在我们可以使用二分法了。我们可以将上限确定为 Smax = n/k,下限可以从等式 (2*Smin- 1)*(K - 1) + Smin = N 中得到两个大小为 Smin - 1 个的子子树,我们将剩下大小为 Smin 的部分。 Smin = (n + k -1)/(2*k - 1) 现在我们可以检查 S = (Smax + Smin)/2 如果我们设法使用上述方法构造分区,则 S 小于或等于它的最大可能值,那么构造分区中的最小部分也可能大于 S,如果我们失败 S,我们可以为其设置新的下限而不是 S比可能的更大。
一次检查的时间复杂度是 k 乘以每次更新的父节点数,对于平衡良好的树,更新节点的数量是恒定的(我们将使用前面解释的技巧并且不会更新所有父节点),但它仍然不是更大对于最终不平衡的树,在最坏情况下比 (n/k)。搜索合适的顶点具有非常相似的行为(搜索时传递的所有顶点将在稍后更新。)。
n/k 和 (n + k -1)/(2*k - 1) 之间的差值与 n/k 成正比。
所以我们有时间复杂度 O(k * log(n/k)) 如果我们有预先计算的子树大小,O(n) 如果子树大小没有预先计算,O(n * log(n/k) ) 在最坏的情况下。
这种方法可能会导致最后一个部件相对较大,但我想一旦你得到建议的方法,你就可以找出一些改进来最小化它。
【讨论】: