【问题标题】:Count the semiprime numbers in the given range [a..b]计算给定范围内的半素数 [a..b]
【发布时间】:2019-05-22 23:23:28
【问题描述】:

我正在解决 Codility 问题CountSemiprimes: Count the semiprime numbers in the given range [a..b]

任务说明

素数是一个正整数 X,它正好有两个不同的除数:1 和 X。前几个素数是 2、3、5、7、11 和 13。

半素数是一个自然数,它是两个(不一定不同)素数的乘积。前几个半素数是 4、6、9、10、14、15、21、22、25、26。

给定两个非空数组 P 和 Q,每个数组由 M 个整数组成。这些数组表示关于指定范围内的半素数的查询。

查询 K 要求您在 (P[K], Q[K]) 范围内找到半素数的个数,其中 1 ≤ P[K] ≤ Q[K] ≤ N。

为以下假设编写一个有效的算法:

  • N 是 [1..50,000] 范围内的整数;
  • M是[1..30,000]范围内的整数;
  • 数组 P、Q 的每个元素都是 [1..N] 范围内的整数; P[i] ≤ Q[i]。

我的解决方案

我目前的分数是 66%,问题是大数据集的性能:

  • 大随机数,长度 = ~30,000
  • 所有最大范围

测试表明,它应该需要大约 2 秒,但我的解决方案需要 7 秒。

这是我目前的解决方案

class Solution {
    private static List<Integer> getPrimes(int max) {
        List<Integer> primes = new ArrayList<>(max / 2);

        for (int i = 0; i < max; i++)
            if (isPrime(i))
                primes.add(i);

        return primes;
    }

    private static boolean isPrime(int val) {
        if (val <= 1)
            return false;
        if (val <= 3)
            return true;

        for (int i = 2, sqrt = (int)Math.sqrt(val); i <= sqrt; i++)
            if (val % i == 0)
                return false;

        return true;
    }

    private static boolean[] getSemiPrimes(int N) {
        List<Integer> primes = getPrimes(N);
        boolean[] semiPrimes = new boolean[N + 1];

        for (int i = 0; i < primes.size(); i++) {
            if (primes.get(i) > N)
                break;

            for (int j = i; j < primes.size(); j++) {
                if (primes.get(j) > N || N / primes.get(i) < primes.get(j))
                    break;

                int semiPrime = primes.get(i) * primes.get(j);

                if (semiPrime <= N)
                    semiPrimes[semiPrime] = true;
            }
        }

        return semiPrimes;
    }

    public static int[] solution(int N, int[] P, int[] Q) {
        boolean[] semiPrimes = getSemiPrimes(N);
        int[] res = new int[P.length];

        for (int i = 0; i < res.length; i++)
            for (int j = P[i]; j <= Q[i]; j++)
                if (semiPrimes[j])
                    res[i]++;

        return res;
    }
}

关于提高性能的任何想法?我的最后一个是删除 Set 以使用数组保存半素数。它帮助我解决了几个性能测试。

【问题讨论】:

  • 你应该使用类似 Eratosthenes 的筛子来生成素数。我认为这应该更快。
  • @marstran 我已经检查过了。 for 循环到 sqrt(n) 是找到所有素数的最有效方法 [0...n]
  • 这绝对不是找到 n 以内的所有素数的最有效方法。检查单个值是否为质数效果更好,但有一些方法可以使其更快,例如使用i += 2 而不是i++,或者只使用check divisibility for values in the form 6*i ± 1。筛子始终是生成素数列表的最佳方法。您错误地完成了基准测试
  • @phuclv 无论如何,这不会增加3倍
  • @oleg.cherednik 直到sqrt(n) 的for循环可能是确定一个数是否为素数的最快方法。但是,生成素数列表不是最快的。为此目的,筛子要快得多。

标签: java algorithm performance


【解决方案1】:

一个得分 100% 的 Java 解决方案如下:

  • 找出它们的乘积不大于N的素数集合

  • 从它们创建半素数作为 0 和 1 的按位数组

  • 创建半素数的前缀和

  • 计算O(M)中从P[i]Q[i]的查询

