这个很有趣!这个怎么样:
def nonconsecutive_combinations(r, n):
# first combination, startng at 1, step-size 2
combination = range(1, r*2, 2)
# as long as all items are less than or equal to n
while combination[r-1] <= n:
yield tuple(combination)
p = r-1 # pointer to the element we will increment
a = 1 # number that will be added to the last element
# find the rightmost element we can increment without
# making the last element bigger than n
while p > 0 and combination[p] + a > n:
p -= 1
a += 2
# increment the item and
# fill the tail of the list with increments of two
combination[p:] = range(combination[p]+1, combination[p] + 2*(r-p), 2)
Each next()
调用应该有一个 O(r) ..
我在思考如何将其转换为自然数时得到了这个想法,但花了相当长的时间才得到正确的细节。
> list(nonconsecutive_combinations(2, 4))
[(1, 3), (1, 4), (2, 4)]
> list(nonconsecutive_combinations(3, 6))
[(1, 3, 5), (1, 3, 6), (1, 4, 6), (2, 4, 6)]
让我尝试解释一下这是如何工作的。
元组的条件c
with r
作为结果集一部分的元素:
- 元组中的任何元素至少与前一个元素加 2 一样大。
(
c[x] >= c[x-1] + 2
)
- 所有元素都小于或等于
n
。
因为 1. 说这一点就足够了最后一个元素小于
或等于n
. (c[r-1] <= n
)
最小的元组may成为结果集的一部分是(1, 3, 5, ..., 2*r-1)
。
当我说一个元组比另一个元组“小”时,我假设的是字典顺序。
正如 Blckknght 所指出的,即使是最小的可能元组也可能太大而无法满足条件 2。
上面的函数包含两个 while 循环:
-
外循环遍历结果并假设它们按字典顺序出现并满足条件一。一旦有问题的元组违反了条件二,我们就知道我们已经用尽了结果集并且完成了:
combination = range(1, r*2, 2)
while combination[r-1] <= n:
第一行根据条件一用第一个可能的结果初始化结果元组。第二行直接转化为条件二。
-
内循环查找满足条件一的下一个可能的元组。
yield tuple(combination)
自从while
条件(2)为真,并且我们确保结果满足条件一,我们可以产生当前的结果元组。
接下来,为了找到按字典顺序排列的下一个元组,我们将向最后一个元素添加“1”。
# we don't actually do this:
combination[r-1] += 1
然而,这可能会过早地破坏条件 2。所以,if该操作会破坏条件 2,我们增加前面的元素并相应地调整最后一个元素。这有点像计算以 10 为基数的整数:“如果最后一位数字大于 9,则递增前一位数字并使最后一位数字为 0”。但我们不是添加零,而是填充元组以使条件 1 为真。
# if above does not work
combination[r-2] += 1
combination[r-1] = combination[r-2] + 2
问题是,第二行可能会再次违反条件二。所以我们实际做的是,我们跟踪最后一个元素,这就是对a
。我们还使用p
变量来引用我们正在查看的当前元素的索引。
p = r-1
a = 1
while p > 0 and combination[p] + a > n:
p -= 1
a += 2
我们从右到左(p = r-1,p -= 1)迭代结果元组的项目。
最初,我们想要向最后一项添加 1 (a = 1),但是当单步执行元组时,我们实际上想要将最后一项替换为前一项的值加上2*x
, where x
是两个项目之间的距离。 (a += 2, 组合[p] + a)
最后,我们找到了要增加的项,并用从增加的项开始的序列填充元组的其余部分,步长为 2:
combination[p:] = range(combination[p]+1, combination[p] + 2*(r-p), 2)
就是这样。当我第一次想到它时,它看起来很简单,但是整个函数中的所有算术都为差一错误提供了一个很好的地方,并且描述它比应该的更难。当我添加内部循环时我应该知道我遇到了麻烦:)
论性能..
不幸的是,充满算术的 while 循环并不是用 Python 编写的最有效的东西。其他解决方案接受这一现实,并使用列表理解或过滤将繁重的工作推入 Python 运行时。这在我看来是正确的做法.
另一方面,我非常确定,如果这是 C,我的解决方案会比大多数解决方案执行得更好。内部 while 循环是 O(log r) ,它会就地改变结果,并且相同的 O(log r )。它不消耗额外的堆栈帧,并且除了结果和两个变量之外不消耗任何内存。但显然这不是 C,所以这些都不重要。