基于“2-d迭代器”的“排序1-d迭代器”(迭代器的笛卡尔积)

时间:2011-05-09 14:29:35

标签: python iterator

我正在寻找一种在Python中执行此操作的简洁方法:

假设我有两个迭代器“iter1”和“iter2”:也许是素数生成器和itertools.count()。我先验地知道两者都是无限的并且单调递增。现在我想对两个args“op”(可能是operator.add或operator.mul)进行一些简单的操作,并使用每个元素计算第一个迭代器的每个元素接下来,使用所述操作,然后一次一个地产生它们,进行分类。显然,这本身就是一个无限的序列。 (正如@RyanThompson在评论中所提到的:这将被称为这些序列的Cartesian Product ......或者更准确地说,是该产品的1d类型。)

最好的方法是:

  • 汇总“iter1”,“iter2”和“op”在一个迭代中,它本身会产生单调增加输出的值。

允许简化假设:

  • 如果有帮助,我们可以假设op(a,b)> = a和op(a,b)> = b。
  • 如果有帮助,我们可以假设op(a,b)> op(a,c)for all b>角

也允许:

  • 同样可以接受的是迭代器以“一般增加”的顺序产生值...我的意思是迭代可能偶尔会给我一个小于前一个的数字,但它会以某种方式使“辅助信息”可用(通过一个对象的方法)会说“我不保证我给你的下一个值将大于我刚给你的那个值,但我确定所有未来的值至少会大于N. “......和”N“本身是单调增加的。

我能想到这样做的唯一方法是一种“对角化”过程,在这里我会保留越来越多的部分处理的迭代,并且“向前看”所有可能的next()值的最小值,并且产量。但是,即使在我开始对代码进行编码之前,这种古怪的聚集和一堆deques似乎都是异乎寻常的。

请:根据我的例子中提到的primes或count()这个事实的答案....我对这个概念有几个用途,与素数和计数无关( )。


更新: OMG!多么棒的讨论!并通过非常彻底的解释得到一些很好的答案。非常感谢。 StackOverflow摇滚;你们摇滚吧。

我将尽快深入研究每个答案,并给出示例代码。从我到目前为止所读到的内容来看,我最初的怀疑是确认没有“简单的Python成语”来做到这一点。相反,通过这种或那种方式,我无法避免无限期地保持iter1和iter2的所有产生的值。

FWIW:如果您想尝试解决方案,这是一个官方的“测试案例”。

import operator

def powers_of_ten():
    n = 0
    while True:
        yield 10**n
        n += 1

def series_of_nines():
    yield 1
    n = 1
    while True:
        yield int("9"*n)
        n += 1

op = operator.mul
iter1 = powers_of_ten()
iter2 = series_of_nines()

# given (iter1, iter2, op), create an iterator that yields:
# [1, 9, 10, 90, 99, 100, 900, 990, 999, 1000, 9000, 9900, 9990, 9999, 10000, ...]

6 个答案:

答案 0 :(得分:5)

import heapq
import itertools
import operator


def increasing(fn, left, right):
    """
    Given two never decreasing iterators produce another iterator
    resulting from passing the value from left and right to fn.
    This iterator should also be never decreasing.
    """
    # Imagine an infinite 2D-grid.
    # Each column corresponds to an entry from right
    # Each row corresponds to an entry from left
    # Each cell correspond to apply fn to those two values

    # If the number of columns were finite, then we could easily solve
    # this problem by keeping track of our current position in each column
    # in each iteration, we'd take the smallest value report it, and then
    # move down in that column. This works because the values must increase
    # as we move down the column. That means the current set of values
    # under consideration must include the lowest value not yet reported

    # To extend this to infinite columns, at any point we always track a finite
    # number of columns. The last column current tracked is always in the top row
    # if it moves down from the top row, we add a new column which starts at the top row
    # because the values are increasing as we move to the right, we know that
    # this last column is always lower then any columns that come after it





    # Due to infinities, we need to keep track of all
    # items we've ever seen. So we put them in this list
    # The list contains the first part of the incoming iterators that
    # we have explored
    left_items = [next(left)]
    right_items = [next(right)]

    # we use a heap data structure, it allows us to efficiently
    # find the lowest of all value under consideration
    heap = []

    def add_value(left_index, right_index):
        """
        Add the value result from combining the indexed attributes
        from the two iterators. Assumes that the values have already
        been copied into the lists
        """
        value = fn( left_items[left_index], right_items[right_index] )
        # the value on the heap has the index and value.
        # since the value is first, low values will be "first" on the heap
        heapq.heappush( heap, (value, left_index, right_index) )

    # we know that every other value must be larger then 
    # this one. 
    add_value(0,0)

    # I assume the incoming iterators are infinite
    while True:
        # fetch the lowest of all values under consideration
        value, left_index, right_index = heapq.heappop(heap)

        # produce it
        yield value

        # add moving down the column
        if left_index + 1 == len(left_items):
            left_items.append(next(left))

        add_value(left_index+1, right_index)

        # if this was the first row in this column, add another column
        if left_index == 0:
            right_items.append( next(right) )
            add_value(0, right_index+1)






