【问题标题】:Dynamic pruning of a tree树的动态修剪
【发布时间】:2014-06-25 21:53:16
【问题描述】:

我的问题是:我想找到m 可能数字的所有n 长度组合,使得数字的平均值大于阈值X

例如,假设长度为n=3,数字为{1, 2},阈值为1.5。允许的总组合为 2*2*2 == 2**3 = 8

222 - avg 2.000000 > 1.500000 -> include in acceptable set
221 - avg 1.666667 > 1.500000 -> include in acceptable set
212 - avg 1.666667 > 1.500000 -> include in acceptable set
211 - avg 1.333333 < 1.500000 -> adding 1 and below to the exclude list
122 - avg 1.666667 > 1.500000 -> include in acceptable set
121 - avg 1.333333 < 1.500000 -> skipping this vote combo
112 - avg 1.333333 < 1.500000 -> skipping this vote combo
111 - avg 1.000000 < 1.500000 -> skipping this vote combo

final list of valid  votecombos
[[2, 2, 2], [2, 2, 1], [2, 1, 2], [1,2,2]]

我认为,解决这个问题的方法是想象一棵包含所有可能组合的树,然后动态修剪树以获得不可能的解决方案。例如想象n=3这样的级别树

                            root
                         /        \
                        1          2
                    /      \    /     \
                   1       2    1     2
                 /  \    /  \  /  \  /  \
                1   2   1   2  1  2  1   2

到叶子的每条路径都是一个可能的组合。正如您可以想象的那样,n=3m=5 级别的节点数约为 N == m**n == 5**3 == 125' nodes. Its easy to see that the tree gets really really large even form=5andn=20`。 96 万亿个节点。所以树不能存储在内存中。但它也不必如此,因为它非常结构化。

获得所有可能的有效组合的方法是通过 DFS 以一种预先排序的方式遍历树,但在遍历的同时继续修剪树。例如,在上面的示例中,前三个组合{222, 221, 212} 有效,但 211 无效。这也意味着任何其他包含两个 1 的 从那时起 的组合都将无效。所以我们几乎可以用根 1 修剪树的整个左侧,除了 122 !这将有助于我们避免检查 3 种组合。

为此,我编写了一个简单的 python 脚本

import string
import itertools
import numpy as np
import re

chars = '21'
grad_thr = 1.5
seats = 3

excllist = []
validlist = []

for word in itertools.product(chars, repeat = seats):
    # form the string of digits
    votestr = ''.join(word)
    print (votestr)

    # convert string into list of chars
    liststr = list(votestr)
    #print liststr

    # map list of chars to list of ints
    listint = map(int, liststr)

    if len(list(set(listint) & set(excllist))) == 0:
        # if there are no excluded votes in this votecombo then proceed

        # compute a function over the digits; func can be average/bayesian score/something else.
        y_mean = np.mean(listint)
        print 'avg %f' %y_mean
        #y_bayes = bayesian score

        if y_mean >= grad_thr:
            # if function result is greater than grad threshold then save result to a list of valid votes
            validlist.append(listint)
            print 'geq than %f -> include in acceptable set' %grad_thr

        elif y_mean < grad_thr:
            # if function result is not greater than grad threshold then add logic to stop searching the tree further
            # prune unnecessary part of the tree

            if listint[-1] not in excllist:
                excllist = [int(d) for d in range(listint[-1] + 1)]
                print 'adding %d and below to the exclude list' %listint[-1]
            else:
                print '%d already present in exclude list' %listint[-1]

    else:
        print 'skipping this vote combo'

print '\nfinal valid list of votecombos'
print validvotelist
print 'exclude list'
print excllist
print '\n'

通过这种方式,我会遍历所有可能的组合并 跳过 以避免计算平均值。但是,在进入 for 循环后,我仍然会检查每个可能的组合。

是否可以根本不检查组合?即我们知道组合 121 不起作用,但是我们仍然必须进入 for 循环然后跳过组合。可以不做吗?

【问题讨论】:

  • 为什么n=4 在您的第一个示例中?看起来你只接受长度为3 的字符串。为什么122 的平均值不是1.667
  • 抱歉,已编辑 n=3
  • 我还是想知道122的位数的平均值是1.33,而且111的位数的平均值也是1.33...跨度>
  • 希望改正错误
  • 我想到了一种不同的算法,它从 DFS 树根处最高数字的 n 个副本的多重集开始,并在每个 DFS 边缘减少一些数字。这导致有效的修剪。它将适用于任意数字集,但如果没有孔,它会更简单,因为这意味着孩子的总和总是比其父母少 1。

