Fatoração semiprime mais rápida

28

Escreva um programa para fatorar um número semi-primo no menor tempo possível.

Para fins de teste, use o seguinte: 38! +1 (523022617466601111760007224100074291200000001)

É igual a: 14029308060317546154181 × 37280713718589679646221

Soham Chowdhury
fonte
2
Embora eu goste da parte "mais rápida", uma vez que oferece a linguagens como C a vantagem das linguagens típicas de codegolfing, eu me pergunto como você testará os resultados?
Lister
1
Se você quer dizer que 12259243será usado para testar a rapidez com que os programas são, os resultados serão tão pequenos que você não obterá diferenças estatisticamente significativas.
Peter Taylor
Eu adicionei um número maior, thx para o heads-up.
Soham Chowdhury
@ Sr. Lister, vou testá-lo no meu próprio PC.
Soham Chowdhury
5
inb4 alguém usa abuso de pré-processador para escrever uma tabela de pesquisa de 400 exabytes.
Wug

Respostas:

59

Python (com PyPy JIT v1.9) ~ 1.9s

Usando uma peneira quadrática polinomial múltipla . Entendi que isso era um desafio de código, então optei por não usar nenhuma biblioteca externa (exceto a logfunção padrão , suponho). Ao cronometrar, o PyPy JIT deve ser usado, pois resulta em tempos 4-5 vezes mais rápidos que o do cPython .

Atualização (29/07/2013):
Desde a publicação original, fiz várias alterações menores, mas significativas, que aumentam a velocidade geral em um fator de cerca de 2,5x.

Atualização (27/08/2014):
Como esta postagem ainda está recebendo atenção, atualizei a my_math.pycorreção de dois erros para qualquer pessoa que possa estar usando:

  • isqrtestava com defeito, às vezes produzindo saída incorreta para valores muito próximos de um quadrado perfeito. Isso foi corrigido e o desempenho aumentou usando uma semente muito melhor.
  • is_primeTem sido atualizado. Minha tentativa anterior de remover 2 sprips quadrados perfeitos foi sem entusiasmo, na melhor das hipóteses. Adicionei uma verificação de 3 sprp - uma técnica usada pelo Mathmatica - para garantir que o valor testado seja sem quadrados.

Atualização (24/11/2014):
se ao final do cálculo não forem encontradas congruências não triviais, o programa agora peneirará polinômios adicionais. Isso foi marcado anteriormente no código como TODO.


mpqs.py

from my_math import *
from math import log
from time import clock
from argparse import ArgumentParser