def fib():
    a = 1
    b = 1
    while True:
        yield a
        a,b = b,a+b



r = increasing(operator.add, fib(), itertools.count() )
for x in range(100):
    print next(r)

答案 1 :(得分:4)

将序列定义为:

a1 <= a2 <= a3 ...
b1 <= b2 <= b3 ...

简而言之a1b1表示op(a1,b1)

根据您允许的假设(非常重要),您知道以下内容:

max(a1, b1) <= a1b1 <= a1b2 <= a1b3 ...
   <=
max(a2, b1) <= a2b1 <= a2b2 <= a2b3 ...
   <=
max(a3, b1) <= a3b1 <= a3b2 <= a3b3 ...
    .     .
    .      .
    .       .

您必须执行以下操作:

生成a1b1。您知道,如果继续增加b变量,则只能获得更高的值。现在的问题是:通过增加a变量是否有较小的值?您的下限为min(a1, b1),因此您必须将a值增加到min(ax,b1) >= a1b1。一旦达到这一点,您就可以找到anb1 1 <= n <= x所在的最小值并安全地获得该值。

然后,您将拥有多个水平链,您必须跟踪它们。每当您的值超过min(ax,b1)时,您必须增加x(添加更多链),直到min(ax,b1)大于安全发送它之前的a1bn

只是一个起点...我现在没有时间对其进行编码。

编辑:哦,嘿,这正是你已经拥有的。好吧,没有更多的信息,这就是你所能做的一切,因为我非常确定在数学上,这是必要的。

EDIT2:至于您的“可接受”解决方案:您可以按n的递增顺序生成min(a1,b1),将N作为op(a,c) = P返回。你需要更加具体。你说的好像你有一个你通常想看的东西的启发式,你想要通过两个迭代进行的一般方式,但没有告诉我们它是什么我不知道如何做得更好。


更新:温斯顿很好,但假设海报没有提到:op(b,c)&gt; b>a如果op(a,b)>=a。但是,我们知道op(a,b)>=bdef increasing(fn, left, right): left_items = [next(left)] right_items = [next(right)] #columns are (column value, right index) columns = [(fn(left_items[0],right_items[0]),0)] while True: #find the current smallest value min_col_index = min(xrange(len(columns)), key=lambda i:columns[i][0]) #generate columns until it's impossible to get a smaller value while right_items[0] <= columns[min_col_index][0] and \ left_items[-1] <= columns[min_col_index][0]: next_left = next(left) left_items.append(next_left) columns.append((fn(next_left, right_items[0]),0)) if columns[-1][0] < columns[min_col_index][0]: min_col_index = len(columns)-1 #yield the smallest value yield columns[min_col_index][0] #move down that column val, right_index = columns[min_col_index] #make sure that right value is generated: while right_index+1 >= len(right_items): right_items.append(next(right)) columns[min_col_index] = (fn(left_items[min_col_index],right_items[right_index+1]), right_index+1) #repeat

这是我的解决方案,采取第二个假设,但不是温斯顿采取的。不过,为了代码结构而向他道具:

