动态修剪树

时间:2014-06-25 21:53:17

标签: python algorithm tree

我的问题是:我想查找n个可能数字的所有m长度组合,以使得数字的平均值大于阈值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 for m = 5 and n = 20`约。 96万亿个节点。所以树不能存储在内存中。但它也不必是因为它非常有条理。

获得所有可能的有效组合的方法是通过DFS以一种预订方式遍历树,但是在遍历的同时继续修剪树。例如,在上面的示例中,前三个组合{222, 221, 212}有效,但211无效。这也意味着从上的那一点开始包含两个1的的任何其他组合都不会有效。所以除了122之外,我们可以用root 1修剪树的整个左侧部分!这可以帮助我们避免检查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循环然后跳过组合。是不是可以这样做?

2 个答案:

答案 0 :(得分:1)

一些建议:

  1. 构建 multisets 数字而不是有序列表。有序列表的平均值并不取决于其中的数字的顺序,并且每个多重数字对应于许多有序列表,因此您可以通过仅保留多个集合来节省大量内存,并生成所有相应的在需要时从他们那里订购清单。
  2. 不是从空的多集合开始,而是通过在每个DFS边缘添加一个数字来构建它,从包含n个最高位数的完整多重集开始,并且在每个DFS边缘,< strong>将其中一个数字减1。(这假设在可用数字集中没有&#34;间隙&#34;。)这里的优点是我们知道向下遍历DFS边缘只能降低平均值,所以如果这样做会产生低于阈值的平均值,我们知道我们可以完全修剪分支,因为所有更深层次的后代必须具有更低的平均值。
  3. 您实际上并不需要在任何地方进行单一划分:您需要做的只是将阈值x乘以n以获得最小数字总和一开始,您可以比较多重集的总和。此外,根据先前关于如何生成孩子的建议,孩子的总和总是比其父母的总和少1,所以我们甚至不需要循环来计算总和 - 它是一个恒定的时间操作。
  4. 避免重复

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

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

    但是在DFS期间,我们会从父母那里生成孩子,而不是反过来,所以我们仍然需要改变这个规则,以便选择父母&#34;当我们处于一个可能是某个其他节点的父节点的节点时,是否实际上是&#34;拾取&#34;那个孩子的父母。要做到这一点,请考虑一些子节点v的所有潜在父母。首先,有多少个?如果v具有小于最大数字值的r个不同数字,那么有r个可能的父项,每个父项等于v,但有1个不同的数字大于1。

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

    的所有其他r-1潜在父母大

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

    假设我们在某个节点u,并希望根据上述规则为DFS生成其所有子节点,以便在树中生成每个令人满意的多节点。和以前一样,让d成为你中最小的数字。然后:

    • 如果d> smallest_digit,生成一个与u相同的子项,除了u中的一个d位已减少到d-1。示例:{3,3,3,4,6,6}应该生成孩子{2,3,3,4,6,6}。
    • 如果u包含数字d + 1,则生成与u相同的子项,但(d + 1)-digits中的一个已减少为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
    

    在一个稍微大一点的例子中,数字= {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
    

答案 1 :(得分:0)

看起来你已经过度复杂了。我要做的是将您的数字集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