# Multiple Polynomial Quadratic Sieve
def mpqs(n, verbose=False):
  if verbose:
    time1 = clock()

  root_n = isqrt(n)
  root_2n = isqrt(n+n)

  # formula chosen by experimentation
  # seems to be close to optimal for n < 10^50
  bound = int(5 * log(n, 10)**2)

  prime = []
  mod_root = []
  log_p = []
  num_prime = 0

  # find a number of small primes for which n is a quadratic residue
  p = 2
  while p < bound or num_prime < 3:

    # legendre (n|p) is only defined for odd p
    if p > 2:
      leg = legendre(n, p)
    else:
      leg = n & 1

    if leg == 1:
      prime += [p]
      mod_root += [int(mod_sqrt(n, p))]
      log_p += [log(p, 10)]
      num_prime += 1
    elif leg == 0:
      if verbose:
        print 'trial division found factors:'
        print p, 'x', n/p
      return p

    p = next_prime(p)

  # size of the sieve
  x_max = len(prime)*60

  # maximum value on the sieved range
  m_val = (x_max * root_2n) >> 1

  # fudging the threshold down a bit makes it easier to find powers of primes as factors
  # as well as partial-partial relationships, but it also makes the smoothness check slower.
  # there's a happy medium somewhere, depending on how efficient the smoothness check is
  thresh = log(m_val, 10) * 0.735

  # skip small primes. they contribute very little to the log sum
  # and add a lot of unnecessary entries to the table
  # instead, fudge the threshold down a bit, assuming ~1/4 of them pass
  min_prime = int(thresh*3)
  fudge = sum(log_p[i] for i,p in enumerate(prime) if p < min_prime)/4
  thresh -= fudge

  if verbose:
    print 'smoothness bound:', bound
    print 'sieve size:', x_max
    print 'log threshold:', thresh
    print 'skipping primes less than:', min_prime

  smooth = []
  used_prime = set()
  partial = {}
  num_smooth = 0
  num_used_prime = 0
  num_partial = 0
  num_poly = 0
  root_A = isqrt(root_2n / x_max)

  if verbose:
    print 'sieving for smooths...'
  while True:
    # find an integer value A such that:
    # A is =~ sqrt(2*n) / x_max
    # A is a perfect square
    # sqrt(A) is prime, and n is a quadratic residue mod sqrt(A)
    while True:
      root_A = next_prime(root_A)
      leg = legendre(n, root_A)
      if leg == 1:
        break
      elif leg == 0:
        if verbose:
          print 'dumb luck found factors:'
          print root_A, 'x', n/root_A
        return root_A

    A = root_A * root_A

    # solve for an adequate B
    # B*B is a quadratic residue mod n, such that B*B-A*C = n
    # this is unsolvable if n is not a quadratic residue mod sqrt(A)
    b = mod_sqrt(n, root_A)
    B = (b + (n - b*b) * mod_inv(b + b, root_A))%A

    # B*B-A*C = n <=> C = (B*B-n)/A
    C = (B*B - n) / A

    num_poly += 1

    # sieve for prime factors
    sums = [0.0]*(2*x_max)
    i = 0
    for p in prime:
      if p < min_prime:
        i += 1
        continue
      logp = log_p[i]

      inv_A = mod_inv(A, p)
      # modular root of the quadratic
      a = int(((mod_root[i] - B) * inv_A)%p)
      b = int(((p - mod_root[i] - B) * inv_A)%p)

      k = 0
      while k < x_max:
        if k+a < x_max:
          sums[k+a] += logp
        if k+b < x_max:
          sums[k+b] += logp
        if k:
          sums[k-a+x_max] += logp
          sums[k-b+x_max] += logp

        k += p
      i += 1

    # check for smooths
    i = 0
    for v in sums:
      if v > thresh:
        x = x_max-i if i > x_max else i
        vec = set()
        sqr = []
        # because B*B-n = A*C
        # (A*x+B)^2 - n = A*A*x*x+2*A*B*x + B*B - n
        #               = A*(A*x*x+2*B*x+C)
        # gives the congruency
        # (A*x+B)^2 = A*(A*x*x+2*B*x+C) (mod n)
        # because A is chosen to be square, it doesn't need to be sieved
        val = sieve_val = A*x*x + 2*B*x + C

        if sieve_val < 0:
          vec = set([-1])
          sieve_val = -sieve_val

        for p in prime:
          while sieve_val%p == 0:
            if p in vec:
              # keep track of perfect square factors
              # to avoid taking the sqrt of a gigantic number at the end
              sqr += [p]
            vec ^= set([p])
            sieve_val = int(sieve_val / p)

        if sieve_val == 1:
          # smooth
          smooth += [(vec, (sqr, (A*x+B), root_A))]
          used_prime |= vec
        elif sieve_val in partial:
          # combine two partials to make a (xor) smooth
          # that is, every prime factor with an odd power is in our factor base
          pair_vec, pair_vals = partial[sieve_val]
          sqr += list(vec & pair_vec) + [sieve_val]
          vec ^= pair_vec
          smooth += [(vec, (sqr + pair_vals[0], (A*x+B)*pair_vals[1], root_A*pair_vals[2]))]
          used_prime |= vec
          num_partial += 1
        else:
          # save partial for later pairing
          partial[sieve_val] = (vec, (sqr, A*x+B, root_A))
      i += 1

    num_smooth = len(smooth)
    num_used_prime = len(used_prime)

    if verbose:
      print 100 * num_smooth / num_prime, 'percent complete\r',

    if num_smooth > num_used_prime:
      if verbose:
        print '%d polynomials sieved (%d values)'%(num_poly, num_poly*x_max*2)
        print 'found %d smooths (%d from partials) in %f seconds'%(num_smooth, num_partial, clock()-time1)
        print 'solving for non-trivial congruencies...'

      used_prime_list = sorted(list(used_prime))

      # set up bit fields for gaussian elimination
      masks = []
      mask = 1
      bit_fields = [0]*num_used_prime
      for vec, vals in smooth:
        masks += [mask]
        i = 0
        for p in used_prime_list:
          if p in vec: bit_fields[i] |= mask
          i += 1
        mask <<= 1

      # row echelon form
      col_offset = 0
      null_cols = []
      for col in xrange(num_smooth):
        pivot = col-col_offset == num_used_prime or bit_fields[col-col_offset] & masks[col] == 0
        for row in xrange(col+1-col_offset, num_used_prime):
          if bit_fields[row] & masks[col]:
            if pivot:
              bit_fields[col-col_offset], bit_fields[row] = bit_fields[row], bit_fields[col-col_offset]
              pivot = False
            else:
              bit_fields[row] ^= bit_fields[col-col_offset]
        if pivot:
          null_cols += [col]
          col_offset += 1

      # reduced row echelon form
      for row in xrange(num_used_prime):
        # lowest set bit
        mask = bit_fields[row] & -bit_fields[row]
        for up_row in xrange(row):
          if bit_fields[up_row] & mask:
            bit_fields[up_row] ^= bit_fields[row]

      # check for non-trivial congruencies
      for col in null_cols:
        all_vec, (lh, rh, rA) = smooth[col]
        lhs = lh   # sieved values (left hand side)
        rhs = [rh] # sieved values - n (right hand side)
        rAs = [rA] # root_As (cofactor of lhs)
        i = 0
        for field in bit_fields:
          if field & masks[col]:
            vec, (lh, rh, rA) = smooth[i]
            lhs += list(all_vec & vec) + lh
            all_vec ^= vec
            rhs += [rh]
            rAs += [rA]
          i += 1

        factor = gcd(list_prod(rAs)*list_prod(lhs) - list_prod(rhs), n)
        if factor != 1 and factor != n:
          break
      else:
        if verbose:
          print 'none found.'
        continue
      break

  if verbose:
    print 'factors found:'
    print factor, 'x', n/factor
    print 'time elapsed: %f seconds'%(clock()-time1)
  return factor