标签: python algorithm tree


【解决方案1】:

一些建议:

  1. 构建 multisets 数字而不是有序列表。有序列表的平均值不依赖于其中数字的顺序,并且每个数字的多重集对应于许多有序列表,因此您可以通过仅保留多重集并生成所有对应的有序列表来节省大量内存需要时从他们那里获得。
  2. 不是从一个空的多重集开始并通过在每个 DFS 边缘添加一个数字来构建它,从一个包含最高数字的 n 个副本的完整多重集开始,并且在每个 DFS 边缘中,将其中一个数字减 1。(假设在可用数字集合中没有“间隙”。)这里的好处是我们知道向下遍历 DFS 边缘只能减少平均值,所以如果这样做会产生低于阈值的平均值,我们知道我们可以完全修剪分支,因为所有更深的后代必须具有更低的平均值。
  3. 您实际上不需要在任何地方进行单个除法:您所需要做的就是将阈值 x 乘以 n 以在一开始就得到一个最小数字 sum,即然后,您可以比较多重集的总和。此外,根据前面关于如何生成子项的建议,子项的总和总是比其父项的总和小 1,因此我们甚至不需要循环来计算总和 - - 这是一个恒定时间的操作。

避免重复

上面(2)中生成孩子的规则确实带来了一个困难:我们如何确保我们不会多次生成孩子?例如。如果我们在树中有一个节点包含多重集 {5, 8}(在此示例中只是一个普通集),那么这将生成子节点 {4, 8} 和 {5, 7};但是如果我们在树的某个地方为集合 {4, 9} 有另一个节点,那么这将生成子 {3, 9} 和 {4, 8} - 所以子 {4, 8} 将生成两次.

解决这个问题的方法是找出一个规则,让每个孩子都可以“挑选”一个独特的父母,然后安排事情让父母只生成他们将成为“挑选”父母的孩子 例如我们可以说一个孩子应该选择它的唯一父母作为它的唯一父母,在所有可以生成它的父母中,当它的元素按升序排列时,它在字典上是最大的。 (您也可以选择按字典顺序最小的,但结果证明最大的计算效率更高。)对于示例多重集 {4, 8},可以生成它的两个父节点是 {5, 8} 和 {4, 9} ;其中,{5, 8} 按字典顺序较大,因此我们将其选为父代。

但是在 DFS 期间,我们从父节点生成子节点,而不是反过来,所以我们仍然需要将“选择父节点”的规则转换为告诉我们,当我们处于可能是父节点的节点时其他一些节点,无论它实际上是那个孩子的“挑选”父母。为此,请考虑某个子节点 v 的所有潜在父节点。首先,有多少个?如果 v 有 r 个不同的数字小于最大数字值,则有 r 个可能的父代,每个都等于 v,但有 1 个不同的数字大 1。

假设 v 中的最低位是 d,并且它有 k >= 1 个副本。在 v 的 r 个潜在父代中,它们都将有 k 个 d 副本——除了一个父代 u,它将有 k-1 个副本,因为在这个父代中,必须是数字 d+1减少 1 到 d(从而将 d 的副本数量从 k-1 增加到 k)以产生 v。现在如果我们写出 v 的 r 个潜在父级,其数字按升序排列,请注意,除了u 将从 d 的 k 个副本开始,而 u 从 d 的 k-1 个(可能是 0)个副本开始,然后是 d+1 个(至少 1 个副本)。因此 u 在字典上比 v 的所有其他 r-1 个潜在父代都大。

这告诉我们从潜在父节点的角度成为选择父节点的标准。假设 u 中的最小数字是 d。 然后某个节点 v 将 u 作为其选择的父节点当且仅当 v 可以通过将 u 中的 d-digit 减少到 d-1 或通过将 u 中的 (d+1)-digit 减少到 d 来形成. 这转化为生成孩子的两个简单而有效的规则:

假设我们在某个节点 u,并且想要根据上面的规则为 DFS 生成它的所有子节点,这样每个满足的多重集在树上只生成一次。和以前一样,设 d 是 u 中的最小数字。那么:

  • 如果 d > minimum_digit,则生成一个与 u 相同的子代,除了 u 中的一个 d 位已减少到 d-1。示例:{3, 3, 3, 4, 6, 6} 应生成子 {2, 3, 3, 4, 6, 6}。
  • 如果 u 包含一个数字 d+1,则生成一个与 u 相同的子代,除了 (d+1) 个数字之一已减少为 d。示例:{3, 3, 3, 4, 6, 6} 应生成子 {3, 3, 3, 3, 6, 6}。

