确实,这种模式有一种方法,结果证明它很有效。它还可以用于枚举具有给定属性的所有 X,前提是它们的数量相当小。您甚至可以使用它来聚合具有给定属性的所有 X 上的一些关联运算符,例如找到它们的总和。
为了理解大意,让我们试着用 X 和 Y 的decimal representations 来表述条件 X ≤ Y。
假设我们有 X = x1 x2 ... xn - 1 xn sub> 和 Y = y1 y2 ... yn - 1 yn,其中xi和yi是X和Y的十进制数字. 如果数字有不同的长度,我们总是可以在较短的数字前面加零。
让我们将leftmost_lo 定义为具有xii 的最小i。如果没有这样的i,我们将leftmost_lo 定义为n + 1。
类似地,我们将leftmost_hi 定义为具有 xi > yi 或 的最小 i n + 1 否则。
现在 X ≤ Y 为真,如果且恰好如果 leftmost_lo <= leftmost_hi。通过这种观察,可以对问题应用dynamic programming 方法,即一个接一个地“设置” X 的数字。我将用你的示例问题来证明这一点:
计算具有属性 X ≤ Y 且 X 的位数和为 60 的整数 X 的个数 f(Y)
根据上面的定义,设n 是Y 的位数,y[i] 是Y 的第i 个十进制数。下面的递归算法解决了这个问题:
count(i, sum_so_far, leftmost_lo, leftmost_hi):
if i == n + 1:
# base case of the recursion, we have recursed beyond the last digit
# now we check whether the number X we built is a valid solution
if sum_so_far == 60 and leftmost_lo <= leftmost_hi:
return 1
else:
return 0
result = 0
# we need to decide which digit to use for x[i]
for d := 0 to 9
leftmost_lo' = leftmost_lo
leftmost_hi' = leftmost_hi
if d < y[i] and i < leftmost_lo': leftmost_lo' = i
if d > y[i] and i < leftmost_hi': leftmost_hi' = i
result += count(i + 1, sum_so_far + d, leftmost_lo', leftmost_hi')
return result
现在我们有了f(Y) = count(1, 0, n + 1, n + 1),我们已经解决了这个问题。我们可以在函数中添加memoization 以使其更快。对于这个特定的实现,运行时间是 O(n4)。事实上,我们可以巧妙地优化这个想法,使其O(n)。这留给读者作为练习(提示:您可以将存储在leftmost_lo 和leftmost_hi 中的信息压缩成一个位,如果sum_so_far > 60 则可以修剪)。解决方法见文末。
如果你仔细观察,sum_so_far 这里只是一个任意函数从 X 的数字序列中计算一个值的例子。
它可以是 any 函数,可以逐位计算并输出足够小的结果。它可能是数字的乘积、满足特定属性的数字集的位掩码或许多其他事物。
它也可能只是一个返回 1 或 0 的函数,具体取决于数字是否仅由数字 4 和 7 组成,这可以轻松解决第二个示例。我们在这里要小心一点,因为我们被允许在开头有前导零,所以我们需要通过递归函数调用携带一个额外的位,告诉我们是否仍然允许使用零作为一个数字。
计算具有性质 X ≤ Y 且 X 是回文的整数 X 的个数 f(Y)
这个稍微难一些。我们需要注意前导零:回文数的镜像点取决于我们有多少前导零,因此我们需要跟踪前导零的数量。
不过有一个技巧可以稍微简化一下:如果我们可以计算 f(Y) 并附加所有数字 X 必须与 Y 具有相同数字计数的附加限制,那么我们可以通过迭代所有可能的数字计数并将结果相加来解决原始问题。
所以我们可以假设我们根本没有前导零:
count(i, leftmost_lo, leftmost_hi):
if i == ceil(n/2) + 1: # we stop after we have placed one half of the number
if leftmost_lo <= leftmost_hi:
return 1
else:
return 0
result = 0
start = (i == 1) ? 1 : 0 # no leading zero, remember?
for d := start to 9
leftmost_lo' = leftmost_lo
leftmost_hi' = leftmost_hi
# digit n - i + 1 is the mirrored place of index i, so we place both at
# the same time here
if d < y[i] and i < leftmost_lo': leftmost_lo' = i
if d < y[n-i+1] and n-i+1 < leftmost_lo': leftmost_lo' = n-i+1
if d > y[i] and i < leftmost_hi': leftmost_hi' = i
if d > y[n-i+1] and n-i+1 < leftmost_hi': leftmost_hi' = n-i+1
result += count(i + 1, leftmost_lo', leftmost_hi')
return result
结果将再次为f(Y) = count(1, n + 1, n + 1)。
更新:如果我们不仅要计算数字,而且可能要枚举它们或从中计算一些不公开组结构的聚合函数,我们需要强制执行下限X 在递归期间也是如此。这增加了一些参数。
更新 2:“数字总和 60”示例的 O(n) 解决方案:
在这个应用程序中,我们从左到右放置数字。由于我们只关心leftmost_lo < leftmost_hi 是否成立,让我们添加一个新参数lo。 lo 如果leftmost_lo < i 为真,否则为假。如果lo 为真,我们可以使用任何数字作为i 的位置。如果它是假的,我们只能使用数字 0 到 Y[i],因为任何更大的数字都会导致leftmost_hi = i < leftmost_lo,因此无法解决。代码:
def f(i, sum_so_far, lo):
if i == n + 1: return sum_so_far == 60
if sum_so_far > 60: return 0
res = 0
for d := 0 to (lo ? 9 : y[i]):
res += f(i + 1, sum + d, lo || d < y[i])
return res
可以说,这种看待它的方式稍微简单一些,但也比leftmost_lo/leftmost_hi 方法更不明确。它也不能立即用于更复杂的场景,例如回文问题(尽管它也可以在那里使用)。