if __name__ == "__main__":
  parser =ArgumentParser(description='Uses a MPQS to factor a composite number')
  parser.add_argument('composite', metavar='number_to_factor', type=long,
      help='the composite number to factor')
  parser.add_argument('--verbose', dest='verbose', action='store_true',
      help="enable verbose output")
  args = parser.parse_args()

  if args.verbose:
    mpqs(args.composite, args.verbose)
  else:
    time1 = clock()
    print mpqs(args.composite)
    print 'time elapsed: %f seconds'%(clock()-time1)

my_math.py

# divide and conquer list product
def list_prod(a):
  size = len(a)
  if size == 1:
    return a[0]
  return list_prod(a[:size>>1]) * list_prod(a[size>>1:])

# greatest common divisor of a and b
def gcd(a, b):
  while b:
    a, b = b, a%b
  return a

# modular inverse of a mod m
def mod_inv(a, m):
  a = int(a%m)
  x, u = 0, 1
  while a:
    x, u = u, x - (m/a)*u
    m, a = a, m%a
  return x

# legendre symbol (a|m)
# note: returns m-1 if a is a non-residue, instead of -1
def legendre(a, m):
  return pow(a, (m-1) >> 1, m)

# modular sqrt(n) mod p
# p must be prime
def mod_sqrt(n, p):
  a = n%p
  if p%4 == 3:
    return pow(a, (p+1) >> 2, p)
  elif p%8 == 5:
    v = pow(a << 1, (p-5) >> 3, p)
    i = ((a*v*v << 1) % p) - 1
    return (a*v*i)%p
  elif p%8 == 1:
    # Shank's method
    q = p-1
    e = 0
    while q&1 == 0:
      e += 1
      q >>= 1

    n = 2
    while legendre(n, p) != p-1:
      n += 1

    w = pow(a, q, p)
    x = pow(a, (q+1) >> 1, p)
    y = pow(n, q, p)
    r = e
    while True:
      if w == 1:
        return x

      v = w
      k = 0
      while v != 1 and k+1 < r:
        v = (v*v)%p
        k += 1

      if k == 0:
        return x

      d = pow(y, 1 << (r-k-1), p)
      x = (x*d)%p
      y = (d*d)%p
      w = (w*y)%p
      r = k
  else: # p == 2
    return a

