【问题标题】:Count of divisors of numbers till N in O(N)?O(N)中直到N的数字的除数计数?
【发布时间】:2017-11-10 18:45:30
【问题描述】:

所以,我们可以在 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)? 提前致谢。

【问题讨论】:

  • 所有这些都在 O(N)??
  • @coderredoc 是的。

标签: algorithm sieve


【解决方案1】:

这是对@גלעד ברקן 解决方案的简单优化。与其使用集合,不如使用数组。这大约是设置版本的 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)))

【讨论】:

  • 我也喜欢简洁的代码;以及使用加法而不是乘法计算除数数量的差异。
  • 我添加了一个额外的优化,在我的笔记本电脑上将我的 10,000,000 的 10,000,000 降低到 8 秒,而将你的降低到 6 秒。 p = p + 2 :)
  • 顺便说一下,这个:while (p &lt; n) 可能是这个:while (p &lt; n / 2),因为任何乘以更大的p 都超出了范围(在您的版本上又缩短了 0.4 秒)。
  • @גלעדברקן 您可以通过注意 min(p, n/p) 以下的所有内容都在 small_factors 中找到另一个加速,因此可以从该列表中删除并通过添加遍历。从我的角度来看,p &lt; n/2 实际上会产生错误的答案,因为它使用None 而不是2。虽然它可能是特殊情况。
  • "特殊情况?"是的,我们称它们为“质数”怎么样? ;) 我不确定你所说的加法遍历是什么意思,你能解释一下吗?
【解决方案2】:

这个怎么样?从素数2开始,保留一个元组列表(k, d_k),其中d_kk的除数个数,从(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 &gt; n 的列表中,并释放所有大于n / next_prime 的输出内存。

Pythoncode

【讨论】:

  • 最简单的确定方法是测试它与一个简单直接的算法并比较结果;然后,测量两者的经验增长顺序。
  • 但是从“在 O(n) 时间内最多 n 的素数列表”我可以立即看到 n 受到缓存大小的限制,因为要拥有 O( n)你需要一个连续的数组,AFAIK(即“欧拉筛”)。 Eratosthenes 在 n log log n 中运行。
  • @WillNess 我很困惑 - 你是说在 O(n) 时间内无法保存到 n 的素数列表吗?我只是从网上的一些阅读中假设了这部分。显然是阿特金筛子。
  • Eratsothenes 在 n log log n 中运行。我在维基百科中看到了有关“n / log log n with simple optimizations”的内容,但这是由数学家添加的;对他们来说“简单”对我来说并不那么简单。他以另一位数学家的文章为基础,这些文章谈到了展开的轮子螺旋线,如果简单地 完成,对尺寸的要求非常高,这势必会影响性能。我所知道的唯一实用的 O(n) 是欧拉的(或 Gries&Misra 的),它本身并不那么简单。并且它不能被分割 AFAIK。
  • "simple" 对我来说是 4-5 行 Haskell,上衣。最好不超过 2 个。Atkin,用户 GordonBGood 对 SO 有很多批评。
【解决方案3】:

考虑这些计数的总和,sum(phi(i) for i=1,n)。该总和为 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?

【讨论】:

  • 是否正确地说,对于从 1 到 N 的 一个连续的数组进行此枚举是必要的?无论如何,这就是我的想法,我想不出任何方法可以从某个较高的数字开始进行 O(1) 枚举。
  • @WillNess 这就是我的直觉。我在大学的数论中取得了优异的成绩,但我看不到这里的解决方案。如果您可以将任何因式分解识别为相对质数(例如上面的 3*4),那么您可以简单地将它们的 phi 值相乘以获得新的值。但是,我不知道有一种 O(1) 方法来识别两个这样的因素。此外,对于素数的幂,您需要一种不同的方法(虽然简单)。例如,8 没有这样的因式分解。
【解决方案4】:

这是一个理论上比O(n log(n)) 更好但合理的n 可能更差的算法。我相信它的运行时间是O(n lg*(n)),其中lg*https://en.wikipedia.org/wiki/Iterated_logarithm

首先,您可以使用阿特金筛在时间O(n) 中找到直到n 的所有素数。详情请见https://en.wikipedia.org/wiki/Sieve_of_Atkin

现在的想法是,我们将建立计数列表,每个计数只插入一次。我们将一个一个地检查素数,并为所有以它作为最大素数的东西插入值。然而,为了做到这一点,我们需要一个具有以下属性的数据结构:

  1. 我们可以为每个值存储一个值(特别是计数)。
  2. 我们可以在O(1) 中前后遍历插入值列表。
  3. 我们可以在i“高效”下方找到最后插入的数字。
  4. 插入应该是“高效的”。

(报价是难以估计的部分。)

第一个是微不足道的,我们数据结构中的每个槽都需要一个位置来存储值。第二个可以用双向链表来完成。第三个可以通过对跳过列表的巧妙变化来完成。第四个从前三个中掉出来。

我们可以用一个节点数组(一开始没有初始化)来做到这一点,其中包含以下字段,看起来像一个双向链表:

  1. value我们正在寻找的答案。
  2. prev 上一个我们有答案的值。
  3. next 下一个我们有答案的值。

现在如果i 在列表中并且j 是下一个值,跳过列表的技巧将是我们还将在i 之后填写prev 作为第一个,第一个可以被除以4,可被 8 整除,以此类推,直到达到 j。因此,如果i = 81j = 96 我们将填写prev82, 84, 88 然后96

现在假设我们要在现有的ij 之间的k 处插入一个值v。我们该怎么做呢?我将展示仅以 k known 开头的伪代码,然后填写 i = 81j = 96k = 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。但是当我们到达88 时,我们实际上被告知prev81。我们愿意搜索到90, 92, 96, 128, 256, ...,但是我们只需要设置92.prev 96.prev 就可以了。

现在这是一段复杂的代码,但它的性能是O(log(k-i) + log(j-k) + 1)。这意味着它以 O(log(n)) 开始,但随着更多值的填写而变得更好。

那么我们如何初始化这个数据结构呢?好吧,我们初始化一个未初始化值的数组,然后设置1.value := 01.next := n+12.prev := 4.prev := 8.prev := 16.prev := ... := 1。然后我们开始处理我们的素数。

当我们到达素数p 时,我们首先在n/p 下方搜索先前插入的值。从那里向后我们不断插入x*p, x*p^2, ... 的值,直到达到我们的极限。 (向后的原因是我们不想尝试在 3 中插入​​ 18 一次,在 9 中插入一次。向后移动可以防止这种情况。)

现在我们的运行时间是多少?寻找素数是O(n)。找到初始插入也很容易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(n lg*(n)) 估计值,我们得到的此类因子的数量是O(lg*(n))

我没有证明这个估计值和你能做的一样好,但我认为它是。

所以,不是O(n),而是非常接近。

【讨论】:

    猜你喜欢
    • 2011-05-11
    • 2019-09-29
    • 2021-10-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-21
    相关资源
    最近更新 更多