在编程竞赛中,许多任务都会出现以下模式:
给定数字A和B很大(可能是20个十进制数字或更多),确定具有A≤X≤B的具有特定属性P的整数X
SPOJ has lots of tasks like these用于练习。
有趣属性的示例包括:
我知道如果我们将 f(Y)定义为这样的整数X≤Y,那么我们问题的答案是 f(B) - f(A - 1)。减少的问题是如何有效地计算函数 f 。在某些情况下,我们可以利用某些数学属性来得出一个公式,但通常属性更复杂,我们在比赛中没有足够的时间。
在很多情况下是否有更通用的方法?它是否也可以用于枚举具有给定属性的数字或计算它们的某些聚合?
这种变化是找到具有给定属性的第k个数字,当然可以通过使用二进制搜索和计数函数来解决。
答案 0 :(得分:67)
事实上,这种模式有一种方法可以经常使用。它也可用于枚举具有给定属性的所有X,前提是它们的数量相当小。您甚至可以使用它来聚合具有给定属性的所有X上的某个关联运算符,例如查找它们的总和。
为了理解一般的想法,让我们尝试用X和Y的decimal representations来表达条件X≤Y。
假设我们有X = x 1 x 2 ... x n - 1 x n 和Y = y 1 y 2 ... y n - 1 y n < / sub> ,其中 x i 和 y i 是X和Y的十进制数字如果数字的长度不同,我们总是可以在较短的数字前面添加零数字。
让我们将leftmost_lo
定义为最小的 i ,其中 x i &lt; ÿ<子> I 子> 的。如果没有这样的 i ,我们将leftmost_lo
定义为 n + 1 。
类似地,我们将leftmost_hi
定义为最小的 i ,其中 x i &gt; y i ,否则 n + 1 。
现在X≤Y如果且恰好是leftmost_lo <= leftmost_hi
则为真。通过这种观察,可以对问题应用dynamic programming方法,一个接一个地“设置”X的数字。我将用您的示例问题证明这一点:
计算整数X的数字f(Y),其属性X≤Y,X的数字总和为60
根据上面的定义,设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(n 4 )。事实上,我们可以巧妙地优化这个想法,使其成为 O(n)。这是留给读者的练习(提示:您可以将leftmost_lo
和leftmost_hi
中存储的信息压缩成一个位,并且可以修剪sum_so_far > 60
)。该解决方案可以在本文末尾找到。
如果仔细观察,sum_so_far
这里只是一个计算X数字序列值的任意函数的例子。
它可以是任何函数,可以逐位计算并输出足够小的结果。它可能是数字的乘积,是满足特定属性或许多其他东西的数字集的位掩码。
它也可以只是一个返回1或0的函数,具体取决于数字是否只包含数字4和7,这可以简单地解决第二个例子。我们必须要小心一点,因为我们 允许在开头有前导零,所以我们需要在递归函数调用中携带一个额外的位来告诉我们是否仍然允许使用零作为一个数字。
计算整数X的数字f(Y),其属性X≤Y,X是回文
这个稍微强硬一点。我们需要小心前导零:回文数的镜像点取决于我们有多少前导零,所以我们需要跟踪前导零的数量。
有一个技巧可以简化它:如果我们可以计算 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: O(n)“数字和60”示例的解决方案:
在此应用程序中,我们将数字从左到右放置。由于我们只对leftmost_lo < leftmost_hi
是否成立感兴趣,因此我们添加一个新参数lo
。 lo
为{i} leftmost_lo < i
,否则为false。如果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
方法稍微简单一些,但也不那么明确。对于像回文问题这样的更复杂的情况,它也不会立即起作用(尽管它也可以在那里使用)。