整个算法是Codility的测试结果评估所声明的O(N * log(log(N)) + M)

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class CountSemiPrime {

    public static void main(String[] args) {
        int[] P = new int[] {1, 4, 16};
        int[] Q = new int[] {26, 10, 20};
        System.out.println( Arrays.toString( new CountSemiPrime().solution( 26, P, Q ) ) );
    }

    public int[] solution(int N, int[] P, int[] Q) {

        Integer[] primes = sieve(N/2+1);

        int[] temp = new int[N+1];
        for (int i = 0; i < primes.length; i++) {
            for (int j = 0; j < primes.length; j++) {
                int semiPrime = primes[i] * primes[j];
                if(semiPrime <= N)
                    temp[semiPrime] = 1;
            }
        }

        int[] prefix = new int[N+1];
        for (int i = 1; i < temp.length; i++) {
            prefix[i] = temp[i] + prefix[i-1];
        }

        int[] retVal = new int[P.length];
        for (int i = 0; i < retVal.length; i++) {
            retVal[i] = prefix[Q[i]] - prefix[P[i]-1];
        }

        return retVal; 
    }


    public Integer[] sieve(int n) {

        boolean[] temp = new boolean[n+1];
        for (int i = 0; i < temp.length; i++) {
            temp[i] = true;
        }
        temp[0] = temp[1] = false;

        int i = 2;
        while (i * i <= n) {
            removeProducts( temp, i );
            i++;
        }

        List<Integer> ret = new ArrayList<>();
        for (int j = 0; j < temp.length; j++) {
            if(temp[j])
                ret.add( j );
        }

        return ret.toArray( new Integer[ret.size()] );
    }

    private void removeProducts(boolean[] temp, int i) {
        for (int j = i*i; j < temp.length; j++) {
            if(temp[j] && j % i == 0) {
                temp[j] = false;
            }
        }
    }
}

