Project Euler 第15题
文章目录
Project Euler
最近在刷Project Euler的题目, 非常有意思, 都是一些有着简短描述的数学题目, 而且提交答案只需要最终结果, 所以不限语言, 不限方法, 尤其适合对数学和编程感兴趣的人。对它不了解的人可以在维基的这个页面查看相关介绍, 而且国内已经有人做了一个中文翻译站, 有时间的时候刷一刷还挺有意思的。
第15题
进展一直很顺利, 但是到第15题的时候发现提交答案的人数一下少了很多, 心想看来这道题目有点难度啊。尝试了一下, 第一次的算法可行, 但是经过测算需要几千小时。。。。。。继续改进, 还是太慢, 最后, 终于达到了毫秒级别!所以就感觉这个思考的过程还是挺有意思的。 从一个2×2网格的左上角开始, 有6条(不允许往回走)通往右下角的路。
对于20×20的网格, 这样的路有多少条?
思路过程
路径查找
|
|
第一条路径
首先结合上面的图观察它的坐标变化:
从起始点(0,0)
开始, 向右有(0,1)
和向下有(1,0)
, 两个方向都可以;
而图中第一条路径选择向右, 因此我们可以记录路径为(0,0) → (0,1)
, 可选的坐标列表为(1,0)
;
此时向右有(0,2)
和向下有(1,1)
, 两个方向都可以;
结合图中的第一条路径, 我们还是选择向右, 因此记录路径为(0,0) → (0,1) → (0,2)
, 可选的坐标列表变为(1,0), (1,1)
;
此时向右没有坐标, 向下有(1,2)
, 和图中第一条路径相符, 因此记录路径为(0,0) → (0,1) → (0,2) → (1,2)
;
同上, 向右没有坐标, 向下有(2,2)
, 和图中第一条路径相符, 因此记录路径为(0,0) → (0,1) → (0,2) → (1,2) → (2,2)
;
第二条路径
很明显, 对于(1,2)
和(2,2)
都是因为向下没有坐标了, 因此要寻找新的路线肯定要回退到有两个方向可选的那个坐标, 然后再继续这个过程。
第一条路径为(0,0) → (0,1) → (0,2) → (1,2) → (2,2)
, 记录下来的可选坐标列表为(1,0), (1,1)
;
路径中最后一个出现两个方向的坐标为(0,1)
, 因此需要回退到这里, 路径变为(0,0) → (0,1)
;
从可选坐标列表中取出最后一个坐标作为新的方向, 路径变为(0,0) → (0,1) → (1,1)
, 可选的坐标列表为(1,0)
;
此时向下有(2,1)
, 则可选的坐标列表变为(1,0), (2,1)
, 向右有(1,2)
, 则路径变为(0,0) → (0,1) → (1,1) → (1,2)
;
向右没有坐标, 向下有(2,2)
, 和图中第二条路径相符, 因此记录路径为(0,0) → (0,1) → (1,1) → (1,2) → (2,2)
;
第三条路径
同样的回退策略, 回退到坐标(1,1)
, 再使用同样的前进策略, 得到路径(0,0) → (0,1) → (1,1) → (2,1) → (2,2)
第四、五、六条路径
同样的回退和前进策略, 即可得到第四、五、六条路径
代码实现
根据以上的前进、回退策略, 结合栈即可实现如下的代码
|
|
运行以上代码即可得到如下六条2x2
网格的路径
|
|
计算次数-递归方法
虽然以上代码可以求得nxn
网格的路径, 但是换到20x20
时它复杂的入栈出栈操作会拖累整个运行时间, 使用这个办法求解次数显然是不行的, 只能继续查找一些规律了。
|
|
画出一个3x3
的网格来, 想着2x2
的时候从起始点(0,0)
出发, 首先选择向右有三条路径, 然后3*2=6
, 同理, 对于3x3
的网格来说, 总共有20条路径, 从起始点出发, 选择向右则有10条路径, 那么这十条路线是如何得到的呢?
从(0,0)
出发, 只考虑向右的(0,1)
这个方向, 因为向下向右两个方向对称, 所以20/2=10
;
而对于(0,1)
来说, 它也有两个方向(1,1)
和(0,2)
, 则可知这十条路径即为在这两个方向产生的;
对于(1,1)
来说, 它到底终点(3,3)
其实即为一个2x2
网格, 因此这个方向有6条路径, 那么(0,2)
将贡献4条路径;
经过验证, 对于3x1
网格来说确实有4条路径;
由此我们即可得到: f(3, 3) = [f(2, 2) + f(3,1)]*2
进而进行推广: f(n, n) = [f(n-1, n-1) + f(n, n-2)]*2
显然, 有f(1, 1) = 2
和f(n, 0) = 1
于是就可以采用递归方式来实现了
代码实现
|
|
但是很不幸, 虽然这种实现并没用记录具体的路径, 但是递归的次数还是太多, 而且在计算n*m
网格时存在太多的重复计算导致仍然会消耗比较多的时间, 因此如果能记录下已经计算出结果的n*m
, 那么速度将会大大提高。
计算次数-循环方法
根据f(3, 3) = [f(2, 2) + f(3, 1)]*2
, 可知问题出在f(3, 1)
需要计算, 同理对于f(4, 4) = [f(3, 3) + f(4, 2)]*2
, 其中f(4, 2)
也需要计算, 继续按照分解f(3, 3)
到f(2, 2)
的过程, 可以得到:
f(4, 2) = f(3, 2) + f(4, 1)
f(3, 2) = f(2, 2) + f(3, 1)
于是推得公式: f(n, m) = f(n-1, m) + f(n, m-1)
根据这个公式可以发现一些比较有意思的事情, 比如当m = 1
时有f(n, 1) = f(n-1, 1) + f(n, 0)
, 而f(n, 0) = 1
, 所以有f(n, 1) = f(n-1, 1) + 1
, 同理对f(n-1, 1)
展开, 得到f(n, 1) = f(n-2, 1) + 2
, 这样最后将得到f(n, 1) = n+1
。
同样的推理过程, f(n, 2) = f(n-1, 2) + f(n, 1)
即为f(n, 2) = f(n-1, 2) + (n+1)
, 这样即可得知:
f(n, 1)
的值是一个等差数列, f(n, 2)
前项减后项的值是一个等差数列, 进而推广到f(n, 3)
需要做两次差才能得到等差数列。
取n = 4
进行验证:
|
|
对f(n, 3)
进行验证, 前项减后项, 做两次即可得到等差数列, 进而即可发现:
求解f(3, 3)
时, 由f(3, 3) = [f(2, 2) + f(3, 1)]*2
可以得到
求解f(4, 4)
时, 由f(4, 4) = [f(3, 3) + f(4, 2)]*2
可以得到
因此对于f(n, n)
来说, 使用公式f(n, n) = [f(n-1, n-1) + f(n, n-2)]
降维后可由前面的值组合而成, 因此即可得到如下求解过程:
要求f(3, 3)
, 其中f(n, 0) = 1, f(n, 1) = n+1
为已知项, 进而可得到如下推导过程:
f(2, 2) = [f(1, 1) + f(2, 0)]*2 = (2+1)*2 = 6
f(3, 2) = f(2, 2) + f(3, 1) = 6+4 = 10
f(3, 3) = [f(2, 2) + f(3, 1)]*2 = (6+4)*2 = 20
对f(4, 4)
进行验证, 仍然可以使用此方法得到正确结果, 因此对于f(n, n)
可以依次求解f(n, m), m从 0 到 n
来求解。
代码实现
|
|
使用此方法求解时间即可达到毫秒级别。
终极解法
提交答案后可以查看其他人提交的解法, 这才知道原来这个问题求解的是卡塔兰数, 看题目给出的2x2
网格的路径图, 从起点到终点共有四步, 这四步中必然有横着的两步和竖着的两步, 所以总路径数其实就是从四步中挑选两横两竖的组合有多少种, 因此这就转化成了一个排列组合问题!
而对于排列组合从2n中选择n个
的求解有公式(2n)!/(n!)^2
, 因此这个问题就可以直接套用公式来求解了!