因此,我们可以使用筛子计算O(NlogN)算法中每个数字的除数从1到N:
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = i; j <= n; j += i) {
cnt[j]++; //// here cnt[x] means count of divisors of x
}
}
有没有办法将它减少到O(N)? 提前谢谢。
答案 0 :(得分:2)
这个怎么样?从素数2开始,并保留一个元组列表(k, d_k)
,其中d_k
是k
的除数,从(1,1)
开始:
for each prime, p (ascending and lower than or equal to n / 2):
for each tuple (k, d_k) in the list:
if k * p > n:
remove the tuple from the list
continue
power = 1
while p * k <= n:
add the tuple to the list if k * p^power <= n / p
k = k * p
output (k, (power + 1) * d_k)
power = power + 1
the next number the output has skipped is the next prime
(since clearly all numbers up to the next prime are
either smaller primes or composites of smaller primes)
上面的方法也会生成素数,依靠O(n)
内存来继续寻找下一个素数。拥有更高效,独立的素数流可以允许我们避免将任何元组(k, d_k)
附加到列表中k * next_prime > n
,以及释放保存输出大于n / next_prime
的所有内存。
Python code
答案 1 :(得分:2)
这是对@גלעדברקן的解决方案的简单优化。而不是使用集合,使用数组。这大约是设定版本的10倍。
n = 100
answer = [None for i in range(0, n+1)]
answer[1] = 1
small_factors = [1]
p = 1
while (p < n):
p = p + 1
if answer[p] is None:
print("\n\nPrime: " + str(p))
limit = n / p
new_small_factors = []
for i in small_factors:
j = i
while j <= limit:
new_small_factors.append(j)
answer[j * p] = answer[j] + answer[i]
j = j * p
small_factors = new_small_factors
print("\n\nAnswer: " + str([(k,d) for k,d in enumerate(answer)]))
值得注意的是,这也是用于枚举素数的O(n)算法。但是,如果使用从尺寸log(n)/2
以下的所有素数生成的轮子,它可以及时创建一个主要列表O(n/log(log(n)))
。
答案 2 :(得分:0)
考虑这些计数的总和,总和(对于i = 1,n的phi(i))。该总和 O(N log N),因此任何 O(N)解决方案都必须绕过个别计数。
这表明任何改进都需要依赖于先前的结果(动态编程)。我们已经知道phi(i)是每个主要学位加上一个的产物。例如,12 = 2 ^ 2 * 3 ^ 1。度数分别为2和1。 (2 + 1)*(1 + 1)= 6. 12有6个除数:1,2,3,4,6,12。
这“减少”了一个问题,即你是否可以利用先验知识来获得 O(1)直接计算除数的方法,而不必单独计算它们。
考虑一下给定的案例......到目前为止除数计数包括:
1 1
2 2
3 2
4 3
6 4
是否有 O(1)从这些数字中获得phi(12)= 6的方式?
答案 3 :(得分:0)
这是一种理论上比O(n log(n))
好的算法,但对于合理n
可能更差。我相信其运行时间为O(n lg*(n))
,其中lg*
为https://en.wikipedia.org/wiki/Iterated_logarithm。
首先,您可以使用阿特金筛选找到n
及时O(n)
以内的所有素数。有关详细信息,请参阅https://en.wikipedia.org/wiki/Sieve_of_Atkin。
现在的想法是,我们将建立我们的计数列表,只插入一次计数。我们将逐个查看素数因子,并插入值作为最大素数的所有内容。但是,为了做到这一点,我们需要一个具有以下属性的数据结构:
O(1)
中向前和向后遍历插入值列表。i
“有效”地找到最后插入的数字。(行情是难以估计的部分。)
第一个是微不足道的,我们的数据结构中的每个槽都需要一个值的位置。第二个可以用双向链表完成。第三个可以通过跳过列表上的聪明变体来完成。第四部分从前三部分落下。
我们可以使用一组节点(不会开始初始化),并使用下面的字段看起来像一个双向链表:
value
我们正在寻找的答案。prev
我们得到答案的最后一个值。next
我们得到答案的下一个值。现在如果i
位于列表中且j
是下一个值,那么跳过列表技巧就是我们也会在prev
之后为第一个填充i
j
1}},第一个可被4整除,可被8整除,依此类推,直到达到i = 81
。因此,如果j = 96
和prev
我们会为82, 84, 88
和96
填写v
。
现在假设我们要在现有k
和i
之间的j
处插入值k
。我们该怎么做呢?我将展示只有i = 81
已知的伪代码,然后填写j = 96
,k = 90
和k.value := v
for temp in searching down from k for increasing factors of 2:
if temp has a value:
our_prev := temp
break
else if temp has a prev:
our_prev = temp.prev
break
our_next := our_prev.next
our_prev.next := k
k.next := our_next
our_next.prev := k
for temp in searching up from k for increasing factors of 2:
if j <= temp:
break
temp.prev = k
k.prev := our_prev
。
90
在我们的特定示例中,我们愿意从90, 88, 80, 64, 0
向下搜索prev
。但是当我们到达81
时,我们实际上被告知88
是90, 92, 96, 128, 256, ...
。我们愿意最多搜索92.prev
,但我们必须设置96.prev
O(log(k-i) + log(j-k) + 1)
,我们就完成了。
现在这是一个复杂的代码,但它的性能是O(log(n))
。这意味着它从1.value := 0
开始,但随着更多值的填充而变得更好。
那么我们如何初始化这个数据结构呢?我们初始化一组未初始化的值,然后设置1.next := n+1
,2.prev := 4.prev := 8.prev := 16.prev := ... := 1
和p
。然后我们开始处理我们的素数。
当我们到达素数n/p
时,我们首先搜索x*p, x*p^2, ...
下面的上一个插入值。从那里开始向后我们继续插入O(n)
的值,直到达到我们的极限。 (向后的原因是我们不想尝试插入,例如,一次为3次,一次为9次。向后移动可以防止这种情况发生。)
现在我们的运行时间是多少?找到素数是O(n/log(n))
。对于另一个O(log(n))
,查找初始插入也很容易O(n)
时间O(n log(n))
的操作。那么所有值的插入呢?这很简单1/log(n)
但我们可以做得更好吗?
首先,填写密度O(n/log(n)) * O(log(n)) = O(n)
的所有插入都可以及时完成1/log(log(n))
。然后,密度O(n/log(log(n))) * O(log(log(n))) = O(n)
的所有插入同样可以及时完成O(lg*(n))
。等等越来越多的日志。对于我给出的O(n lg*(n))
估算,我们获得的这些因素的数量为O(n)
。
我没有证明这个估计和你能做的一样好,但我认为它是。
所以,不是createProperties()
,而是非常接近。