#integer sqrt of n
def isqrt(n):
  c = n*4/3
  d = c.bit_length()

  a = d>>1
  if d&1:
    x = 1 << a
    y = (x + (n >> a)) >> 1
  else:
    x = (3 << a) >> 2
    y = (x + (c >> a)) >> 1

  if x != y:
    x = y
    y = (x + n/x) >> 1
    while y < x:
      x = y
      y = (x + n/x) >> 1
  return x

# strong probable prime
def is_sprp(n, b=2):
  if n < 2: return False
  d = n-1
  s = 0
  while d&1 == 0:
    s += 1
    d >>= 1

  x = pow(b, d, n)
  if x == 1 or x == n-1:
    return True

  for r in xrange(1, s):
    x = (x * x)%n
    if x == 1:
      return False
    elif x == n-1:
      return True

  return False

# lucas probable prime
# assumes D = 1 (mod 4), (D|n) = -1
def is_lucas_prp(n, D):
  P = 1
  Q = (1-D) >> 2

  # n+1 = 2**r*s where s is odd
  s = n+1
  r = 0
  while s&1 == 0:
    r += 1
    s >>= 1

  # calculate the bit reversal of (odd) s
  # e.g. 19 (10011) <=> 25 (11001)
  t = 0
  while s:
    if s&1:
      t += 1
      s -= 1
    else:
      t <<= 1
      s >>= 1

  # use the same bit reversal process to calculate the sth Lucas number
  # keep track of q = Q**n as we go
  U = 0
  V = 2
  q = 1
  # mod_inv(2, n)
  inv_2 = (n+1) >> 1
  while t:
    if t&1:
      # U, V of n+1
      U, V = ((U + V) * inv_2)%n, ((D*U + V) * inv_2)%n
      q = (q * Q)%n
      t -= 1
    else:
      # U, V of n*2
      U, V = (U * V)%n, (V * V - 2 * q)%n
      q = (q * q)%n
      t >>= 1

  # double s until we have the 2**r*sth Lucas number
  while r:
    U, V = (U * V)%n, (V * V - 2 * q)%n
    q = (q * q)%n
    r -= 1

  # primality check
  # if n is prime, n divides the n+1st Lucas number, given the assumptions
  return U == 0

# primes less than 212
small_primes = set([
    2,  3,  5,  7, 11, 13, 17, 19, 23, 29,
   31, 37, 41, 43, 47, 53, 59, 61, 67, 71,
   73, 79, 83, 89, 97,101,103,107,109,113,
  127,131,137,139,149,151,157,163,167,173,
  179,181,191,193,197,199,211])

# pre-calced sieve of eratosthenes for n = 2, 3, 5, 7
indices = [
    1, 11, 13, 17, 19, 23, 29, 31, 37, 41,
   43, 47, 53, 59, 61, 67, 71, 73, 79, 83,
   89, 97,101,103,107,109,113,121,127,131,
  137,139,143,149,151,157,163,167,169,173,
  179,181,187,191,193,197,199,209]

