让我们采取暴力解决问题的方法并打印结果*:
############################# 2^^1 -1 == 1
-#-#-#-#-#-#-#-#-#-#-#-#-#-# 2^^2 -1 == 3
--#--#--#--#--#--#--#--#--# 2^^3 -1 == 7
---#---#---#---#---#---#
----#----#----#----#----# 2^^5 -1 == 31
-----#-----#-----#-----# 2^^6 -1 == 63
------#------#------#
-------#-------#
--------#--------#
---------#---------# 2^^10 -1 == 1023
----------#
-----------#
------------#
-------------#
--------------# 2^^15 -1 == 32767
... ...
一个#号表示你的条件被满足的情况。一个好的模式出现了:每一个数字都可以被1整除,其他的数字都可以被3整除,每第三个数字都可以被7整除,以此类推。每个 i 个数字都可以被2^^i - 1
整除。
有了这个见解,我们可以将您的函数编码为:
int f(int n)
{
int sum = 0;
int i;
for (i = 1; i <= n; i++) sum += (n - i) / i;
return sum;
}
我们可以用 n / i - 1
替换 (n - i) / i
,并将共同的减数 -1
移到返回值中:
int g(int n)
{
int sum = 0;
int i;
for (i = 1; i <= n; i++) sum += n / i;
return sum - n;
}
现在让我们来看一下求和式∑(1, n: n / i)
。例如:
∑(i = 1, 9: 9 / i) = 9 + 4 + 3 + 2 + 1 + 1 + 1 + 1 + 1
我们可以从右往左看,并计算每个加数出现的次数,得到相同的总和。
∑(i = 1, 9: 9 / i) = 5*1 + 1*2 + 1*3 + 1*4 + 1*9
我们可以轻松地获取这个表示:
∑(i = 1, n: n / i) = ∑(1, n: i * (n / i - n / (i + 1))
这其实只是另一种写求和式的方式;你可以通过重新分组,使它们共享相同的分母来看出这一点:
∑(i = 1, N: i * (n / i - n / (i + 1))
= n + ∑(i = 1, n: ((i + 1) - i) * n / (i + 1))
= n + ∑(i = 1, n: n / (i + 1)) - (N + 1) * (n / (N + 1))
= n + ∑(i = 2, n + 1: n / i) - c
= ∑(i = 1, n: n / i) - c
附加项
c = (N + 1) * (n / (N + 1))
是一个校正项,因为只使用了
i = n + 1
的一半。当对整个范围求和时,
n / (n + 1)
为零并消失。但是,在仅对数组的一部分进行求和时,它不会消失,稍后我们将看到。
如果我们在
s = sqrt(n)
处将总和分成头和尾,我们得到:
∑(i = 1, n: n / i) = ∑(i = 1, s: n / i) + ∑(s + 1, n: n / i)
让我们用原始方式代表头部和“计算加数”的方式代表尾部,例如:
∑(i = 1, 9: 9 / i) = (9 + 4 + 3) + (5*1 + 1*2)
对于任意n
:
∑(i = 1, n: n / i)
= ∑(i = 1, s: n / i) + ∑(1, s - 1: i * (n / i - n / (i + 1))
= ∑(i = 1, s: n / i) + ∑(1, s: n / i) - s * (n / s)
所有的除法都是整数除法(这就是为什么有时需要括号),且 n / s == s
,所以:
∑(1, n: n / i) = ∑(i = 1, s: n / i) + ∑(i = 1, s: n / i) - s * (n / s)
= 2 * ∑(i = 1, s: n / i) - s²
这会生成您最初的函数:
int h(int n)
{
int nrt = sqrt(n);
int sum = 0;
int i;
for(i = 1; i <= nrt; i++) sum += n/i;
return 2 * sum - nrt * nrt - n;
}
其中,∑(1, n: n / i)
在g
中被替换为2 * ∑(i = 1, s: n / i) - s²
。
*) 我在这里借用了D语言的幂运算符 ^^
,以免混淆那些将^
视为异或的老C语言爱好者。
**) 我不知道这个模式为什么存在。可能有一个合理的解释,但现在我相信我的模式匹配技能。盲目地。 编辑@nevets的答案中有关于这个模式的解释。
2 ^ j
是一个异或操作,它切换了2位; -1从中减去。 RHS也有类似的操作。%
运算符在LHS除以RHS时给出余数。但是,看过代码后,我感到困惑-(2 ^ j-1)%(2 ^ i-1)== 0
的问题与所显示的代码有什么关联?那是计算相同结果的另一种方式吗? - Jonathan Lefflergcc -Wall -g
)。考虑scanf
的返回值(它是成功扫描项目的数量)。请使用调试器(gdb
)。 - Basile Starynkevitch