如何计算具有特定属性的大 A 和 B 之间的整数?

Posted

技术标签:

【中文标题】如何计算具有特定属性的大 A 和 B 之间的整数?【英文标题】:How to count integers between large A and B with a certain property? 【发布时间】:2014-04-19 02:38:39 【问题描述】:

在编程竞赛中,很多任务都会出现以下模式:

给定巨大的数字 A 和 B(可能是 20 个十进制数字或更多),确定具有特定属性 P 且 A ≤ X ≤ B 的整数 X 的数量

SPOJ has lots of tasks like these 练习。

有趣的属性示例包括:

"X的位数和为60" “X 仅由数字 4 和 7 组成” “X 是回文”,表示 X 的十进制表示等于它的倒数(例如,X = 1234321)

我知道如果我们定义 f(Y) 为这样的整数 X ≤ Y 的个数,那么我们问题的答案是 f(B) - f(A - 1)。减少的问题是如何有效地计算函数f。在某些情况下,我们可以利用某些数学性质来提出一个公式,但通常这些性质更复杂,我们在比赛中没有足够的时间。

是否有更通用的方法适用于很多情况?它还可以用于枚举具有给定属性的数字或计算它们的聚合吗?

这种方法的一种变体是找到具有给定属性的第 k 个数,这当然可以通过使用二进制搜索和计数函数来解决。

【问题讨论】:

@JuanLopes:是的。这个想法是记录您的想法和结果,以便其他人可以从中受益,就像博客一样 :) 我也在努力让竞争激烈的编程社区对 Stack Overflow 更加感兴趣,所以也许这将有助于树立一个榜样像这样的问题实际上是受欢迎的,而且是热门话题(显然我希望其他人也会觉得这很有趣) @JuanLopes:我当然也愿意接受其他答案 @arunmoezhi 你的意思是,自我回答的问题?因为这些都发生在 SO 中(“提问”对话框中甚至还有一个复选框,可让您在发布问题之前编写答案)。还是您的意思是竞争性编程?我想我现在已经为此发明了一个标签 @Charles:或者换一种说法,这几乎是一个竞争性编程特定的问题,因为它描述了一个仅在该设置中有用而不在“现实世界”中有用的技巧 @RobNeuhaus:公平地说,80% 的编程竞赛相关的也很糟糕。这些问题使它们变得有趣。我认为如果有更多来自竞争性编程社区的人开始在这里提出高质量的问题,而不仅仅是“为什么我的 SPOJ FOO 代码会给出 WA”,这真的会有所帮助 【参考方案1】:

确实,这种模式有一种方法,结果证明它很有效。它还可以用于枚举具有给定属性的所有 X,前提是它们的数量相当小。您甚至可以使用它来聚合具有给定属性的所有 X 上的一些关联运算符,例如找到它们的总和。

为了理解大意,让我们试着用 X 和 Y 的decimal representations 来表述条件 X ≤ Y。

假设我们有 X = x1 x2 ... xn - 1 xn sub> 和 Y = y1 y2 ... yn - 1 yn,其中xiyi是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_loleftmost_hi 中的信息压缩成一个位,如果sum_so_far &gt; 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 &lt; leftmost_hi 是否成立,让我们添加一个新参数lolo 如果leftmost_lo &lt; i 为真,否则为假。如果lo 为真,我们可以使用任何数字作为i 的位置。如果它是假的,我们只能使用数字 0 到 Y[i],因为任何更大的数字都会导致leftmost_hi = i &lt; 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 方法更不明确。它也不能立即用于更复杂的场景,例如回文问题(尽管它也可以在那里使用)。

【讨论】:

Compute the number f(Y) of integers X with the property X ≤ Y and X has the digit sum 60 的算法看起来复杂度为 O(10^n)。我看不出它是O(n ^ 4)。递归级别可以上升到 n 并且在每个递归中都有一个从 0 到 9 的循环。我在这里遗漏了什么吗? @Rushil 正如我所说,您需要添加记忆功能以使其快速。参数的赋值只有 O(n^4) 次,所以函数体最多执行 O(n^4) 次,每次除了递归调用(0 到 9 的循环可以被认为是恒定的时间)。或者换句话说:有 10^n 个分支,但其中只有 O(n^4) 个实际上是不同的子问题。由于我们不会多次求解一个分支,因此递归树中只有 O(n^4) 个节点 因此,O(10^n) 复杂度可以通过记忆化降低到 O(n^4)。你如何使它 O(n^2) 或更好? @NiklasB。你不是必须记住 i 和 sum_so_far 所以它变成 O(n*n) 吗?如果我错了,请纠正我 @harish.venkat 是的,但在这种情况下 sum_so_far 以常数 60 为界

以上是关于如何计算具有特定属性的大 A 和 B 之间的整数?的主要内容,如果未能解决你的问题,请参考以下文章

c语言编程。输入两个正整数,分别赋给变量a,b,计算ab之间所有整数之和。

11 计算a 和b 之间整数的平方和,模块化思想,输入检测的重要性

11 计算a 和b 之间整数的平方和,模块化思想,输入检测的重要性

具有特定属性的Mysql计数行

HIVE/Impala 查询:计算满足特定条件的行之间的行数

计算具有特定条件的值