# distances between sieve values
offsets = [
  10, 2, 4, 2, 4, 6, 2, 6, 4, 2, 4, 6,
   6, 2, 6, 4, 2, 6, 4, 6, 8, 4, 2, 4,
   2, 4, 8, 6, 4, 6, 2, 4, 6, 2, 6, 6,
   4, 2, 4, 6, 2, 6, 4, 2, 4, 2,10, 2]

max_int = 2147483647

# an 'almost certain' primality check
def is_prime(n):
  if n < 212:
    return n in small_primes

  for p in small_primes:
    if n%p == 0:
      return False

  # if n is a 32-bit integer, perform full trial division
  if n <= max_int:
    i = 211
    while i*i < n:
      for o in offsets:
        i += o
        if n%i == 0:
          return False
    return True

  # Baillie-PSW
  # this is technically a probabalistic test, but there are no known pseudoprimes
  if not is_sprp(n, 2): return False

  # idea shamelessly stolen from Mathmatica
  # if n is a 2-sprp and a 3-sprp, n is necessarily square-free
  if not is_sprp(n, 3): return False

  a = 5
  s = 2
  # if n is a perfect square, this will never terminate
  while legendre(a, n) != n-1:
    s = -s
    a = s-a
  return is_lucas_prp(n, a)

# next prime strictly larger than n
def next_prime(n):
  if n < 2:
    return 2
  # first odd larger than n
  n = (n + 1) | 1
  if n < 212:
    while True:
      if n in small_primes:
        return n
      n += 2

  # find our position in the sieve rotation via binary search
  x = int(n%210)
  s = 0
  e = 47
  m = 24
  while m != e:
    if indices[m] < x:
      s = m
      m = (s + e + 1) >> 1
    else:
      e = m
      m = (s + e) >> 1

  i = int(n + (indices[m] - x))
  # adjust offsets
  offs = offsets[m:] + offsets[:m]
  while True:
    for o in offs:
      if is_prime(i):
        return i
      i += o

E / S de amostra:

$ pypy mpqs.py --verbose 94968915845307373740134800567566911
smoothness bound: 6117
sieve size: 24360
log threshold: 14.3081031579
skipping primes less than: 47
sieving for smooths...
144 polynomials sieved (7015680 values)
found 405 smooths (168 from partials) in 0.513794 seconds
solving for non-trivial congruencies...
factors found:
216366620575959221 x 438925910071081891
time elapsed: 0.685765 seconds

$ pypy mpqs.py --verbose 523022617466601111760007224100074291200000001
smoothness bound: 9998
sieve size: 37440
log threshold: 15.2376302725
skipping primes less than: 59
sieving for smooths...
428 polynomials sieved (32048640 values)
found 617 smooths (272 from partials) in 1.912131 seconds
solving for non-trivial congruencies...
factors found:
14029308060317546154181 x 37280713718589679646221
time elapsed: 2.064387 seconds

Nota: não usar a --verboseopção fornecerá tempos ligeiramente melhores:

$ pypy mpqs.py 94968915845307373740134800567566911
216366620575959221
time elapsed: 0.630235 seconds

$ pypy mpqs.py 523022617466601111760007224100074291200000001
14029308060317546154181
time elapsed: 1.886068 seconds

Conceitos básicos

Em geral, uma peneira quadrática é baseada na seguinte observação: qualquer composto ímpar n pode ser representado como:

Isso não é muito difícil de confirmar. Como n é ímpar, a distância entre quaisquer dois co-fatores de n deve ser igual a 2d , onde x é o ponto médio entre eles. Além disso, a mesma relação vale para qualquer múltiplo de n

Observe que, se qualquer um desses x e d puder ser encontrado, ele resultará imediatamente em um fator (não necessariamente primo) de n , pois x + d e x - d dividem n por definição. Essa relação pode ser ainda mais enfraquecida - na conseqüência de permitir possíveis congruências triviais - da seguinte forma:

So in general, if we can find two perfect squares which are equivalent mod n, then it's fairly likely that we can directly produce a factor of n a la gcd(x ± d, n). Seems pretty simple, right?