def pathological_one():
    cur = 0
    while True:
        yield cur
        cur += 100

def pathological_two():
    cur = 0
    while True:
        yield cur
        cur += 100

lookup = [
    [1,   666, 500],
    [666, 666, 666],
    [666, 666, 666],
    [666, 666, 666]]

def pathological_op(a, b):
    if a >= 300 or b >= 400: return 1005
    return lookup[b/100][a/100]

r = increasing(pathological_op, pathological_one(), pathological_two())
for x in range(15):
    print next(r)

对于演示差异的(病态)输入,请考虑:

>>> 
1
666
666
666
666
500
666
666
666
666
666
666
1005
1005
1005
温斯顿的答案给出了:

>>> 
1
500
666
666
666
666
666
666
666
666
666
666
1005
1005
1005

虽然我给出了:

{{1}}

答案 2 :(得分:2)

所以你基本上想要采用两个单调递增的序列,然后(懒惰地)计算它们之间的乘法(或加法或另一个运算)表,这是一个二维数组。然后,您希望将该二维数组的元素按排序顺序放置并迭代它们。

一般来说,这是不可能的。但是,如果您的序列和操作是这样的,您可以对表的行和列做出某些保证,那么您可以取得一些进展。例如,假设您的序列仅是正整数的单调增加序列,并且该运算是乘法(如您的示例所示)。在这种情况下,我们知道数组的每一行和一列都是单调递增的序列。在这种情况下,您不需要计算整个数组,而只需计算其中的一部分。具体而言,您必须跟踪以下内容:

  • 您使用了多少行
  • 您从已使用的每一行中获取的元素数量
  • 您使用的输入序列中的每个元素,以及每个
  • 中的一个元素

要计算迭代器中的下一个元素,必须执行以下操作:

  • 对于您曾经使用过的每一行,计算该行中的“下一个”值。例如,如果您已使用第1行中的5个值,则通过从第一个序列获取第一个值并从第二个序列获取第6个值(两者都有)来计算第6个值(i = 1,j = 6)缓存)并将操作(乘法)应用于它们。另外,计算第一个未使用行中的第一个值。
  • 取您计算的所有值中的最小值。将此值作为迭代器中的下一个元素
  • 增加您在上一步中对元素进行采样的行的计数器。如果从新的未使用的行中获取该元素,则必须增加已使用的行数,并且必须为初始化为1的行创建新计数器。如有必要,还必须计算更多的值一个或两个输入序列。

这个过程有点复杂,特别注意要计算N值,你必须在最坏的情况下保存一个与 N的平方根成比例的状态量。(编辑:sqrt (N)实际上是最佳情况。)这与典型的生成器形成鲜明对比,典型的生成器只需要恒定的空间来遍历其元素,无论长度如何。

总之,您可以在某些假设下执行此操作,并且您可以为其提供类似于生成器的界面,但它不能以“流式”方式完成,因为您需要保存大量状态才能以正确的顺序遍历元素。

答案 3 :(得分:2)

让我先谈谈如何直观地解决这个问题。

因为内联读代码有点乏味,我会介绍一些符号:

表示法

  • i1 将代表iter1 i1 0 将代表iter1的第一个元素。 iter2
  • 也是如此
  • ※代表op运营商

直观的解决方案

通过使用简化假设2,我们知道 i1 0 i2 0 是最小元素将永远从你的最终迭代器中获益。下一个元素是较小的 i1 0 i2 1 i1 < sub> 1 ※ i2 0

假设 i1 0 i2 1 较小,则会产生该元素。接下来,您将得到较小的 i1 1 i2 0 i1 1 i2 0 i1 1 i2 <子> 1

表达式作为DAG的遍历

这里有一个图遍历问题。首先,将问题视为一棵树。树的根是 i1 0 i2 0 。此节点及其下面的每个节点都有两个子节点。 i1 x i2 y 的两个孩子如下:一个孩子是 i1 x + 1 i2 y ,另一个孩子是 i1 x i2 y + 1 。基于您的第二个假设,我们知道 i1 x i2 y 小于其两个孩子。

