查找列表中至少相隔 x 的最小 n 值

Posted

技术标签:

【中文标题】查找列表中至少相隔 x 的最小 n 值【英文标题】:Finding the smallest n values that are at least x apart in a list 【发布时间】:2021-06-24 07:18:03 【问题描述】:

我试图在一个列表中找到最小的 n 个值,它们的位置至少相隔 x,考虑到重复项。例如彼此相距至少 2 的最小 5 个值。

简单的例子:

values = [-9995, -82, -659, -1006, -2009, 2062, -10107, -12, 13]
result: [-10107, -9995, -2009, -659, 13]

更复杂的例子:

values = [-9995, -83, -82, -82, -1006, -2009, 18, 2062, -659 ,-9995, -9995]

例如在上面的列表中:

    -9995 是最小值。 -9995 再次出现,并且与第一个相距至少 2。剩余的 -9995 将被忽略,因为它与前一个仅相差 1。 -2009 是第三个最小值 -1006 不被考虑,因为它与之前的值仅相差 1。所以我们取下一个最小值 -659,因为它与之前的值至少相差 2(假设我们取第一个和最后一个 -9995 并忽略倒数第二个) -83 不被考虑,因为它与 -9995 仅相差一个。所以我们取 -82。 我们已经达到了 5 个数字,所以我们停止了
result: [-9995, -9995, -2009, -659, -82]

我正在使用的列表有大约 1,000,000 个元素长,我有大约 1000 个列表。我从 pandas DataFrame 生成了这些列表(通过迭代 groupby),所以如果有一个 numpy/pandas 方法来优化这个计算,那将会很有帮助。

假设没有重复,到目前为止的尝试能够生成结果:


def smallest_values(list_of_numbers: list, n_many: int, x_apart: int):
    
    sorted_values = sorted(values)
    small_val, small_val_loc = [], []

    for val in sorted_values:
        if len(small_val) <= n_many:
            ind = list_of_numbers.index(val)
            within_x = [i for i in range(ind-(x_apart-1), ind+x_apart)]
            if not any(i in small_val_loc for i in within_x):
                small_val_loc.append(ind)     
                small_val.append(val)

    return small_val

values_simple = [-9995, -82, -659, -1006, -2009, 2062, -10107, -12, 13]
values_complex = [-9995, -83, -82, -82, -1006, -2009, 18, 2062, -659 ,-9995, -9995]
d = 2
n = 5
smallest_values(values_simple, n, d) # [-10107, -9995, -2009, -659, 13] CORRECT
smallest_values(values_complex, n, d) # [-9995, -2009, -659, -82] INCORRECT

【问题讨论】:

我不确定您要优化的内容是否明确。考虑[1, 0, 1, 300, 500, 400]n=3 列表。如果你从零开始,你会得到[0, 300, 400],但如果你从一开始,你会得到[1, 1, 400]。第一个具有最小的数字,但第二个具有最小的总数。哪个是正确答案? [0, 300, 400] 将是我的问题的正确结果。我不是在寻找最小的总和,我想要至少相隔 x 的最小 n 个数字。谢谢。该算法将从最小的数字开始,并迭代地添加下一个最小的数字,但要遵守它在列表中与之前添加的数字至少相距 x 的约束 所以你是说你总是取下一个最小的数字,即使那个选择迫使你以后取更大的数字?所以把-1加到上面,使n=4——[-1, 100, 1, 0, 1, 300, 500, 400],正确答案是[-1, 0, 300, 400]不是[-1, 1, 1, 400] 是的,完全正确 在更复杂的示例中取最后一个 -9995 值的逻辑是什么,而不是倒数第二个(这是相同的值,但在列表中较早)? 【参考方案1】:

这是一项复杂的工作,我们希望构建一个索引列表,其中第一个条目是 list_of_numbers 中最小值的索引,并且该 index_list 中的每个下一个条目都指向 list_of_numbers 中的下一个最大值来操作这种类型的list 会更容易和有效。 我们可以这样做:

index_map=dict()
for i in range(len(list_of_numbers)):
    value=list_of_numbers[i]
    if value in index_map:
        index_map[value]+=[i]
    else:
        index_map[value]=[i]
sorted_values = sorted(index_map)

现在我们有一个包含 list_of_numbers 中每个唯一值的字典,它映射到指向它的所有索引。我们还有一个从最小的唯一值到最大的列表。我们现在可以构建我们的 index_list:

index_list=[]
for value in sorted_values:
    index_list+=index_map[value]

del index_map, sorted_values

剩下要做的就是在我们的 index_list 中从左到右迭代并找到具有适当间隙的索引的第一个组合。这在算法中计算起来更容易、更快。

不幸的是,时间复杂度不可能小于 O(n),因为您需要检查 list_of_numbers 中的每个条目以找到最小的条目。

我使用递归函数做到了这一点,但您绝对可以优化它并使算法更智能:

def gap_selecter(numlist, n_many, gap):

if numlist==None:          # Fast exit if recursion fails
    return None

x=numlist[0]
speudolist=numlist[1:]
                    
if n_many==1:              # base case
    return [x] 
                            