Exceto que não é. Se pretendíamos realizar uma pesquisa exaustiva sobre todo x possível , precisaríamos pesquisar o intervalo inteiro de [ n , √ ( 2n ) ], que é marginalmente menor que a divisão de teste completa, mas também exige uma is_squareoperação cara a cada iteração para confirme o valor de d . A menos que seja sabido de antemão que n tem fatores muito próximos de n , a divisão de teste provavelmente será mais rápida.

Talvez possamos enfraquecer ainda mais essa relação. Suponha que escolhemos um x , de modo que, para

uma fatoração primária completa de y é facilmente conhecida. Se tivéssemos essas relações suficientes, poderíamos construir um d adequado , se escolhermos um número de y de modo que o produto deles seja um quadrado perfeito; isto é, todos os fatores primos são usados ​​um número par de vezes. De fato, se tivermos mais y do que o número total de fatores primos únicos que eles contêm, é garantida a existência de uma solução; Torna-se um sistema de equações lineares. A questão agora é: como escolhemos esse x ? É aí que a peneira entra em jogo.

A peneira

Considere o polinômio:

Então, para qualquer primo p e inteiro k , o seguinte é verdadeiro:

Isso significa que, depois de resolver as raízes do polinômio mod p - ou seja, você encontrou um x de modo que y (x) ≡ 0 (mod p) , ergo y é divisível por p - e encontrou um número infinito de tal x . Dessa forma, você pode peneirar um intervalo de x , identificando pequenos fatores primos de y , esperançosamente encontrando alguns para os quais todos os fatores primos são pequenos. Números conhecidos como k-smooth , onde k é o maior fator primordial usado.

Existem alguns problemas com essa abordagem. Nem todos os valores de x são adequados; na verdade, existem apenas muito poucos, centrados em torno de n . Valores menores se tornarão amplamente negativos (devido ao termo -n ), e valores maiores se tornarão muito grandes, de modo que é improvável que sua fatoração primária consista apenas em números primos pequenos. Haverá vários desses x , mas, a menos que o composto que você está fatorando seja muito pequeno, é altamente improvável que você encontre suavidades suficientes para resultar em uma fatoração. E assim, para n maior , torna-se necessário peneirar múltiplos polinômios de uma determinada forma.

Polinômios múltiplos

Então, precisamos de mais polinômios para peneirar? Que tal agora:

Isso vai funcionar. Observe que A e B podem literalmente ser qualquer valor inteiro, e a matemática ainda é válida. Tudo o que precisamos fazer é escolher alguns valores aleatórios, resolver a raiz do polinômio e peneirar os valores próximos de zero. Nesse ponto, poderíamos chamá-lo de bom o suficiente: se você atirar pedras suficientes em direções aleatórias, poderá quebrar uma janela mais cedo ou mais tarde.

Exceto, também há um problema com isso. Se a inclinação do polinômio for grande na interceptação x, o que será se não for relativamente plana, haverá apenas alguns valores adequados para peneirar por polinômio. Vai funcionar, mas você vai peneirar vários polinômios antes de conseguir o que precisa. Podemos fazer melhor?

Nós podemos fazer melhor. Uma observação, como resultado de Montgomery, é a seguinte: se A e B são escolhidos de modo que exista algum C satisfazendo

todo o polinômio pode ser reescrito como

Além disso, se A for escolhido como um quadrado perfeito, o termo A inicial poderá ser negligenciado durante a peneiração, resultando em valores muito menores e em uma curva muito mais plana. Para que essa solução exista, n deve ser um resíduo quadrático modA , que pode ser conhecido imediatamente calculando o símbolo de Legendre :
( n | √A ) = 1 . Observe que, para resolver B , é necessário conhecer uma fatoração primária completa de √A (para obter a raiz quadrada modular √n (mod √A) ), e é por isso que √A é normalmente escolhido para ser primo.

Pode então ser mostrado que se , então, para todos os valores de x ∈ [ -M, M ] :

