我正在研究Project Euler问题(number 15, lattice paths)。我已经用另一种方式解决了这个问题,但我很好奇如何优化我以前尝试解决问题的算法,因为它增长得非常快,并且对实际需要多长时间感到惊讶。所以我真的想学习如何分析并继续优化算法。
此算法的方法是使用角点作为点 - 左上角为(0,0)
,左下角为(2,2)
,为2x2网格。从顶点开始,路径只会是x+1
或y+1
。所以我通过检查网格中点的空间中是否存在下一个允许的移动来迭代地形成这些路径。
我最初从左上角(x+1, y+1)
开始,但发现从底部倒退,删除一些冗余并开始仅将有价值的数据存储在内存中会更有效率。这就是我现在的位置。可以进一步优化吗?还有什么其他类型的应用程序?
givenPoints
是网格中所有点的列表,存储为字符串 - 即'0202'
。算法存储唯一路径的最新点而不是整个路径,最后列表中的条目数等于唯一路径的数量。
def calcPaths4(givenPoints):
paths = []
paths.append(givenPoints[-1])
dims = int(math.sqrt(len(givenPoints))) - 1
numMoves = 2*dims
numPaths = 0
for x in range(0,numMoves):
t0= time.clock()
newPaths = []
for i in paths:
origin = int(i)
dest1 = origin - 1
dest3 = origin - 100
if ('%04d' % dest1) in givenPoints:
newPaths.append(('%04d' % dest1))
numPaths +=1
if ('%04d' % dest3) in givenPoints:
newPaths.append(('%04d' % dest3))
numPaths +=1
t= time.clock() - t0
paths = newPaths
print(str(x)+": " +str(t)+": " +str(len(paths)) )
return(paths)
答案 0 :(得分:3)
你的方法错了。从左上角开始到右下角向右移动20次,向下移动20次。
因此,您可以将任何路径视为长度为20的序列,其中10个元素为right
,10个元素为down
。你只需计算那里有多少个arrengements。
一旦你修复了right
移动down
移动,from math import factorial
def number_of_paths(k):
"""Number of paths from the top left and bottom right corner in a kxk grid."""
return factorial(2*k)//(factorial(k)**2)
移动就固定了,所以整个问题简化为:你可以用多少种方式从一组中选择10个位置20?
这可以通过binomial coefficient解决。
因此解决方案是:
n!/(k!*k!) = (n·(n-1)···(k+1))/k!
通过注明import operator as op
from functools import reduce
def number_of_paths(k):
"""Number of paths from the top left and bottom right corner in a kxk grid."""
return reduce(op.mul, range(2*k, k, -1), 1)//factorial(k)
:
(x,y)
请注意,路径数量会快速增长,这意味着 创建不同路径的任何算法都会变慢。认真“优化”这一点的唯一方法是改变方法,避免创建路径,只是计算它们。
我要指出一种不同的,更通用的方法:递归和memoization / dynamic programming。
当路径位于某个位置(x-1,y)
时,它可以直接转到(x, y-1)
或转到(x-1,y)
。因此,从该点到右下角的路径数是从(x, y-1)
到达右下角的路径数与从x==0
到达右下角的路径数之和:
基本情况是指您在边缘时,即y==0
或def number_of_paths(x, y):
if not x or not y:
return 1
return number_of_paths(x-1, y) + number_of_paths(x, y-1)
。
number_of_paths(x, y)
此解决方案遵循您的推理,但它只跟踪路径的数量。你可以再次看到效率非常低。
问题在于,当我们尝试计算number_of_paths(x-1, y)
时
我们最终执行以下步骤:
计算number_of_paths(x-2, y)
number_of_paths(x-1, y-1)
和number_of_paths(x, y-1)
计算number_of_paths(x-1, y-1)
number_of_paths(x, y-2)
和number_of_paths(x-1, y-1)
请注意def number_of_paths(x, y, table=None):
table = table if table is not None else {(0,0):1}
try:
# first look if we already computed this:
return table[x,y]
except KeyError:
# okay we didn't compute it, so we do it now:
if not x or not y:
result = table[x,y] = 1
else:
result = table[x,y] = number_of_paths(x-1, y, table) + number_of_paths(x, y-1, table)
return result
如何计算两次。但结果显然是一样的!所以我们可以在第一次计算它时,下次看到该调用时我们会返回已知的结果:
>>> number_of_paths(20,20)
137846528820
现在执行起来非常快:
(x-1,y-1)
您可以认为“执行两次通话,并不是什么大问题”,但您必须考虑到,如果(x-2, y-2)
的调用计算两次,则每次调用两次(x-2, y-2)
因此导致计算(x-3, y-3)
四次次。然后(x-20, y-20)
八次次,......然后kxk
1048576次!
或者我们可以构建一个def number_of_paths(x, y):
table = [[0]*(x+1) for _ in range(y+1)]
table[-1][-1] = 1
for i in reversed(range(x+1)):
for j in reversed(range(y+1)):
if i == x or j == y:
table[i][j] = 1
else:
table[i][j] = table[i+1][j] + table[i][j+1]
return table[0][0]
矩阵并从右下角填充它:
+1
请注意,此处的表格代表了交叉点,因此我们最终得到的尺寸为{{1}}。
这种记忆先前调用以便稍后重用它的技术称为memoization。一个更一般的原则是动态编程,你基本上把问题简化为填充表格数据结构,就像我们在这里做的那样,使用递归和memoization然后你通过使用你之前填充的指针“回溯”单元格以获得原始的解决方案问题