项目欧拉#15

时间:2010-02-04 14:14:31

标签: c# math

昨晚我试图解决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搜索了一下,看来这是一个常见的解决方案。哦,好吧。

14 个答案:

答案 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;
    }

https://www.quora.com/How-do-you-count-all-the-paths-from-the-first-element-to-the-last-element-in-a-2d-array-knowing-you-can-only-move-right-or-down引用