我试图计算范围A到B之间的整数,其数字和S(假设S = 60)。
A和B的范围为1到10 ^ 18。
让X为数字,Upto Y我们必须计算整数。
X = x1 x2 ... xn - 1 xn和Y = y1 y2 ... yn - 1 yn,其中xi和yi是X和Y的十进制数字。
leftmost_lo是最小的i,xi<义。如果没有这样的i,我们将leftmost_lo定义为n + 1。类似地,我们将leftmost_hi定义为xi> 1的最小i。 yi,否则为n + 1。
函数计数返回整数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
Compute the number f(Y) of integers X with the property X ≤ Y and X has the digit sum 60
现在我们有f(Y)= count(1,0,n + 1,n + 1),我们已经解决了问题。运行时
对于这个特定的实现,是O(n ^ 4)。
我理解这个概念来自这个链接。 How to count integers between large A and B with a certain property?
但无法理解如何优化这一点。
现在,我如何针对此特定问题优化此O(n)解决方案。
任何人都可以帮助我。
答案 0 :(得分:2)
首先,你可以注意到,如果你有一个函数F,它返回的数量&lt; = A,数字和S,那么A和B之间的数字和S的整数是F(B)-F (A-1)。
然后定义一些符号:
然后你有这些关系,一次做一个数字,并注意到可能性是你匹配A的第一个数字,或者你在那里放一个较低的数字(然后对于递归的情况,你可以用一个数字替换A所有9s。)
F(S, A) = 1 if S = 0
F(S, A) = 0 if S < 0 or A = 0
otherwise F(S, A) =
F(S-A[0], A[1:])
+ F(S-0, n(A[1:])) + F(S-1, n(A[1:])) + ... + F(S-A[0]-1, n(A[1:]))
这为您提供了这段代码(使用缓存来避免多次计算相同的事情):
def count1(S, digits, nines, k, cache):
if S <= 0 or k == len(digits): return S==0
key = (S, nines, k)
if key not in cache:
dk = 9 if nines else digits[k]
cache[key] = sum(count1(S-i, digits, nines or i<dk, k+1, cache)
for i in xrange(dk+1))
return cache[key]
def count(S, A):
return count1(S, map(int, str(A)), False, 0, {})
def count_between(S, A, B):
return count(S, B) - count(S, A-1)
print count_between(88, 1, 10**10)
缓存最终大小为S * 2 * len(str(A))并且每个事物都计算一次,这给你复杂性:O(S * log_10(A))。
答案 1 :(得分:1)
对于A = 1且B = 10 ^ 18,生成S的所有整数分区,其中少于19个部分且每个部分小于10.答案是每个分区的不同排列数的总和数字与(18 - number_of_parts)零结合。
对于其他A和B,在边缘处涉及的数学略多一些:)
对于范围1到任意B,我们可以使用类似的过程,但有更多的枚举:
让我们说B有数字b1 b2 ... bn - 1 bn。我们递减b1并枚举数字S - (b1 - 1)的分区(少于n个部分,每个部分低于10),以及与(n - 1 - number_of_parts)零结合时它们的不同排列的基数。我们重复这个过程直到并包括b1 = 0(这里最大的零件数和前导零将减1)。然后我们对b2重复类似的过程,但这次S首先减少b1。等等,总结结果。
对于任意A和B,我们从f(B)中减去f(A)。
JavaScript代码:
function choose(n,k){
if (k == 0 || n == k){
return 1;
}
var product = n;
for (var i=2; i<=k; i++){
product *= (n + 1 - i) / i
}
return product;
}
function digits(n){
var ds = [];
while (n){
ds.push(n % 10);
n = Math.floor(n/10);
}
return ds.reverse()
}
function ps(n,maxParts){
if (maxParts <= 0){
return 0;
}
var result = 0;
for (var i=9; i>=Math.floor(n/maxParts); i--){
var r = [0,0,0,0,0,0,0,0,0,0,1]; // 11th place is number of parts
r[i]++;
result += _ps(n-i,r,i,1,maxParts);
}
return result;
}
function _ps(n,r,i,c,maxParts){
if (n==0){
return numPs(r,maxParts);
} else if (c==maxParts || n<0){
return 0;
} else{
var result = 0;
for (var j=i; j>0; j--){
var r0 = r.slice();
r0[j]++;
r0[10]++;
result += _ps(n-j,r0,j,c+1,maxParts);
}
return result;
}
}
function numPs(partition,n){
var l = choose(n,n - partition[10]);
n = partition[10];
for (var i=0; i<10;i++){
if (partition[i] != 0){
l *= choose(n,partition[i]);
n -= partition[i];
}
}
return l;
}
function f(n,s){
var ds = digits(n),
n = ds.length,
answer = 0;
for (var i=0; i<n - 1; i++){
if (ds[i] != 0){
var d = ds[i] - 1;
while (d >= 0){
answer += ps(s - d,n - i - 1);
d--;
}
s -= ds[i];
}
}
if (s <= ds[n - 1]){
answer++;
}
return answer;
}
输出:
console.log(f(1,1));
1
console.log(f(1000,3));
10
console.log(f(1001,3));
10
console.log(f(1002,3));
11
console.log(f(1003,3));
11
console.log(f(1010,3));
11
答案 2 :(得分:0)
编辑哦亲爱的!就在我承认我的答案错过了重点之后,它就被接受了。我已经离开了原来的答案,并解释了我的算法背后的想法,以及它在折叠后如何不如原始解决方案。
这个特殊问题应该被视为“所有18位数的整数”而不是“1到10 ^ 18之间的所有整数”。 (对于数字总和,少于18位的数字可以被视为带有前导零的18位数字。)
然后你可以使用从下往上传播的算法,就像Erathostenes的筛子遍布所有非素数一样。
从数字计数dig
开始,数字1到9表示0,即全零。 (零的数量可以计算为18 - sum(dig)
。然后你可以像这样递归:
recurse(dig[], pos) {
if (digitsum(dig) > 60) return;
if (digitsum(dig) == 60) {
count += poss(dig)
} else {
if (pos < 9) recurse(dig, pos + 1);
if (sum(dig) < 18) {
dig[pos]++;
recurse(dig, pos);
dig[pos]--;
}
}
}
这样您就可以处理所有数字计数组合并在超过60时返回。当您准确命中60时,您必须计算与该数字计数对应的可能数字的数量:
poss(dig) = 18! / prod(dig[i]!)
阶乘prod(dig[i]!)
的乘积必须包含零阶乘。 (当然,0! == 1
。)
如果你到目前为止跟踪总和并预先计算阶乘,那么这种方法应该足够快。如果你想计算50到5,000,000,000之间数字总和为60的所有数字,它就不起作用了。
附录您链接到的框架可以处理从A到B的任何范围。这里,让我们关注范围从0到10 ^ n,即n位数字,其中数字较少数字被认为具有前导零。
我的算法的想法不是枚举所有数字,而是考虑数字的计数。例如,如果我的号码有5倍数字9和3倍数字5,则数字总和为60.现在我们必须找到满足该条件的18位数。 590,050,005,090,900,099就是这样一个数字,但这个数字的所有唯一排列也是有效的。该数字具有18 - (5 + 3)= 10个零,因此该组合具有
N(5x9, 3x5) = 18! / (5! * 3! * 10!)
排列。
算法必须枚举所有排列。它跟踪数组中的枚举dig
:
^ count
|
2x ... ... ... ... ... ... ... ... ...
1x ... ... ... ... ... ... ... ... ...
0x ... ... ... ... ... ... ... ... ...
---------------------------------------------> pos
1 2 3 4 5 6 7 8 9
以上案例将是
dig == [0, 0, 0, 0, 3, 0, 0, 0, 5]
为实现这一目标,它以锯齿形图案传播。当前数字称为pos
。它可以通过将当前数字的计数递增1来垂直移动,也可以通过考虑下一个数字来水平移动。如果数字总和达到或超过S或者如果pos超过9,则递归停止。每当我们达到S时,我们进行如上所示的置换计算。
因为数组是通过引用传递的,并且实际上是相同的数组,所以我们必须在递增后递减它:我们必须在追溯后清理它。
此算法有效,并且会找到所有18位数字的数字的答案,其数字总和为60秒。
但它没有扩展,因为它的运行时间呈指数级增长。还因为你可以计算18的阶乘! 64位整数,但20后!你需要大整数算术。 (然而,通过简化分数N! / prod(dig[i]!)
,巧妙的实现将能够进一步发展。)
现在考虑您发布的代码。我删除了计算范围的所有内容。简陋的版本是:
ds_count(i, sum)
{
if (sum > 60) return 0;
if (i == 18) {
if (sum == 60) return 1;
return 0;
}
result = 0;
for (d = 0; d < 10; d++) {
result += ds_count(i + 1, sum + d);
}
return result;
}
这列举了所有18位数值。当总和超过60时,它会停止,但这就是它。这并不比蛮力解决方案好。
但是这个解决方案有助于记忆。它经常会被调用相同的值,并且很容易理解为什么。例如,将从ds_count(2, 5)
,05...
,14...
,23...
,32...
和41...
调用来电50...
。 (这让我想起了Settlers of Catan的棋盘游戏中不同大小的数字筹码,它占了两个骰子的总和。)
如果您可以确定其中一个值,则可以保存它并有效地将5个调用切换为16个数字的尾部。所以:
ds_count(i, sum)
{
if (sum > 60) return 0;
if (i == 18) {
if (sum == 60) return 1;
return 0;
}
if (defined(memo[i, sum])) return memo[i, sum];
result = 0;
for (d = 0; d < 10; d++) {
result += ds_count(i + 1, sum + d);
}
memo[i, sum] = result;
return result;
}
这非常快,并且没有像阶乘解决方案那样的硬性限制。它也更容易被推动,因为它是一种递归的枚举。
值得注意的是,我的解决方案不适合记忆。 (除了记住阶乘,但这不是限制因素。)之字形计数集生成的要点是只进行唯一的递归调用。还有一个状态,即数字集,这使得记忆更难。