代码之家  ›  专栏  ›  技术社区  ›  Aistina

欧拉计划#15

  •  43
  • Aistina  · 技术社区  · 15 年前

    昨晚我想解决这个问题 challenge #15 from Project Euler :

    从a的左上角开始 回溯)到右下角 角落。

    alt text
    projecteuler.net )

    2020电网?

    我觉得这应该不难,所以我写了一个基本的递归函数:

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

    很明显,我走错了方向。你将如何解决这个问题?我认为应该用一个方程来解决,而不是像我这样的方法,但不幸的是,这不是我的优点。

    更新 :

    我现在有了一个工作版本。基本上,当nm块仍然需要遍历时,它会缓存以前获得的结果。以下是代码和一些注释:

    // 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次,对于大小为11到2020的网格,将产生以下输出:

    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
    

    我接受丹本的回答,因为他的回答最能帮助我找到这个解决方案。但投票也给了蒂姆·古德曼和阿戈斯:)

    奖金更新 :

    // 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();
    

    顺便说一下,我想不出更好的方法来使用这两个参数作为字典的键。我在谷歌上搜索了一下,似乎这是一个常见的解决方案。哦,好吧。

    13 回复  |  直到 6 年前
        1
  •  43
  •   danben    15 年前

    如果您使用 dynamic programming Wikipedia ).

    我宁愿不给出答案,而是考虑到右下角的路径的数量可能与相邻方格的路径数有关。

        2
  •  52
  •   Tim Goodman    15 年前

    快速无编程解决方案 (基于组合数学)

    我认为“无回溯”意味着我们要么增加x,要么增加y。

    唯一的问题是,这40项中的哪一项是x的20项增加。问题是:在一组40个元素中,有多少种不同的方法可以选择20个元素(这些元素是:步骤1,步骤2,等等,我们正在选择,比如说,在x中增加的元素。

    这里有一个公式:它是一个二项系数,上面是40,下面是20。公式是 40!/((20!)(40-20)!) 40!/(20!)^2 . 在这里 ! 5! = 5*4*3*2*1 )

    (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 .

        3
  •  43
  •   Eric Lippert    15 年前

    正如其他人所指出的,这个特殊问题有一个离散的数学解决方案。但假设您确实想要递归地解决它。你的表现问题在于你一次又一次地解决同样的问题。

    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();
    

        4
  •  19
  •   Agos    15 年前

    虽然动态规划无疑是解决这类问题的正确方法,但这个特定实例显示了一种可以利用的规律性。


    例如,尺寸2问题的解决方案(在问题中的图像中报告)可以通过以下方式查看:

    →→↓↓  
    →↓→↓
    →↓↓→
    ↓→→↓
    ↓→↓→
    ↓↓→→
    

    combinatorics :

    from math import factorial
    n = 20
    print factorial(2*n)/(factorial(n)*factorial(n))
    

    2n!是20+20的排列数,而两个n!说明和的排列方式相同。

        5
  •  5
  •   Blaine Mucklow    15 年前

    顺便说一句,您可以通过意识到2x3将具有与3x2相同的路径数量来进一步提高性能。您的记忆函数似乎只考虑正好是列x行的字符串。但是,您可以在记忆中包含2x3和3x2键的总路径。

        6
  •  4
  •   Vatine    15 年前

    尽管动态编程看起来是解决这个问题的一种很有吸引力的方法(并使其成为一种有趣的编码挑战),但对数据结构的一点创造性思考有助于立即给出答案。


    如果我们有一个nXm网格,我们可以将每个有效的角到角路由表示为一个n+m位字符串,使用0或1表示“向下”。再仔细考虑一下,我们手头上的确切路由数就是从N+M项中提取N项的方法数,这恰好是标准的简单组合M除以N。

    …(m+1)/(n*(n-1)*。。。1).

        7
  •  3
  •   Alexandre C.    15 年前

    Catalan Numbers 其中,使用泰勒级数的闭合公式可用。

    因此,一个计算解决方案的程序可以计算二项式系数,如果你没有BigInt类,这很难得到正确的结果。。。

        8
  •  1
  •   StrixVaria    15 年前

    考虑到这一点,可以将计算时间减半,一旦将其减少为正方形,网格将是对称的。因此,无论何时,只要在X和Y方向上有相等的空间来遍历剩余空间,就可以对增加的X行程和增加的Y行程使用相同的计算。

        9
  •  1
  •   Scott Smith    15 年前

        10
  •  1
  •   ajnatural    15 年前

    http://mathworld.wolfram.com/Combination.html

    现在,用它来计算通过正方形网格的路径数,公式变成2n选择n。

        11
  •  1
  •   Danny Duberstein    9 年前

    这个问题比许多人想象的要简单得多。路径必须是具有20个“权限”和20个“向下”的序列。不同序列的数量是指你可以从可能的40个位置中选择20个位置(比如“权利”)。

        12
  •  0
  •   Trevoke    15 年前

    每个人都表示动态编程和缓存结果。 我在某个地方有一个Ruby脚本,结果是所有数据都存储在一个非常大的散列中。事实上,就像大多数Euler项目问题一样,这是一个隐藏的数学“把戏”,有很多方法可以通过简单的计算得到结果。

        13
  •  0
  •   Phil Gan    15 年前

    我的解决方案是否定的,但很容易理解:

    网格上到给定交叉口的路线数是到其两个相邻交叉口的路线数之和。


        14
  •  0
  •   Prabjot Singh    5 年前

    这可以通过n个选择k个组合来完成。如果你看看这个问题,无论你选择从起始单元格到目标单元格的路径是什么,水平和垂直步数都是相同的。

    使用生物棺材(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