【问题标题】:Code is taking too much time代码花费了太多时间
【发布时间】:2014-01-13 19:08:43
【问题描述】:

我在接受用户输入后编写了代码来排列数字。排序要求相邻数字的总和是素数。直到 10 作为输入代码都可以正常工作。如果我超出这个范围,系统就会挂起。请告诉我优化它的步骤

ex 输入 8
答案应该是:(1、2、3、4、7、6、5、8)
代码如下......

import itertools

x = raw_input("please enter a number")
range_x = range(int(x)+1)
del range_x[0]
result = list(itertools.permutations(range_x))
def prime(x):
    for i in xrange(1,x,2):
        if i == 1:
            i = i+1
        if x%i==0 and i < x :
            return False
    else:
        return True

def is_prime(a):
    for i in xrange(len(a)):
        print a
        if i < len(a)-1:
            if prime(a[i]+a[i+1]):
                pass
            else:
                return False
        else:
            return True


for i in xrange(len(result)):
    if i < len(result)-1:
        if is_prime(result[i]):
            print 'result is:'
            print result[i]
            break
    else:
        print 'result is'
        print result[i-1]

【问题讨论】:

  • 这可能不是你要找的答案,但从长远来看,这本书教你用pythonic的方式编写python代码。 diveintopython3.net
  • 不要为了快速的事情而使用 Python 怎么样? Python 本质上很慢。
  • 我仍在试图弄清楚订单有什么偏差,因为(1, 2, 3, 4, 5, 6, 7, 8) 似乎是输入8 的合理输出,因为每个差异都是素数1
  • @Cosine:切换到不同的语言不会修复错误的算法。
  • @user2873552,例如,您是否注意到两个奇数从不相邻?两个赔率之和总是偶数,所以不可能是素数。你怎么能利用它?仅此一项就会极大地削减搜索空间。同样,两个偶数也永远不会相邻。想想:-)

标签: python


【解决方案1】:

对于后代 ;-),这里还有一个基于找到哈密顿路径的方法。这是 Python3 代码。如所写,它在找到第一条路径时停止,但可以轻松更改以生成所有路径。在我的盒子上,它在大约 1 分钟内找到了 1 到 900 的所有 n 的解决方案。对于略大于 900 的n,它超过了最大递归深度。

prime 生成器 (psieve()) 对于这个特殊问题来说太过分了,但我很方便,不想再写一个 ;-)

路径查找器 (ham()) 是一种递归回溯搜索,使用经常(但不总是)非常有效的排序启发式方法:路径中与最后一个顶点相邻的所有顶点到目前为止,首先查看剩余出口最少的那些。例如,这是用于解决 Knights Tour 问题的“常用”启发式算法。在这种情况下,它通常会发现根本不需要回溯的游览。您的问题似乎比这更棘手。

def psieve():
    import itertools
    yield from (2, 3, 5, 7)
    D = {}
    ps = psieve()
    next(ps)
    p = next(ps)
    assert p == 3
    psq = p*p
    for i in itertools.count(9, 2):
        if i in D:      # composite
            step = D.pop(i)
        elif i < psq:   # prime
            yield i
            continue
        else:           # composite, = p*p
            assert i == psq
            step = 2*p
            p = next(ps)
            psq = p*p
        i += step
        while i in D:
            i += step
        D[i] = step

def build_graph(n):
    primes = set()
    for p in psieve():
        if p > 2*n:
            break
        else:
            primes.add(p)

    np1 = n+1
    adj = [set() for i in range(np1)]
    for i in range(1, np1):
        for j in range(i+1, np1):
            if i+j in primes:
                adj[i].add(j)
                adj[j].add(i)
    return set(range(1, np1)), adj

def ham(nodes, adj):
    class EarlyExit(Exception):
        pass

    def inner(index):
        if index == n:
            raise EarlyExit
        avail = adj[result[index-1]] if index else nodes
        for i in sorted(avail, key=lambda j: len(adj[j])):
            # Remove vertex i from the graph.  If this isolates
            # more than 1 vertex, no path is possible.
            result[index] = i
            nodes.remove(i)
            nisolated = 0
            for j in adj[i]:
                adj[j].remove(i)
                if not adj[j]:
                    nisolated += 1
                    if nisolated > 1:
                        break
            if nisolated < 2:
                inner(index + 1)
            nodes.add(i)
            for j in adj[i]:
                adj[j].add(i)

    n = len(nodes)
    result = [None] * n
    try:
        inner(0)
    except EarlyExit:
        return result

def solve(n):
    nodes, adj = build_graph(n)
    return ham(nodes, adj)