else:
    for i in range(len(speudolist)):
        
        if abs(x-speudolist[i])>=gap:   #recursive step occurs here
            
            recursion_list = gap_selecter(speudolist[i:], n_many-1, gap)   
            
            if recursion_list !=None:
                return [x]+recursion_list

return None                # if we find no possible list we return None

这里是所有的东西。

def smallest_values(list_of_numbers: list, n_many: int, x_apart: int):

index_map=dict()
for i in range(len(list_of_numbers)):
    value=list_of_numbers[i]
    if value in index_map:
        index_map[value]+=[i]
    else:
        index_map[value]=[i]
sorted_values = sorted(index_map)

index_list=[]
for value in sorted_values:
    index_list+=index_map[value]

del index_map, sorted_values

final_indices=gap_selecter(index_list, n_many, x_apart)
if final_indices==None:
    return None

final_numbers=[]
for i in final_indices:
    final_numbers+=[list_of_numbers[i]]

return final_numbers

values_simple = [-9995, -82, -659, -1006, -2009, 2062, -10107, -12, 13]
values_complex = [-9995, -83, -82, -82, -1006, -2009, 18, 2062, -659 ,-9995, -9995]
d = 2
n = 5

test_simple = smallest_values(values_simple, n, d)       # [-10107, -9995, -2009, -659, -12]
test_complex = smallest_values(values_complex, n, d)     # [-9995, -9995, -2009, -659, -83]

【讨论】:

test_complex 失败了,不是吗?它不应该有-83(如果我正确理解了这个问题) 是的,很抱歉,在这个简单的例子中应该是[-10107, -9995, -2009, -659, 13] -12 在 -10107 旁边,正如 perl 提到的那样,不应该选择 -83【参考方案2】:

//编辑:啊,现在我明白了。关键词是(assuming we take the first and last -9995 and ignore the second to last)

最大的问题是您不能选择任何重复的值(在您的示例中,列表中的 倒数第二个 -9995)。相反,您希望选择重复值,以使最后一个元素(或结果列表的总和?)最小,对吗?

对我来说,这听起来像是一个受限的优化问题。我什至不确定它是否具有相同的结果,具体取决于您定义为“最佳”的内容(总和或最后一个元素或其他...)

【讨论】:

【参考方案3】:

这里的关键问题是打破重复值的关系,例如示例中的 -9995。我们基本上需要尝试以不同的顺序选择它们,并检查哪一个产生具有下一个较低值的序列(或者如果下一个值相同,那么接下来的那个,等等)。

一种方法是递归搜索:

from collections import defaultdict

# find the next smallest and return all locations of that number
# that can be used (i.e. not within d from the previously used values)
def get_next(vs, vd, d, skip):
    for v in vs:
        os = []
        for l in vd[v]:
            if not any([l>x-d and l<x+d for x in skip]):
                os.append((l, v))
        if len(os) > 0:
            return os
    return None

# recursive search
def r(vs, vd, n, d, skip=[], out=[]):
    if len(out) >= n:
        return out
    
    os = []
    for (l, v) in get_next(vs, vd, d, skip):
        o = r(vs, vd, n, d, skip+[l], out+[v])
        os.append(o)
    mo = min(os)
    return mo

# main func
def smallest_values(values, n, d):
    vd = defaultdict(list)
    for l, v in enumerate(values):
        vd[v].append(l)
    vs = sorted(vd.keys())
    return r(vs, vd, n, d, [], [])

对提供的示例进行测试:

values_simple = [-9995, -82, -659, -1006, -2009, 2062, -10107, -12, 13]
values_complex = [-9995, -83, -82, -82, -1006, -2009, 18, 2062, -659 ,-9995, -9995]

print('simple:  ', smallest_values(values_simple, 5, 2))
print('complex: ', smallest_values(values_complex, 5, 2))

输出:

simple:   [-10107, -9995, -2009, -659, 13]
complex:  [-9995, -9995, -2009, -659, -82]

对 1,000,000 个值列表的计时测试(800 毫秒,因此单线程 1,000 个列表大约需要 15 分钟):

%%time
vs = np.random.randint(0, 1000000, 1000000)
smallest_values(vs, 5, 2)

输出:

CPU times: user 780 ms, sys: 20.8 ms, total: 800 ms
Wall time: 800 ms
[3, 5, 6, 7, 8]

附:这会找到序列中较早的具有最低值的序列。例如,它会更喜欢[1, 2, 100] 而不是[1, 3, 4](两者都在位置 1 有 1,但第一个序列在位置 2 有 2 I guess the logic would be worded as, is there a selection of the previous chosen values that allows the next smallest value to be chosen.

【讨论】:

你的方法很聪明,非常感谢。你对问题陈述的理解是正确的,[1,2,100]应该是首选 酷,很高兴它有帮助!原来是一个非常有趣的问题:)

以上是关于查找列表中至少相隔 x 的最小 n 值的主要内容,如果未能解决你的问题,请参考以下文章

使用绝对函数查找两个数字的最小值

SML,如何查找列表中最小数量的出现次数?

在数组中查找 n 个最小值

在python中跨多个列表查找列的最小值

用于查找数组中最小值的分治算法

在列表列表中查找最小值[重复]