【问题标题】:Can I reduce the computational complexity of this?我可以降低它的计算复杂度吗?
【发布时间】:2008-12-20 20:51:36
【问题描述】:

嗯,我有这段代码大大减慢了程序的速度,因为它是线性复杂度,但被调用了很多次,使程序成为二次复杂度。如果可能的话,我想降低它的计算复杂度,否则我会尽可能地优化它。到目前为止,我已经减少到:

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

      a += 1

有人看到我错过的任何东西吗?谢谢!

编辑:我忘了说:n 总是一个素数。

编辑 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 字典记住了函数...

【问题讨论】:

  • 你多次调用这个:对于很多不同的 n 值?它们是 n 值的模式吗?
  • 是否有可能会多次调用 n 的某些值? (在这种情况下,存储计算值可能会有一些价值,以节省重新运行计算)
  • n 的最大值是多少,通常有多大?
  • 我添加了您的代码的改进(更快)版本...在我的答案中查看。
  • 这个函数被调用(和记忆)许多不同的素数 n。有的 n 很大,有的很小...... strager,谢谢!

标签: algorithm python-3.x complexity-theory time-complexity


【解决方案1】:

暂时忽略算法(是的,我知道,这是个坏主意),只需从 while 切换到 for,就可以大大减少该算法的运行时间。

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

(希望这不会有一个错误。我很容易犯这些。)

我会尝试的另一件事是查看步长是否可以增加。

【讨论】:

  • 你的意思是运行时间可以减少
【解决方案2】:

看看http://modular.fas.harvard.edu/ent/ent_py。 如果你设置 a = -1 和 p = n,函数 sqrtmod 就可以完成这项工作。

您错过了一个小点,因为您改进的算法的运行时间仍然是 n 的平方根。只要你只有小的素数 n(比如说小于 2^64),没关系,你可能应该更喜欢你的实现而不是更复杂的实现。

如果素数 n 变大,您可能需要切换到使用一点数论的算法。据我所知,您的问题只能使用时间 log​​(n)^3 中的概率算法来解决。如果我没记错的话,假设黎曼假设成立(大多数人都这样做),可以证明以下算法的运行时间(在 ruby​​ - 抱歉,我不知道 python)是 log(log(n))*日志(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 位素数仍然不到一秒。

【讨论】:

    【解决方案3】:

    考虑预先计算结果并将其存储在文件中。现在很多平台都有巨大的磁盘容量。那么,获取结果将是一个 O(1) 操作。

    【讨论】:

    • 我不会对磁盘​​执行此操作,只是对内存执行此操作。
    • @me.yahoo.com/loren.pechtel,他的意思是在启动时将文件加载到内存中,而不是每次启动应用程序时都计算表。
    • 这不切实际:首先计算它们太慢了。但通常这是一种选择。
    【解决方案4】:

    (以亚当的回答为基础。) 查看quadratic reciprocity上的维基百科页面:

    x^2 ≡ −1 (mod p) 当且仅当 p ≡ 1 (mod 4) 是可解的。

    那么你就可以避免为那些奇数的素数 n 精确地搜索根,这些素数 n 不等于 1 模 4:

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

    【讨论】:

      【解决方案5】:

      基于 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 mod >= n: mod -= n
      • @Rex Logan,嗯...我想你是对的,但是当我测试值 0..31337 时,它们都起作用了。我认为这是因为 a1=n-1:如果是 a1=n-1,我也许可以做 mod>n-1。奇怪的副作用对我有利,我猜。无论如何我都会纠正它。谢谢!
      【解决方案6】:

      您似乎正试图找到 -1 模 n 的平方根。不幸的是,这不是一个简单的问题,这取决于 n 的哪些值输入到您的函数中。根据n,甚至可能没有解决方案。有关此问题的更多信息,请参阅Wikipedia

      【讨论】:

      • n 始终是质数,因此可能会有所帮助。
      【解决方案7】:

      编辑 2:令人惊讶的是,降低平方的强度会大大减少时间,至少在我的 Python2.5 安装中是这样。 (我很惊讶,因为我认为解释器开销占用了大部分时间,这并没有减少内部循环中的操作计数。)将 table(1234577) 的时间从 0.572 秒减少到 0.146 秒。

       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
      

      strager 发布了the same idea,但我认为编码不那么紧密。同样,jug's answer 是最好的。

      原始答案:在 Konrad Rudolph 之上的另一个微不足道的编码调整:

      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.0,只涉及旧版本。但是,使用// 当然更好。
      • 将 n-1(或 -a)提升到循环之外是我想到的改变。这占了加速的大部分。
      【解决方案8】:

      你可以缓存结果吗?

      当您计算较大的 n 时,您几乎可以免费获得较小 n 的结果。

      【讨论】:

        【解决方案9】:

        您正在做的一件事是一遍又一遍地重复计算 -a*a。

        创建一个值表,然后在主循环中查找。

        虽然这可能不适用于您,因为您的函数名称是表,但是如果您调用一个需要时间计算的函数,您应该将结果缓存在一个表中,如果您再次调用它,只需进行表查找具有相同的价值。这可以节省您在第一次运行时计算所有值的时间,但您不会浪费时间多次重复计算。

        【讨论】:

        • 查找可能比乘法慢。与无符号模数相比,乘法相当便宜。
        • 这是学术性的,因为有更好的算法,但我的回答使 -a*a 计算的一部分脱离了循环。
        【解决方案10】:

        我查看并修复了哈佛版本以使其与 python 3 一起使用。 http://modular.fas.harvard.edu/ent/ent_py

        我做了一些小改动,使结果与 OP 的功能完全相同。有两个可能的答案,我强迫它返回较小的答案。

        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 745ms
        接受的答案 522ms
        以上函数0.2ms

        【讨论】:

          猜你喜欢
          • 2017-03-20
          • 1970-01-01
          • 1970-01-01
          • 2011-01-10
          • 2023-03-06
          • 1970-01-01
          • 2020-10-03
          • 1970-01-01
          相关资源
          最近更新 更多