【发布时间】:2022-03-25 06:07:49
【问题描述】:
昨晚我试图解决challenge #15 from Project Euler:
从左上角开始 2×2网格,有6条路线(不含 回溯)到右下角 角落。
(来源:projecteuler.net)有多少条路线通过 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 块之前获得的结果。这是代码以及一些 cmets:
// 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
我接受丹本的回答,因为他最能帮助我找到这个解决方案。但也赞成 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();
顺便说一句,我想不出更好的方法来使用这两个参数作为字典的键。我用谷歌搜索了一下,似乎这是一个常见的解决方案。哦,好吧。
【问题讨论】:
-
如果你愿意,你可以用离散数学而不是编程来解决它。前方剧透。在 2x2 广场上,需要 4 步才能到达终点。所以在 20x20 上需要 40 步。您可以使用 n 选择 r 公式来解决它:n!/r!(n-r)! r = 行数,即 20,n 为 2r,即 40。可以简化为 40!/20!^2。如果有人有兴趣看看这里:en.wikipedia.org/wiki/Binomial_coefficient
-
@RBarryYoung:从给出的示例中,“回溯”似乎意味着从 x 或 y 坐标中减去任何内容。如果不是这个例子,我会认为这只是直接越过你的路径。即,我会假设
x++, y++, x--, y++, x++, x++是一个有效的解决方案,但他们不包括这样的解决方案的事实说明了其他情况。 -
@Tim Goodman:所以我也会假设,但我讨厌做出这样的假设。
-
有多种方法可以为字典使用两个参数。您的方法的问题在于,并非所有类型都“往返”到字符串。大多数类型实际上只是返回它们的类型名称作为它们的字符串表示。两个很好的技术:(1)您可以使用“元组”来保留两个元素,然后将其用作字典键。 (2) 制作一个 Dictionary
>。也就是说,外部字典以第一个键为键。它只为键在第二个键上的第一个键返回一个字典。第二种技术往往速度较慢。 -
缓存路径长度的好主意。