E agora, finalmente, temos todos os componentes necessários para implementar nossa peneira. Ou nós?

Poderes primários como fatores

Nossa peneira, como descrito acima, tem uma falha importante. Ele pode identificar quais valores de x resultarão em um y divisível por p , mas não pode identificar se esse y é divisível ou não por uma potência de p . Para determinar isso, precisaríamos realizar uma divisão de teste sobre o valor a ser peneirado, até que não seja mais divisível por p . Parecemos ter chegado a um impassé: o ponto principal da peneira era que não precisávamos fazer isso. Hora de verificar o manual.

Isso parece bastante útil. Se a soma do ln de todos os pequenos fatores primos de y estiver próxima do valor esperado de ln (y) , é quase certo que y não tenha outros fatores. Além disso, se ajustarmos um pouco o valor esperado, também podemos identificar valores tão suaves que possuem vários poderes de números primos como fatores. Dessa forma, podemos usar a peneira como um processo de 'pré-triagem' e apenas fatorar os valores que provavelmente serão suaves.

Isso também tem algumas outras vantagens. Observe que primos pequenos contribuem muito pouco para a soma final , mas ainda assim exigem mais tempo de peneira. Peneirar o valor 3 requer mais tempo que 11, 13, 17, 19 e 23 combinados . Em vez disso, podemos simplesmente pular os primeiros números primos e ajustar o limite de acordo, assumindo que uma certa porcentagem deles teria passado.

Outro resultado é que é permitido que vários valores "deslizem", que são na maioria suaves, mas contêm um único cofator grande. Poderíamos simplesmente descartar esses valores, mas suponha que encontrássemos outro valor praticamente suave, com exatamente o mesmo cofator. Podemos então usar esses dois valores para construir um y utilizável ; como o produto conterá esse grande cofator ao quadrado, ele não precisa mais ser considerado.

Juntando tudo

A última coisa que precisamos fazer é usar esses valores de y para construir x e d adequados . Suponha que consideremos apenas os fatores não quadrados de y , ou seja, os fatores primos de uma potência ímpar. Então, cada y pode ser expresso da seguinte maneira:

que pode ser expresso na forma de matriz:

O problema torna-se então para encontrar um vector v de tal modo que vM =(modificação 2) , onde é o vector nulo. Ou seja, para resolver para o espaço nulo esquerdo da M . Isso pode ser feito de várias maneiras, a mais simples das quais é para executar Gaussian Eliminação de M T , substituindo a operação de adição fila com uma linha xor . Isso resultará em vários vetores de base de espaço nulo, qualquer combinação dos quais produzirá uma solução válida.

A construção de x é bastante direta. É simplesmente o produto de Ax + B para cada um dos y usados. A construção de d é um pouco mais complicada. Se pegarmos o produto de todos os y , teremos um valor de 10s de milhares, se não 100s de milhares de dígitos, para o qual precisamos encontrar a raiz quadrada. Essa calcação é impraticável cara. Em vez disso, podemos manter o controle dos poderes pares de primos durante o processo de peneiramento, e depois usar e e XOR operações sobre os vetores de fatores não-quadrados para reconstruir a raiz quadrada.

Parece que atingi o limite de 30000 caracteres. Ahh, bem, acho que isso é bom o suficiente.

primo
fonte
5
Bem, eu nunca passei na álgebra no ensino médio (na verdade desisti no primeiro semestre do primeiro ano), mas você simplifica o entendimento da perspectiva de um programador. Não pretendo compreendê-lo completamente sem colocá-lo em prática, mas aplaudo. Você deve expandir este post fora do site e publicá-lo, sério!
Jdstankosky
2
Concordo. Excelente resposta com uma ótima explicação. +1
Soham Chowdhury
1
@primo Suas respostas para várias perguntas aqui foram incrivelmente completas e interessantes. Muito apreciado!
Paul Walls
4
Como último comentário, gostaria de expressar minha gratidão a Will Ness por conceder a recompensa de +100 nesta questão. Era literalmente toda a sua reputação.
Primo
2
@StepHen faz. Infelizmente, ele usa a versão original de 2012, sem as melhorias de velocidade e com um erro na eliminação gaussiana (erros quando a coluna final é uma coluna dinâmica). Tentei entrar em contato com o autor há algum tempo, mas não recebi resposta.
Primo
2