【讨论】:

    【解决方案2】:

    此答案基于@Tim Peters' suggestion about Hamiltonian paths

    有许多可能的解决方案。为了避免中间解决方案过多的内存消耗,可以生成随机路径。它还允许轻松利用多个 CPU(每个 cpu 并行生成自己的路径)。

    import multiprocessing as mp
    import sys
    
    def main():
        number = int(sys.argv[1])
    
        # directed graph, vertices: 1..number (including ends)
        # there is an edge between i and j if (i+j) is prime
        vertices = range(1, number+1)
        G = {} # vertex -> adjacent vertices
        is_prime = sieve_of_eratosthenes(2*number+1)
        for i in vertices:
            G[i] = []
            for j in vertices:
                if is_prime[i + j]:
                    G[i].append(j) # there is an edge from i to j in the graph
    
        # utilize multiple cpus
        q = mp.Queue()
        for _ in range(mp.cpu_count()):
            p = mp.Process(target=hamiltonian_random, args=[G, q])
            p.daemon = True # do not survive the main process
            p.start()
        print(q.get())
    
    if __name__=="__main__":
        main()
    

    Sieve of Eratosthenes 在哪里:

    def sieve_of_eratosthenes(limit):
        is_prime = [True]*limit
        is_prime[0] = is_prime[1] = False # zero and one are not primes
        for n in range(int(limit**.5 + .5)):
            if is_prime[n]:
                for composite in range(n*n, limit, n):
                    is_prime[composite] = False
        return is_prime
    

    和:

    import random
    
    def hamiltonian_random(graph, result_queue):
        """Build random paths until Hamiltonian path is found."""
        vertices = list(graph.keys())
        while True:
            # build random path
            path = [random.choice(vertices)] # start with a random vertice
            while True: # until path can be extended with a random adjacent vertex
                neighbours = graph[path[-1]]
                random.shuffle(neighbours)
                for adjacent_vertex in neighbours:
                    if adjacent_vertex not in path:
                        path.append(adjacent_vertex)
                        break
                else: # can't extend path
                    break
    
            # check whether it is hamiltonian
            if len(path) == len(vertices):
                assert set(path) == set(vertices)
                result_queue.put(path) # found hamiltonian path
                return
    

    示例

    $ python order-adjacent-prime-sum.py 20
    

    输出

    [19, 18, 13, 10, 1, 4, 9, 14, 5, 6, 17, 2, 15, 16, 7, 12, 11, 8, 3, 20]
    

    输出是满足条件的随机序列:

    • 它是范围从 1 到 20(包括)的排列
    • 相邻数之和为素数

    时间表现

    平均需要大约 10 秒才能获得 n = 900 的结果并将时间外推为指数函数,20 应该需要大约 20 秒:

    图像是使用以下代码生成的:

    import numpy as np
    figname = 'hamiltonian_random_noset-noseq-900-900'
    Ns, Ts = np.loadtxt(figname+'.xy', unpack=True)
    
    # use polyfit to fit the data
    # y = c*a**n
    # log y = log (c * a ** n)
    # log Ts = log c + Ns * log a
    coeffs = np.polyfit(Ns, np.log2(Ts), deg=1)
    poly = np.poly1d(coeffs, variable='Ns')
    
    # use curve_fit to fit the data
    from scipy.optimize import curve_fit
    def func(x, a, c):
        return c*a**x
    popt, pcov = curve_fit(func, Ns, Ts)
    aa, cc = popt
    a, c = 2**coeffs
    
    # plot it
    import matplotlib.pyplot as plt
    plt.figure()
    plt.plot(Ns, np.log2(Ts), 'ko', label='time measurements')
    plt.plot(Ns, np.polyval(poly, Ns), 'r-',
             label=r'$time = %.2g\times %.4g^N$' % (c, a))
    plt.plot(Ns, np.log2(func(Ns, *popt)), 'b-',
             label=r'$time = %.2g\times %.4g^N$' % (cc, aa))
    plt.xlabel('N')
    plt.ylabel('log2(time in seconds)')
    plt.legend(loc='upper left')
    plt.show()
    

    拟合值:

    >>> c*a**np.array([900, 1000])
    array([ 11.37200806,  21.56029156])
    >>> func([900, 1000], *popt)
    array([ 14.1521409 ,  22.62916398])
    

    【讨论】:

      【解决方案3】:

      动态编程,拯救:

      def is_prime(n):
          return all(n % i != 0 for i in range(2, n))
      
      def order(numbers, current=[]):
          if not numbers:
              return current
      
          for i, n in enumerate(numbers):
              if current and not is_prime(n + current[-1]):
                  continue
      
              result = order(numbers[:i] + numbers[i + 1:], current + [n])
      
              if result:
                  return result
      
          return False
      
      result = order(range(500))
      
      for i in range(len(result) - 1):
          assert is_prime(result[i] + result[i + 1])
      

      您可以通过增加最大递归深度来强制它适用于更大的列表。

      【讨论】:

      • 不适合初学者。主要测试也在进行中!
      • 如果你想对此着迷 ;-) 这是“正确”的方法:OP 正在无向图中寻找哈密顿路径,其顶点标记为 1 到 n,其中顶点当且仅当i+j 是素数时,ij 才连接。有很多算法可以做到这一点,其中包括 DP。找到最快的并证明它是最快的 - LOL ;-)
      • @Blender 如果可能的话,你能解释一下你是如何反转代码的吗?请我不明白。
      • 不太确定我是否喜欢is_prime 函数……它够快吗?
      • @Blender 你能和我分享一下算法吗
      【解决方案4】:

      这是我对解决方案的看法。正如蒂姆彼得斯所指出的,这是一个哈密顿路径问题。 所以第一步是以某种形式生成图表。

      在这种情况下,第 0 步是生成素​​数。我将使用筛子,但无论什么主要测试都可以。我们需要直到2 * n 的素数,因为这是任意两个数可以相加的最大数。

      m = 8
      n = m + 1 # Just so I don't have to worry about zero indexes and random +/- 1's
      primelen = 2 * m
      prime = [True] * primelen
      prime[0] = prime[1] = False
      for i in range(4, primelen, 2):
          prime[i] = False
      for i in range(3, primelen, 2):
          if not prime[i]:
              continue
          for j in range(i * i, primelen, i):
              prime[j] = False
      

      好的,现在我们可以使用prime[i] 测试素数。现在很容易制作图形边缘。如果我有一个数字 i,接下来会出现什么数字。我还将利用 i 和 j 具有相反奇偶性这一事实。

      pairs = [set(j for j in range(i%2+1, n, 2) if prime[i+j])
               for i in range(n)]
      

      所以这里 pairs[i] 是集合对象,其元素是整数 j 使得 i+j 是素数。

      现在我们需要遍历图表。这确实是耗时的部分,所有进一步的优化都将在这里完成。

      chains = [
          ([], set(range(1, n))
      ]
      

      chains 将在我们行走时跟踪有效路径。元组中的第一个元素将是您的结果。第二个元素是所有未使用的数字,或未访问的节点。这个想法是从队列中取出一条链,沿着路径走一步,然后放回去。

      while chains:
          chain, unused = chains.pop()
      
          if not chain:
              # we haven't even started, all unused are valid
              valid_next = unused
          else:
              # We need numbers that are both unused and paired with the last node
              # Using sets makes this easy
              valid_next = unused & pairs[chains[-1]]
      
          for num in valid_next:
              # Take a step to the new node and add the new path back to chains
              # Reminder, its important not to mutate anything here, always make new objs
              newchain  = chain + [num]
              newunused = unused - set([num])
              chains.append( (newchain, newunused) )
      
              # are we done?
              if not newunused:
                  print newchain
                  chains = False
      

      请注意,如果没有有效的下一步,则删除路径而不进行替换。

      这确实是内存效率低下,但在合理的时间内运行。最大的性能瓶颈是走图,因此下一个优化将是在智能位置弹出和插入路径,以优先考虑最可能的路径。在这种情况下,为您的链使用 collections.deque 或其他容器可能会有所帮助。

      编辑

      以下是如何实现路径优先级的示例。我们将为每条路径分配一个分数,并保持chains 列表按此分数排序。对于一个简单的例子,我会建议包含“更难使用”节点的路径更有价值。也就是说,路径上的每一步得分都会增加n - len(valid_next) 修改后的代码将如下所示。

      import bisect
      chains = ...
      chains_score = [0]
      while chains:
           chain, unused = chains.pop()
           score = chains_score.pop()
           ...
      
           for num in valid_next:
                newchain = chain + [num]
                newunused = unused - set([num])
                newscore = score + n - len(valid_next)
                index = bisect.bisect(chains_score, newscore)
                chains.insert(index, (newchain, newunused))
                chains_score.insert(index, newscore)
      

      请记住插入是O(n),因此添加它的开销可能相当大。值得对您的分数算法进行一些分析,以保持队列长度len(chains) 可控。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-02-15
        • 2020-07-23
        • 2013-07-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多