所以在上面的例子中,节点 u = {3, 3, 3, 4, 6, 6} 会生成 2 个子节点。 (没有节点会生成超过 2 个子节点。)

如果多重集表示为排序列表或排序顺序中数字的频率计数,则只需扫描初始段即可有效地检查这两个条件。

示例

在您的示例中(请记住,我们仅在此处生成多重集;在单独的步骤中生成它们的每个排列以查找所有有序列表):

sum_threshold = 1.5*3 = 4.5

                                       SUM
                        222             6
                      /
                    122                 5
                  /
                112                     4
               PRUNE

在一个稍微大一点的例子中,digits = {1, 2, 3}, n = 3 和 x = 0(表明所有多重集都将生成一次):

                                       SUM
                       333              9
                     /
                   233                  8
                 /     \
               133     223              7
                      /  \
                    123  222            6
                    /      \
                  113      122          5
                             \
                             112        4
                               \
                               111      3

【讨论】:

  • 我喜欢这种方法。从某种意义上说,我的解决方案会生成由您的解决方案构建的树的叶子。从那里您可以构建所有其他解决方案,但如果您不聪明,它最终会变得非常多余。您的解决方案摆脱了这种冗余,并且更加简单。
【解决方案2】:

看来你把这个复杂化了。我要做的是取你的数字集A,你的字符串长度n,和你的阈值T,然后解决以下优化问题:

Minimize the sum of n elements of A (with repeats) such that the sum still exceeds
the threshold value T.

您可以使用结果的argmin 然后生成一个多重集,您可以从中提取 而无需 替换以获得任何有效的字符串。例如,任何具有两个 2 的字符串的平均数字值都会超过您的阈值,因此多重集 M = [1, 2, 2, 2] 的任何三元素子集的任何排序都是有效的。

编辑:这是一种可以生成最小有效多重集的方法。 partitionfunc 的定义是从this SO post 借来的,然后我只过滤掉那些元素都在digit_set 中的列表。 min_sum 获得上限操作的原因是因为我假设数字必须是整数,因此它们的总和将是整数。因此,为了超过阈值,数字和的值必须不小于ceil(num_digits * threshold)。希望这会有所帮助!

from math import ceil

def partitionfunc(n,k,l=1):
'''n is the integer to partition, 
   k is the length of partitions, 
   l is the min partition element size'''
if k < 1:
    raise StopIteration
if k == 1:
    if n >= l:
        yield (n,)
    raise StopIteration
for i in range(l,n+1):
    for result in partitionfunc(n-i,k-1,i):
        yield (i,)+result

def find_min_sets(num_digits, digit_set, threshold):
  min_sum = ceil(num_digits * threshold)
  min_sets = [l for l in partitionfunc(min_sum, num_digits) if
              all(map(lambda x: x in digit_set, l))]
  return min_sets

【讨论】:

  • 多重集的想法很好,但可以有许多不同的最小多重集:例如如果digits = {1, 2, 3},n = 2 和x = 1.9,那么{3, 1} 和{2, 2} 都是来自argmin 的可能答案。随着 n 和/或位数的增加,这种最小多重集的数量迅速增长。
  • 好点。最好找到数字和的最小可能值V,并找出V 的所有n-partitions,这听起来像是一个易于处理的动态规划问题(提醒我“有多少种方法可以使N 美元与集合S 的货币面额..)
  • 通过一些调整,这将是一个快速而完整的解决方案。首先,您不仅应该使用k=min_sum 调用partitionfunc,还应该在从min_summax_sum 的循环中调用max_sumnum_digits * max_digit:所有这些分区都将是有效的多重集,并且与每个分区不同其他:)
  • 其次,为了尽量减少partitionfunc 产生的结果之间的延迟,您应该尽早使用if n &gt; k * max_digit || n &lt; k * l: raise StopIteration 退出(我的Python 语法可能是错误的)。我认为这将保证结果之间的 O(m) 时间(这是最佳的),前提是允许的数字集是从 1 开始到某个数字 max_digit 结束的连续范围。 (如果没有这种“早期”检查,partitionfunc 可能需要以 m 为单位的指数时间才能找到下一个有效分区:...
  • ... 例如假设 n=10, k=5 和 digits = {1, 2},那么它将尝试 11111, 11112, 11121, 11122, 11211, 11212, 11221, 11222, ...,然后最终到达有效的多集 22222。)
猜你喜欢
  • 2019-05-22
  • 2019-06-14
  • 2012-07-20
  • 1970-01-01
  • 2012-02-09
  • 1970-01-01
  • 1970-01-01
  • 2022-07-18
相关资源
最近更新 更多