(事实上,正如Ryan在评论中提到的,这是一个有向无环图或DAG。一些“父母”与其他“父”节点共享“孩子”。)

现在,我们需要保留前沿 - 可以接下来返回的节点集合。返回节点后,我们将其子节点添加到边界。要选择要访问的下一个节点(并从新迭代器返回),我们将比较边界中所有节点的值。我们采用具有最小值的节点并返回它。然后,我们再次将其两个子节点添加到边界。如果孩子已经在边境(作为其他父母的子女添加),请忽略它。

存储边界

因为您主要对节点的值感兴趣,所以存储按值索引的这些节点是有意义的。因此,使用dict可能符合您的利益。此dict中的键应该是节点的值。此dict中的值应该是包含单个节点的列表。因为节点中唯一的标识信息是一对操作数,所以可以将各个节点存储为两元组的操作数。

在实践中,经过几次迭代后,您的边界可能如下所示:

>>> frontier
{1: [(2, 3), (2, 4)], 2: [(3, 5), (5, 4)], 3: [(1, 6)], 4: [(6, 3)]}

其他实施说明

因为迭代器不支持随机访问,所以您需要挂起前两个迭代器生成的值,直到不再需要它们为止。 如果边界中的任何值引用了值,您就会知道仍然需要一个值。一旦边界参考值中的所有节点稍后/大于您存储的值,您就会知道不再需要值。例如,当您的边界中的节点仅引用 i1 21 时,不再需要 i1 20 i1 25 i1 33 ,...

如Ryan所述,每个迭代器的每个值都将被无限次使用。因此,每个产生的价值都需要保存。

不实用

不幸的是,为了确保元素仅以递增的顺序返回,边界将无限制地增长。您的memoized值可能也会占用大量空间也会不受限制地增长。这可能是你可以通过使问题不那么普遍来解决的问题,但这应该是一个很好的起点。

答案 4 :(得分:0)

使用generators,它们只是作为产生结果的函数编写的迭代器。在这种情况下,您可以编写iter1iter2的生成器以及另一个生成器来包装它们并生成结果(或者使用它们进行计算,或者结果的历史记录)。

从我对这个问题的阅读中你想要这样的东西,它将使用所述操作计算第一个迭代器的每个元素,并使用所述操作,你还要说明你想要一些方法汇总“iter1”,“iter2”和“op”在一个迭代中,它本身产生单调增加输出的值。我建议发电机为这种问题提供简单的解决方案。

import itertools

def prime_gen():
    D, q = {}, 2
    while True:
        if q not in D:
            yield q
            D[q * q] = [q]
        else:
            for p in D[q]:
                D.setdefault(p + q, []).append(p)
            del D[q]
        q += 1

def infinite_gen(op, iter1, iter2):
    while True:
        yield op(iter1.next(), iter2.next())

>>> gen = infinite_gen(operator.mul, prime_gen(), itertools.count())

>>> gen.next()
<<< 0

>>> gen.next()
<<< 3

>>> gen.next()
<<< 10

生成器提供了很大的灵活性,因此编写iter1iter2作为生成器应该相当容易,这些生成器按照您想要的顺序返回所需的值。您还可以考虑使用coroutines,它允许您将值发送到生成器。

答案 5 :(得分:0)

在其他答案中的讨论观察到,无论算法是什么,都可能存在无限存储空间,因为每个新a[n]必须保持每个b[n]可用。如果你删除了输入是两个迭代器的限制,而只是要求它们是序列(可索引或只是可以重复重新生成的东西),那么我相信你所有的状态突然崩溃到一个数字:您返回的最后一个值。知道了最后的结果值,您可以搜索输出空间以查找下一个结果值。 (如果要正确发出重复项,则可能还需要跟踪返回结果的次数)

使用一对序列,您有一个简单的递归关系:

result(n) = f(seq1, seq1, result(n-1))

其中f(seq1, seq1, p)在输出空间q中搜索q > p的最小值。实际上,您可能会使序列记忆功能并选择您的搜索算法,以避免破坏已记忆项目池。