【讨论】:

    【解决方案2】:

    您可以预先计算一个大小为 N+1 的数组 A,它在 A[i] 中存储小于或等于 i 的半素数。然后可以立即计算出查询p, q:p 和 q(含)之间的半素数为A[q] - A[p-1]

    这个数组可以有效地计算:设 P 是一个小于或等于 N/2 的素数数组。然后(在类似 java 的伪代码中):

    A = new int[N+1]
    for (int p : P) {
      for (int q : P) {
          if (p*q > N || q > p) break;
          A[p*q] = 1
      }
    }
    
    for (int i = 1; i <= N; i++)
        A[i] += A[i-1]
    

    这是通过在数组中用1 标记半素数,然后取一个累积和来实现的。它的运行时间比 O(N^2) 好,比 O(N) 时间差——P 中大约有 N/2logN 素数,所以第一部分是 O((N/logN)^2),而总和是 O(N)。 [注意:我猜第一部分的复杂性比 O((N/log N)^2) 更好,因为内部循环提前终止,但我还没有证明这一点]。使用 Erastothenes 的筛子计算 P 中的素数是 O(N log log N)。

    此程序的 Python 版本需要 0.07 秒来为 N=50000 预计算 A,并执行 30000 次查询。它在 codility 上运行时获得满分 (100),并且 codility 报告它检测到代码的复杂度为 O(N log(log(N)) + M)。

    【讨论】:

      【解决方案3】:

      Ruby 100% 解决方案

      require 'prime'
      require 'set'
      
      def solution(n, p, q)
          primes = Prime::EratosthenesGenerator.new.take_while {|i| i <= n/2 }
          sqrt = Math.sqrt(n)
          semiprimes = primes.each_with_index.inject(Set.new) do |acc, (e,i)|
            break acc if e > sqrt.to_i
            primes[i..-1].each{ |pr| e*pr > n ? break : acc << e*pr }
            acc
          end
          offsets = semiprimes.sort.each_with_index.inject([]) {|acc,(el,i)| acc[el] = i+1;acc  }
      
          p.each_with_index.inject([]) do |acc, (el,i)|
            next acc << 0 unless offsets[el..q[i]]
      
            left =  offsets[el..q[i]].detect{|a| a}
            next acc << 0 unless left
      
            right = offsets[el..q[i]].reverse_each.detect{|a| a}
      
            acc << ((left..right).size)
          end
      end
      

      【讨论】:

        【解决方案4】:

        我的解决方案使用 Eratosthenes 筛法,使得数字 N 的最小素数因子存储在数组 Factor[N] 中。 然后如果 Factor[N/Factor[N]] = 0,我们有一个半素数递增和扫描。 返回数组的条目 r 将是: A[r]=Inclusive_scan[Q[r]]-Inclusive_scan[P[r]-1]。

        这里对应的python代码(100% task score)

        def solution(N, P, Q):
         A=len(P)*[0]
         if N<4:
             return A
        #Minimum prime factor of n stored in Factor[n]
         Factor = [0] * (N + 1)
         i = 2
         while (i * i <= N):
          if (Factor[i] == 0):
           k = i * i
           while (k <= N):
            if (Factor[k] == 0):
             Factor[k] = i;
            k += i
          i += 1
        #Count semi prime numbers and store 
        #sum scan in array Incluse_scan   
         Incluse_scan=[0] * (N + 1)
         cnt_semi=0
         for k in range(4,N+1):
             if Factor[k]!=0:
                 d=int(k/Factor[k])
                 if Factor[d]==0:
                     cnt_semi+=1                 
             Incluse_scan[k]=cnt_semi   
        #Do the difference of semi prime counters
         for r in range(0,len(P)):
             if(P[r]<=4):
               min_inclusive=0
             else:
               min_inclusive=P[r]-1 
             A[r]=Incluse_scan[Q[r]]-Incluse_scan[min_inclusive] 
         return A
        

        【讨论】:

        【解决方案5】:

        这是我在 C++ 中的 100% 解决方案。您可以在我的 cpp 中的github 中找到其他答案:

        
        vector<int> getFactArr(int n) {
            vector<int> f(n+1, 0);
            f[1] = 1;
            int i = 2;
            while (i * i <= n) {
                if (f[i] == 0) {
                    int k = i * i;
                    while (k <= n) {
                        if (f[k] == 0)
                            f[k] = i;
                        k+=i;
                    }
                }
                i++;
            }
        
            return f;
        }
        
        vector<int> solution(int N, vector<int> &P, vector<int> &Q) {
            vector<int> F = getFactArr(N);
            vector<int> prefix_semi_primes(N + 1, 0);
        
            for (int x = 1; x <= N; x++) {
                if (F[x] > 0 && F[x / F[x]] == 0)
                    prefix_semi_primes[x]++;
                prefix_semi_primes[x] += prefix_semi_primes[x - 1];
            }
        
            const int M = P.size();
            vector<int> ans(M, 0);
            for (int i = 0; i < M; i++) {
                ans[i] = prefix_semi_primes[Q[i]] - prefix_semi_primes[P[i] - 1];
            }
        
            return ans;
        }
        
        

        【讨论】:

          【解决方案6】:

          这是一个有趣的问题。我试了一下,得到了 88% 的分数。

          这是我的策略:

          • 我用Sieve of Eratosthenes 得到了一个BitSet 来表示素数。
          • 现在我遍历了 BitSet 并将所有素数添加到 primeList 中。
          • 我寻找半素数的策略有点有趣,我逐渐接触到这个策略。
          private static boolean isSemiPrime(int n) {
              if(n==1 || n==0 || primeBitSet.get(n))
                  return false;
              int firstFactor = findFirstFactor(n);
              if(firstFactor==0 || firstFactor==1)
                  return false;
              return isPrime(n / firstFactor);
          }
          
          private static int findFirstFactor(int n) {
          
              for (int i = 0; i < primeList.size(); i++) {
                  if (n % primeList.get(i) == 0)
                      return primeList.get(i);
              }
              // should never be the case
              return 0;
          }

          我不太清楚为什么我得到了 88% 的分数。 (我错过了什么)

          但最有趣和值得注意的部分是检查给定数字是否为半素数的策略:

          • 找到给定数的第一个质因数
          • 然后检查给定数和第一素数因子的商是否为素数。
          • 如果是素数,则给定数是半素数,否则给定数不是半素数。

          请注意,我还做了一个非常幼稚的簿记形式,其中我创建了一个累积数组,用于存储直到索引 x 的半素数总数。一次填充这个数组并回答O(1) 中的每个查询又是明显的优化。

          与解决方案无关,但我的 Task Score 为 88%、Correctness 100% 和 Performance 80%。我很高兴听到建议和我错过的任何事情。

          希望这会有所帮助。 :)

          【讨论】:

            【解决方案7】:

            const isSemiPrime = (num) => {
                let cnt = 0
                for (let i = 2; cnt < 2 && i * i <= num; ++i) {
                    while (num % i == 0) {
                        num /= i
                        ++cnt
                    }
                }
                if (num > 1)++cnt
                return cnt == 2 ? true : false
            }
            
            console.log(
                [4, 6, 9, 10, 14, 15, 21, 22, 25, 26, 33, 34, 35, 38, 39, 46, 49, 51, 55].filter(isSemiPrime)
                    .length
            )

            【讨论】:

            • 除了发布代码 sn-p 之外,您能详细说明一下吗?
            【解决方案8】:

            这里是Javascript版本的解决方案,不过是55%:

            function solution(N, P, Q) {
            
              function isPrime(num) {
                for(var i = 2; i < num; i++)
                  if(num % i === 0) return false;
                return num > 1;
              }
            
              const min = Math.min(...P)
              const max = Math.max(...Q)
              const A = []
              for(let i=min;i<max;i++) {
                  for(let j=min;j<max;j++) {
                      if (isPrime(i) && isPrime(j)) {
                          const prod = j * i
                          if (prod > max) break
                          if (A.includes(prod)) continue
                          A.push(j * i)
                      }
                  }
              }
            
              const result = []
              for(let i=0;i<P.length;i++) {
                  for(let j=P[i];j<=Q[i];j++) {
                      result[i] = result[i] || 0
                      if (A.includes(j)) {
                          result[i]++
                      }
                  }
              }
              return result
            }
            

            【讨论】:

              【解决方案9】:

              我想提一下,你用来寻找素数的方法效率低。

              您的代码:

              private static List<Integer> getPrimes(int max) {
                  List<Integer> primes = new ArrayList<>(max / 2);
              
              **    for (int i = 0; i < max; i++)
              **        if (isPrime(i))
              **            primes.add(i);
              
                  return primes;
              }
              
              private static boolean isPrime(int val) {
                  if (val <= 1)
                      return false;
                  if (val <= 3)
                      return true;
              
              **    for (int i = 2, sqrt = (int)Math.sqrt(val); i <= sqrt; i++)
              **        if (val % i == 0)
              **            return false;
              
                  return true;
              }
              

              我已经标记了要注意的行。 我会这样做:

              private static List<Integer> getPrimes(int max) {
                  List<Integer> primes = new ArrayList<>(max / 2);
                  primes.add(2);
              
                  for (int i = 3; i < max; i++)
                      if (isPrime(i, primes))
                          primes.add(i);
              
                  return primes;
              }
              
              private static boolean isPrime(int val, List<Integer> primes) {
                  int sqrtv = Math.sqrt(val);
                  for (int i = 0; i < primes.length(); i++)
                  {
                      int prime = primes.get(i);
                      if (val % primes.get(i) == 0)
                      {
                          return false;
                      } else if (prime > sqrtv) {
                          return true;
                      }
                  }
              
                  return true;
              }
              

              这是基于以下事实:

              1. 对 isPrime 的唯一调用来自 getPrimes。 getPrimes 将始终按升序调用 val。
              2. 在使用参数 val 调用 isPrime 时,getPrimes 已经获得了所有小于 val 的素数的列表。
              3. 在确定素数时,除以非素数是没有意义的。如果我们已经知道数字“a”不能被 2 整除,那么为什么还要将它除以 4、6、8 或 10 呢?如果我们知道它不能被 3 整除,那么它就不能被 9 整除...所以所有非素数检查都通过使用先前计算的素数进行过滤,仅用于执行检查。

              【讨论】:

                【解决方案10】:

                这是我 100% 的 C++ 语言。我正在使用前缀和。时间复杂度 O(N * log(log(N)) + M)。

                #include <iostream>
                #include <vector>
                #include <cmath>
                
                using namespace std;
                
                vector<int> solution(int N, vector<int> &P, vector<int> &Q)
                {
                    vector<bool> sieve(N, true);
                    vector<int> ret;
                    sieve[0] = sieve[1] = false;
                    int i = 2;
                
                    while (i * i <= N)
                    {
                        if (sieve[i])
                        {
                            int k = i * i;
                            while (k <= N)
                            {
                                sieve[k] = false;
                                k += i;
                            }
                        }
                        i++;
                    }
                
                    vector<int> prefixSum(N + 1, 0);
                    
                    for (int i = 2; i <= sqrt(N); i++)
                        if (sieve[i])
                            for (int j = i; j <= N; j++)
                            {
                                if (j * i > N)
                                    break;
                
                                if (sieve[j])
                                    prefixSum[j * i]++;
                            }
                
                    int carry;
                    for (unsigned int i = 5; i < prefixSum.size(); i++)
                    {
                        carry = prefixSum[i - 1];
                        prefixSum[i] += carry;
                    }
                
                    for (unsigned int i = 0; i < P.size(); i++)
                        ret.push_back(prefixSum[Q[i]] - prefixSum[P[i] - 1]);
                
                    return ret;
                }
                

                【讨论】:

                • 请详细说明您的答案,仅将代码作为答案不是一个好习惯
                【解决方案11】:

                100% 解决方案被分解。 https://app.codility.com/demo/results/trainingGVNHKU-MA5/

                首先使用 Eratosthenes 的筛子来锻炼什么是素数。

                def get_sieve(n):
                    # Use the sieve or Eratosthenes to produce an array of primes 
                    # where factor[n] == 0 indicates a prime number
                    factors = [0] * (n+1)
                    i=2
                    i2 = i*i
                    while (i2 <= n):
                        if not factors[i]:
                            k = i2
                            while k <= n:
                                if not factors[k]:
                                    factors[k] = i
                                k += i
                        i += 1
                        i2 = i*i
                    return factors
                

                接下来,判断这个数是否是半素数。如果它的两个因数都是素数,则它的半素数。

                def is_semi_prime(n, factors):
                    if factors[n]: # Check its not a prime
                        for r in range(int(n**.5)+1, 1, -1):
                            if not n%r:
                                d = n//r
                                return (not factors[d]) and (not factors[r])
                    return False
                

                然后扫描最多 N 个数字的范围以计算增加的​​半素数的斜率。只需测量切片内的斜率即可查看该切片中出现了多少个半素数。

                def solution(N, P, Q):
                    # produce a slope of increasing semi primes
                    factors = get_sieve(N)
                    slope = [0] * (N+1)
                    for i in range(1, N+1):
                        slope[i] = slope[i-1] + is_semi_prime(i, factors) # Auto casting!! :-)
                    # Optimus Prime!
                    # print(list(enumerate(slope)))
                    return [slope[Q[j]] - slope[P[j]-1] for j in range(len(P))]
                

                https://github.com/niall-oc/things/blob/master/codility/count_semiprimes.py 以及更多 https://github.com/niall-oc/things/blob/master/codility/

                【讨论】:

                  【解决方案12】:

                  我采取了稍微不同的方法。该线程中的其他有效解决方案构建了一个常规的 Eratosthenes (F) 筛,其中记录了槽中最小的素数因子,因此半素数是那些 F[x] > 0 和 F[x // F[ x]] == 0,即除以最小的素数得到另一个素数。

                  我的方法有点慢,但不使用除法,并构建了一个有趣的中间体:一个筛子,可以准确计算有多少因子构成了数字的素数分解(以及素数处的零)。对于每个素数 p,我会在位置 2p、3p、4p、... 处增加筛子,但也会计算 p^2、2p^2、3p^2...、p^3、2p^3 的因子, 3p^3, 4p^3,... 等等。 16 的槽存储值 4(素数分解:2*2*2*2),因为槽被来自 2、2^2、2^3 和 2^4 的访问命中。

                  那么半素数就是那些正好有 2 个素因数的位置。

                  之后,我构建了一个半素数的前缀计数,用于在恒定时间内回答查询。

                  def solution(N, P, Q):
                      num_factors = [0] * (N+1)
                      for i in range(2, N+1):
                          if num_factors[i] == 0:
                              # Count visits to multiples of i by adding i each time
                              add_visit = i+i
                              while add_visit < N+1:
                                  num_factors[add_visit] += 1
                                  add_visit += i
                              # But squares of prime count as 2 factors, cubes count as 3 etc,
                              # so also run visits for multiples of the squares, cubes, etc. 
                              power_prime = i*i
                              while power_prime < N+1:
                                  visit = power_prime
                                  while visit < N+1:
                                      num_factors[visit] += 1
                                      visit += power_prime
                                  power_prime *= i
                      semiprime_prefix_count = [0] * (N+1)
                      for i in range(1, N+1):
                          semiprime_prefix_count[i] = semiprime_prefix_count[i-1]
                          if num_factors[i] == 2:
                              semiprime_prefix_count[i] += 1
                      results = []
                      for p, q in zip(P, Q):
                          results.append(semiprime_prefix_count[q] - semiprime_prefix_count[p-1])
                      #print(list(zip(range(N+1),num_factors)))
                      #print(list(zip(range(N+1),semiprime_prefix_count)))
                      return results
                  

                  【讨论】:

                    【解决方案13】:

                    使用通常的筛子得到最多为 N 的素数。

                    使用素数得到最多为 N 的半素数。您可以通过检查任意数的两个素因数来做到这一点。

                    创建前缀总和以将半素数的数量存储到特定索引。

                    最后,通过在查询结束和开始时减去数字来获得半素数。

                    vector<int> solution(int N, vector<int> &P, vector<int> &Q)                     
                    {                                                                               
                      vector<int> sieve(N, 0);                                                      
                      for (int prime = 2; prime * prime <= N; ++prime) {                            
                        for (int composite = prime * prime; composite <= N; composite += prime) {   
                          if (!sieve[composite - 1]) sieve[composite - 1] = prime;                  
                        }                                                                           
                      }                                                                             
                                                                                                    
                      vector<int> semi_primes;                                                      
                      for (int i = 3; i < N; ++i) {                                                 
                        const int e = sieve[i];                                                     
                        if (e > 0 && !sieve[i / e]) semi_primes.push_back(i + 1);                   
                      }                                                                             
                      if (semi_primes.empty()) semi_primes.push_back(0);                            
                                                                                                    
                      vector<int> prefix_sums(N + 1, 0);                                            
                      for (int i = 1, spi = 0; i <= N; ++i) {                                       
                        prefix_sums[i] = ((semi_primes[spi] != i) ? spi : ++spi);                   
                      }                                                                             
                                                                                                    
                      int M = P.size();                                                             
                      vector<int> semi_prime_counts(M, 0);                                          
                      for (int i = 0; i < M; ++i) {                                                 
                        semi_prime_counts[i] = prefix_sums[Q[i]] - prefix_sums[P[i] - 1];           
                      }                                                                             
                                                                                                    
                      return semi_prime_counts;                                                     
                    } 
                    

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2020-05-05
                      • 1970-01-01
                      • 2020-11-09
                      • 1970-01-01
                      相关资源
                      最近更新 更多