Bem, seu 38! +1 quebrou meu script php, não sei por quê. De fato, qualquer semi-primo com mais de 16 dígitos quebra meu script.

No entanto, usando 8980935344490257 (86028157 * 104395301), meu script conseguiu um tempo de 25,963 segundos no meu computador doméstico (2,61 GHz AMD Phenom 9950). Muito mais rápido que o meu computador de trabalho, que tinha quase 31 segundos no Core 2 Duo a 2,93 GHz.

php - 757 caracteres incl. novas linhas

<?php
function getTime() {
    $t = explode( ' ', microtime() );
    $t = $t[1] + $t[0];
    return $t;
}
function isDecimal($val){ return is_numeric($val) && floor($val) != $val;}
$start = getTime();
$semi_prime = 8980935344490257;
$slice      = round(strlen($semi_prime)/2);
$max        = (pow(10, ($slice))-1);
$i          = 3;
echo "\nFactoring the semi-prime:\n$semi_prime\n\n";

while ($i < $max) {
    $sec_factor = ($semi_prime/$i);
    if (isDecimal($sec_factor) != 1) {
        $mod_f = bcmod($i, 1);
        $mod_s = bcmod($sec_factor, 1);
        if ($mod_f == 0 && $mod_s == 0) {
            echo "First factor = $i\n";
            echo "Second factor = $sec_factor\n";
            $end=getTime();
            $xtime=round($end-$start,4).' seconds';
            echo "\n$xtime\n";
            exit();
        }
    }
    $i += 2;
}
?>

Eu estaria interessado em ver esse mesmo algoritmo em c ou em alguma outra linguagem compilada.

jdstankosky
fonte
Os números do PHP têm precisão de apenas 53 bits, aproximadamente 16 dígitos decimais
copie o
3
A implementação do mesmo algoritmo em C ++ usando números inteiros de 64 bits levou apenas 1,8 segundos para ser executada no meu computador. Existem vários problemas com essa abordagem: 1. Ele não consegue lidar com números grandes o suficiente. 2. Mesmo que pudesse, e assumindo que todos os números, independentemente da duração, usassem a mesma quantidade de tempo para a divisão de teste, cada aumento de ordem de magnitude resultaria em um aumento equivalente no tempo. Como o seu primeiro fator é cerca de 14 ordens de grandeza menor que o primeiro fator, esse algoritmo levaria mais de 9 milhões de anos para fatorar o semiprime fornecido.
CasaDeRobison
É certo que eu não sou o melhor em matemática, mas, para números muito grandes, os métodos padrão de fatoração de primos simplesmente não funcionarão (usando uma elipse etc.), tanto quanto eu sei. Com isso em mente, como o próprio algoritmo poderia ser aprimorado?
Jdstankosky #
2
A peneira de Eratóstenes começa com uma lista de números e remove todos os múltiplos de 2, depois 3, 5 e 7, etc. O que resta depois que a peneira é concluída são apenas números primos. Essa peneira pode ser 'pré-calculada' para um certo número de fatores. Porque lcm(2, 3, 5, 7) == 210, o padrão de números eliminados por esses fatores se repetirá a cada 210 números, e apenas 48 permanecem. Dessa forma, você pode eliminar 77% de todos os números da divisão de teste, em vez dos 50%, utilizando apenas probabilidades.
primo
1
@primo Por curiosidade, quanto tempo você dedicou a isso? Levaria séculos para pensar nessas coisas. Na época em que escrevi isso, eu estava pensando em como os números primos sempre eram ímpares. Eu não tentei ir além disso e eliminar as probabilidades não primárias também. Parece tão simples em retrospecto.
Jdstankosky