Могу ли я уменьшить вычислительную сложность этого?

Ну, у меня есть немного кода, который сильно замедляет программу, потому что это линейная сложность, но многократно вызываемая программа, делающая программу квадратичной сложностью. Если возможно, я бы хотел уменьшить его вычислительную сложность, но в противном случае я просто оптимизирую его, где смогу. До сих пор я сократил до:

def table(n):
    a = 1
    while 2*a <= n:
      if (-a*a)%n == 1: return a

      a += 1

Кто-нибудь видит что-то, что я пропустил? Спасибо!

РЕДАКТИРОВАТЬ: я забыл упомянуть: п всегда простое число.

РЕДАКТИРОВАТЬ 2: Вот моя новая улучшенная программа (спасибо за все вклады!):

def table(n):
    if n == 2: return 1
    if n%4 != 1: return

    a1 = n-1
    for a in range(1, n//2+1):
        if (a*a)%n == a1: return a

РЕДАКТИРОВАТЬ 3: И проверить его в реальном контексте это гораздо быстрее! Ну, этот вопрос кажется решенным, но есть много полезных ответов. Я должен также сказать, что, как и вышеописанные оптимизации, я запомнил функцию, используя словари Python...

10 ответов

Решение

На основании второго редактирования OP:

def table(n):
    if n == 2: return 1
    if n%4 != 1: return

    mod = 0
    a1 = n - 1
    for a in xrange(1, a1, 2):
        mod += a

        while mod >= n: mod -= n
        if mod == a1: return a//2 + 1

Игнорируя алгоритм на мгновение (да, я знаю, плохая идея), время работы этого может быть значительно уменьшено, просто переключаясь с while в for,

for a in range(1, n / 2 + 1)

(Надеюсь, что в этом нет единой ошибки. Я склонен делать это.)

Еще одна вещь, которую я бы попробовал - посмотреть, можно ли увеличить ширину шага.

Взгляните на http://modular.fas.harvard.edu/ent/ent_py. Функция sqrtmod делает работу, если вы установите a = -1 и p = n.

Небольшой момент, который вы пропустили, заключается в том, что время работы вашего улучшенного алгоритма все еще находится в порядке квадратного корня из n. Пока у вас есть только небольшие простые числа n (скажем, менее 2^64), это нормально, и вы, вероятно, предпочтете свою реализацию более сложной.

Если простое число n становится больше, вам, возможно, придется переключиться на алгоритм, использующий немного теории чисел. Насколько мне известно, ваша проблема может быть решена только с помощью вероятностного алгоритма во времени log(n)^3. Если я правильно помню, предполагая, что гипотеза Римана верна (что большинство людей делают), можно показать, что время выполнения следующего алгоритма (в ruby ​​- извините, я не знаю python) - это log(log(n))* журнал (п)^3:

class Integer
  # calculate b to the power of e modulo self
  def power(b, e)
    raise 'power only defined for integer base' unless b.is_a? Integer
    raise 'power only defined for integer exponent' unless e.is_a? Integer
    raise 'power is implemented only for positive exponent' if e < 0
    return 1 if e.zero?
    x = power(b, e>>1)
    x *= x
    (e & 1).zero? ? x % self : (x*b) % self
  end
  # Fermat test (probabilistic prime number test)
  def prime?(b = 2)
    raise "base must be at least 2 in prime?" if b < 2
    raise "base must be an integer in prime?" unless b.is_a? Integer
    power(b, self >> 1) == 1
  end
  # find square root of -1 modulo prime
  def sqrt_of_minus_one
    return 1 if self == 2
    return false if (self & 3) != 1
    raise 'sqrt_of_minus_one works only for primes' unless prime?
    # now just try all numbers (each succeeds with probability 1/2)
    2.upto(self) do |b|
      e = self >> 1
      e >>= 1 while (e & 1).zero?
      x = power(b, e)
      next if [1, self-1].include? x
      loop do
        y = (x*x) % self
        return x if y == self-1
        raise 'sqrt_of_minus_one works only for primes' if y == 1
        x = y
      end
    end
  end
end

# find a prime
p = loop do
      x = rand(1<<512)
      next if (x & 3) != 1
      break x if x.prime?
    end

puts "%x" % p
puts "%x" % p.sqrt_of_minus_one

Медленная часть теперь находит простое число (для этого требуется прибл. Log(n)^4 целочисленная операция); нахождение квадратного корня из -1 занимает 512-разрядных простых чисел, все еще меньше секунды.

Рассмотрите возможность предварительного вычисления результатов и сохранения их в файле. В настоящее время многие платформы имеют огромную емкость диска. Тогда получение результата будет O(1) операцией.

(Основываясь на ответе Адама.) Посмотрите на страницу Википедии о квадратичной взаимности:

x ^ 2 ≡ −1 (mod p) разрешима тогда и только тогда, когда p ≡ 1 (mod 4).

Тогда вы можете избежать поиска корня именно для тех нечетных простых n, которые не совпадают с 1 по модулю 4:

def table(n):
    if n == 2: return 1
    if n%4 != 1: return None   # or raise exception
    ...

Похоже, вы пытаетесь найти квадратный корень из -1 по модулю n, К сожалению, это не простая проблема, в зависимости от того, какие значения n вход в вашу функцию. В зависимости от n, может даже не быть решения. Посмотрите Википедию для получения дополнительной информации по этой проблеме.

Редактировать 2: Удивительно, но уменьшение силы возведения в квадрат значительно сокращает время, по крайней мере, на моей установке Python2.5. (Я удивлен, потому что я думал, что накладные расходы интерпретатора занимают большую часть времени, и это не уменьшает количество операций во внутреннем цикле.) Сокращает время с 0,572 с до 0,146 с для таблицы (1234577).

 def table(n):
     n1 = n - 1
     square = 0
     for delta in xrange(1, n, 2):
         square += delta
         if n <= square: square -= n
         if square == n1: return delta // 2 + 1

Стрэджер опубликовал ту же идею, но я думаю, что менее жестко закодированы Опять же, ответ кувшина лучший.

Оригинальный ответ: Еще одна тривиальная настройка кода над Конрадом Рудольфом:

def table(n):
    n1 = n - 1
    for a in xrange(1, n // 2 + 1):
          if (a*a) % n == n1: return a

Ускоряет это на моем ноутбуке. (Около 25% для таблицы (1234577).)

Изменить: я не заметил тег python3.0; но главное изменение заключалось в том, чтобы вывести часть расчета из цикла, а не использовать xrange, (Академический, так как есть лучший алгоритм.)

Я прошел и исправил версию Гарварда, чтобы она работала с Python 3. http://modular.fas.harvard.edu/ent/ent_py

Я сделал несколько небольших изменений, чтобы результаты были точно такими же, как и у функции ОП. Есть два возможных ответа, и я заставил его вернуть меньший ответ.

import timeit

def table(n):

    if n == 2: return 1
    if n%4 != 1: return

    a1=n-1

    def inversemod(a, p):
        x, y = xgcd(a, p)
        return x%p

    def xgcd(a, b):
        x_sign = 1
        if a < 0: a = -a; x_sign = -1
        x = 1; y = 0; r = 0; s = 1
        while b != 0:
            (c, q) = (a%b, a//b)
            (a, b, r, s, x, y) = (b, c, x-q*r, y-q*s, r, s)
        return (x*x_sign, y)

    def mul(x, y):      
        return ((x[0]*y[0]+a1*y[1]*x[1])%n,(x[0]*y[1]+x[1]*y[0])%n)

    def pow(x, nn):      
        ans = (1,0)
        xpow = x
        while nn != 0:
           if nn%2 != 0:
               ans = mul(ans, xpow)
           xpow = mul(xpow, xpow)
           nn >>= 1
        return ans

    for z in range(2,n) :
        u, v = pow((1,z), a1//2)
        if v != 0:
            vinv = inversemod(v, n)
            if (vinv*vinv)%n == a1:
                vinv %= n
                if vinv <= n//2:
                    return vinv
                else:
                    return n-vinv


tt=0
pri = [ 5,13,17,29,37,41,53,61,73,89,97,1234577,5915587277,3267000013,3628273133,2860486313,5463458053,3367900313 ]
for x in pri:
    t=timeit.Timer('q=table('+str(x)+')','from __main__ import table')
    tt +=t.timeit(number=100)
    print("table(",x,")=",table(x))

print('total time=',tt/100)

Эта версия занимает около 3 мс, чтобы пройти через тестовые примеры выше.

Для сравнения используем простое число 1234577
OP Edit2 745мс
Принятый ответ 522мс
Выше функция 0,2 мс

Можно ли кешировать результаты?

Когда вы вычисляете большое n, вы получаете результаты для более низких n почти бесплатно.

Одна вещь, которую вы делаете, это повторение расчета -a*a снова и снова.

Создайте таблицу значений один раз, а затем посмотрите в главном цикле.

Кроме того, хотя это, вероятно, не относится к вам, потому что ваше имя функции - таблица, но если вы вызываете функцию, для вычисления которой требуется время, вы должны кэшировать результат в таблице и просто выполнить поиск таблицы, если вы вызовете ее снова с тем же значение. Это сэкономит вам время вычисления всех значений при первом запуске, но вы не тратите время на повторение расчета более одного раза.

Другие вопросы по тегам