昨晚我试图解决challenge #15 from Project Euler:
从a的左上角开始 2×2格,有6条路线(没有 回溯到右下角 角。
alt text http://projecteuler.net/project/images/p_015.gif
通过一条路线有多少条路线 20×20格?
我认为这不应该那么难,所以我写了一个基本的递归函数:
const int gridSize = 20;
// call with progress(0, 0)
static int progress(int x, int y)
{
int i = 0;
if (x < gridSize)
i += progress(x + 1, y);
if (y < gridSize)
i += progress(x, y + 1);
if (x == gridSize && y == gridSize)
return 1;
return i;
}
我确认它适用于较小的网格,例如2×2或3×3,然后将其设置为运行20×20网格。想象一下,5个小时之后,程序仍然高兴地处理数字,并且只完成了大约80%(基于检查其在网格中的当前位置/路线),这让我感到惊讶。
显然我是以错误的方式解决这个问题。你会如何解决这个问题?我认为它应该用一个方程而不是像我这样的方法来解决,但不幸的是,这不是我的强项。
更新:
我现在有一个工作版本。基本上它缓存了在仍然需要遍历n×m块之前获得的结果。以下是代码以及一些注释:
// the size of our grid
static int gridSize = 20;
// the amount of paths available for a "NxM" block, e.g. "2x2" => 4
static Dictionary<string, long> pathsByBlock = new Dictionary<string, long>();
// calculate the surface of the block to the finish line
static long calcsurface(long x, long y)
{
return (gridSize - x) * (gridSize - y);
}
// call using progress (0, 0)
static long progress(long x, long y)
{
// first calculate the surface of the block remaining
long surface = calcsurface(x, y);
long i = 0;
// zero surface means only 1 path remains
// (we either go only right, or only down)
if (surface == 0)
return 1;
// create a textual representation of the remaining
// block, for use in the dictionary
string block = (gridSize - x) + "x" + (gridSize - y);
// if a same block has not been processed before
if (!pathsByBlock.ContainsKey(block))
{
// calculate it in the right direction
if (x < gridSize)
i += progress(x + 1, y);
// and in the down direction
if (y < gridSize)
i += progress(x, y + 1);
// and cache the result!
pathsByBlock[block] = i;
}
// self-explanatory :)
return pathsByBlock[block];
}
调用20次,对于尺寸为1×1到20×20的网格,产生以下输出:
There are 2 paths in a 1 sized grid
0,0110006 seconds
There are 6 paths in a 2 sized grid
0,0030002 seconds
There are 20 paths in a 3 sized grid
0 seconds
There are 70 paths in a 4 sized grid
0 seconds
There are 252 paths in a 5 sized grid
0 seconds
There are 924 paths in a 6 sized grid
0 seconds
There are 3432 paths in a 7 sized grid
0 seconds
There are 12870 paths in a 8 sized grid
0,001 seconds
There are 48620 paths in a 9 sized grid
0,0010001 seconds
There are 184756 paths in a 10 sized grid
0,001 seconds
There are 705432 paths in a 11 sized grid
0 seconds
There are 2704156 paths in a 12 sized grid
0 seconds
There are 10400600 paths in a 13 sized grid
0,001 seconds
There are 40116600 paths in a 14 sized grid
0 seconds
There are 155117520 paths in a 15 sized grid
0 seconds
There are 601080390 paths in a 16 sized grid
0,0010001 seconds
There are 2333606220 paths in a 17 sized grid
0,001 seconds
There are 9075135300 paths in a 18 sized grid
0,001 seconds
There are 35345263800 paths in a 19 sized grid
0,001 seconds
There are 137846528820 paths in a 20 sized grid
0,0010001 seconds
0,0390022 seconds in total
我接受了danben的回答,因为他帮助我找到了这个解决方案。但也对Tim Goodman和Agos赞成:)
奖金更新:
在阅读了Eric Lippert的回答之后,我再次看了一下并重写了一下。基本的想法仍然是相同的,但缓存部分已被取出并放入一个单独的功能,如Eric的例子。结果是一些更优雅的代码。
// the size of our grid
const int gridSize = 20;
// magic.
static Func<A1, A2, R> Memoize<A1, A2, R>(this Func<A1, A2, R> f)
{
// Return a function which is f with caching.
var dictionary = new Dictionary<string, R>();
return (A1 a1, A2 a2) =>
{
R r;
string key = a1 + "x" + a2;
if (!dictionary.TryGetValue(key, out r))
{
// not in cache yet
r = f(a1, a2);
dictionary.Add(key, r);
}
return r;
};
}
// calculate the surface of the block to the finish line
static long calcsurface(long x, long y)
{
return (gridSize - x) * (gridSize - y);
}
// call using progress (0, 0)
static Func<long, long, long> progress = ((Func<long, long, long>)((long x, long y) =>
{
// first calculate the surface of the block remaining
long surface = calcsurface(x, y);
long i = 0;
// zero surface means only 1 path remains
// (we either go only right, or only down)
if (surface == 0)
return 1;
// calculate it in the right direction
if (x < gridSize)
i += progress(x + 1, y);
// and in the down direction
if (y < gridSize)
i += progress(x, y + 1);
// self-explanatory :)
return i;
})).Memoize();
顺便说一下,我想不出一个更好的方法来将这两个参数用作字典的关键字。我用Google搜索了一下,看来这是一个常见的解决方案。哦,好吧。
答案 0 :(得分:48)
快速无编程解决方案(基于组合学)
我认为“没有回溯”意味着我们总是增加x或增加y。
如果是这样,我们知道总共有40步可以达到终点 - x增加20,y增加20。
唯一的问题是40个中的哪个是x的20个增加。问题相当于:有多少种不同的方法可以从一组40个元素中选择20个元素。 (元素是:第1步,第2步等,我们选择的是x中增加的元素。)
有一个公式:它是二项式系数,顶部为40,底部为20。公式为40!/((20!)(40-20)!)
,换句话说40!/(20!)^2
。这里!
代表阶乘。 (例如,5! = 5*4*3*2*1
)
取消20个中的一个!并且40!的一部分,这变为:(40*39*38*37*36*35*34*33*32*31*30*29*28*27*26*25*24*23*22*21)/(20*19*18*17*16*15*14*13*12*11*10*9*8*7*6*5*4*3*2*1)
。因此问题简化为简单的算术。答案是137,846,528,820
。
为了进行比较,请注意(4*3)/(2*1)
给出了他们的示例6
的答案。
答案 1 :(得分:40)
如果你使用dynamic programming(存储子问题的结果而不是重新计算它们),这可以更快地完成。动态规划可以应用于表现出最佳子结构的问题 - 这意味着可以从子问题的最优解(信用Wikipedia)构建最优解。
我宁可不放弃答案,还要考虑右下角的路径数量与相邻方块的路径数量之间的关系。
另外 - 如果你打算用手工作,你会怎么做?
答案 2 :(得分:39)
正如其他人所指出的那样,这个特定问题有一个离散的数学解决方案。但是假设你确实想要递归地解决它。你的性能问题是你一遍又一遍地解决同样的问题。
让我向您展示一个稍微高阶的编程技巧,它将带来巨大的回报。让我们来看一个更简单的递归问题:
long Fib(n)
{
if (n < 2) return 1;
return Fib(n-1) + Fib(n-2);
}
你要求它计算Fib(5)。这计算Fib(4)和Fib(3)。计算Fib(4)计算Fib(3)和Fib(2)。计算Fib(3)计算Fib(2)和Fib(1)。计算Fib(2)计算Fib(1)和Fib(0)。现在我们回去再次计算Fib(2) 。然后我们回去再次计算Fib(3) 。大量的重新计算。
假设我们缓存了计算结果。然后第二次请求计算,我们只返回缓存的结果。现在是高阶技巧。我想把这个“缓存函数结果”的概念表示为一个函数,它接受一个函数,并返回一个具有这个不错属性的函数。我会把它写成函数的扩展方法:
static Func<A, R> Memoize(this Func<A, R> f)
{
// Return a function which is f with caching.
var dictionary = new Dictionary<A, R>();
return (A a)=>
{
R r;
if(!dictionary.TryGetValue(a, out r))
{ // cache miss
r = f(a);
dictionary.Add(a, r);
}
return r;
};
}
现在我们对Fib进行一些小的重写:
Func<long, long> Fib = null;
Fib = (long n) =>
{
if (n < 2) return 1;
return Fib(n-1) + Fib(n-2);
};
好的,我们有非记忆功能。现在,魔术:
Fib = Fib.Memoize();
繁荣,当我们调用Fib(5)时,现在我们进行字典查找。 5不在字典中,所以我们调用原始函数。这称为Fib(4),它执行另一个字典查找和未命中。这称为Fib(3),依此类推。当我们回到调用Fib(2)和Fib(3)第二时间时,结果已经在字典中,所以我们不会重新计算它们。
编写两个参数版本:
static Func<A1, A2, R> Memoize(this Func<A1, A2, R>) { ... }
不是太难,而是留作练习。如果你这样做,那么你可以采用你原来漂亮的递归逻辑,做一个简单的重写为lambda,然后说:
progress = progress.Memoize();
突然你的表现会增加,同时不会损失原始算法的可读性。
答案 3 :(得分:18)
虽然动态编程肯定是解决此类问题的正确方法,但这个特定实例显示了可以被利用的规律性。
您可以将问题视为排列多个“正确”和“向下”的问题,小心不要计算多次相同的安排。
例如,2号问题的解决方案(在问题中的图像中报告)可以这样看:
→→↓↓
→↓→↓
→↓↓→
↓→→↓
↓→↓→
↓↓→→
因此,对于任何一方的网格,您可以通过combinatorics找到解决方案:
from math import factorial
n = 20
print factorial(2*n)/(factorial(n)*factorial(n))
2n个!是安排的数量20→+ 20↓,而两个n!说明可以安排→和↓的相同方式。
答案 4 :(得分:5)
顺便说一句,你可以通过实现2x3与3x2相同的路径来提高你的性能。您的记忆功能似乎只考虑一个完全列x行的字符串。但是,您可以在记忆中包含2x3键和3x2键的总路径。
因此,当你记住4x2等时,它会自动用相同数量的路径填充2x4。这会缩短您的时间,因为您之前已经计算过该表面区域的所有路径,为什么要再次这样做呢?
答案 5 :(得分:4)
尽管动态编程看起来像是一种处理问题的有吸引力的方式(并使其成为一个有趣的编码挑战),但对数据结构的一些创造性思考有助于立即给出答案。
[其余部分基本上解释了为什么Tim Goodman的回答最好,因为某些“最佳”的价值] 如果我们有一个nXm网格,我们可以将每个有效的角到边路径表示为n + m位串,使用0或1表示“向下”。更多的想法让位于路线的确切数量是从N + M个项目中获取N个项目的方式的数量,这很方便地恰好是N上的标准简单组合M.
因此,对于任何N + M矩形,从左上角到右下角的可能路线数是(n + m)(n + m-1) .. 。(m + 1)/(n *(n-1)* ... 1)。
最快的程序是不需要非常存储的程序,在存储方式中使用很多,并且(理想情况下)具有封闭形式的答案。
答案 6 :(得分:3)
您实际上正在计算使用泰勒系列的闭合公式计算Catalan Numbers。
因此,计算解决方案的一个程序可以计算二项式系数,如果你没有BigInt类,那么这很难实现......
答案 7 :(得分:1)
您可以将计算时间减半,考虑到一旦将其减小到一个正方形,网格将是对称的。因此,只要您在X和Y方向上有相等的空间来遍历剩余,您就可以对增加的x行程和增加的y行程使用相同的计算。
话虽如此,我在python中做了这个,并做了大量的结果缓存以避免重新计算。
答案 8 :(得分:1)
解决方案沿着网格的NW到SE的对角线反射。所以,你应该只计算网格右上半部分的解决方案,然后反映它们以获得另一半......
答案 9 :(得分:1)
我相信一些高中数学在这里很有用,这个链接解释了所需的组合公式:
http://mathworld.wolfram.com/Combination.html
现在,使用它来查找通过方格的路径数,公式变为2n选择n。 作为警告,您将需要使用可以容纳相当大数量的数据类型
答案 10 :(得分:1)
问题比许多人想要的要简单得多。路径必须是具有20个“权限”和20个“下降”的序列。不同序列的数量是您可以从可能的40个中选择(比如说)“权利”的20个职位的方式。
答案 11 :(得分:0)
每个人都表示动态编程和缓存结果。 在某个地方,我有一个Ruby脚本,最后有一个非常大的哈希,其中存储了所有数据。事实上,像大多数欧拉项目问题一样,这是一个隐藏的数学'技巧',并且有一些方法可以通过简单的计算得到结果。
答案 12 :(得分:0)
我的解决方案非常容易理解:
网格上给定交叉路口的路线数量是到两个邻居的路线数量的总和。
鉴于只有一条路线到达顶部和左边的每个点,很容易迭代剩余的点并填充空白。
对于x或y = 0:grid [x,y] = 1
对于x和y&gt; = 1:grid [x,y] = grid [x-1,y] + grid [x ,y-1]
因此,在遍历所有方块后,最终答案包含在网格[20,20]中。
答案 13 :(得分:0)
这可以通过n个选择k个组合来完成。如果您查看问题,则无论选择从起始单元格到目标单元格的路径,水平步长和垂直步长都将相同。
例如,以2 * 2的网格为例,此处水平步长为2,垂直步长为2,到达底部。
使用仿生咖啡,(a + b) 一个
a和be是水平和垂直步长。
static BigInteger getLatticePath(int m, int n) {
int totalSteps = m + n;
BigInteger result = Factorial.getFactorial(totalSteps)
.divide((Factorial.getFactorial(m).multiply(Factorial.getFactorial(totalSteps - m))));
return result;
}
public static BigInteger getFactorial(int n) {
BigInteger result = BigInteger.ONE;
for (int i = 2; i <= n; i++)
result = result.multiply(BigInteger.valueOf(i));
return result;
}