我目前正在研究项目欧拉问题(www.projecteuler.net),但它有点绊脚石。其中一个问题是提供20x20的数字网格,并要求直线上4个数字的最大乘积。该线可以是水平线,垂直线或对角线。
使用程序语言我没有解决这个问题,但我首先要解决这些问题的部分动机是获得更多经验并学习更多Haskell。
截至目前,我正在网格中读取并将其转换为整数列表列表,例如 - [[Int]]。这使得水平乘法变得微不足道,并且通过对该网格进行转置,垂直也变得微不足道。
对角线是给我带来麻烦的。我想到了一些方法,我可以使用显式数组切片或索引,以获得一个解决方案,但它似乎过于复杂和hackey。我相信这里可能有一个优雅,实用的解决方案,而且我很想听听别人能想出什么。
答案 0 :(得分:10)
我不同意可敬的唐斯图尔特。鉴于问题的组合性质以及问题大小仅为20x20的事实,列表列表将足够快。你需要的最后的东西是使用数组索引来实现的。相反,我建议你扩展理查德伯德在其着名的sudoku solver中开发的技术。更具体地说,我建议如下:
编写一个给定序列的函数,返回长度为4的所有连续子序列。
编写一个给定网格的函数,返回所有行。
编写一个给定网格的函数,返回所有列。
编写一个给定网格的函数,返回所有对角线。
掌握这些功能后,您的解决方案将变得简单。但正如你所提到的,对角线并不那么明显。什么是对角线呢? 我们来看一个例子:
X . . . . .
. X . . . .
. . X . . .
. . . X . .
. . . . X .
. . . . . X
假设您使用drop
函数并从第0行删除0个元素,从第1行删除1个元素,依此类推。这就是你最后的结果:
X . . . . .
X . . . .
X . . .
X . .
X .
X
对角线的元素现在形成了你留下的三角形的第一列。更好的是,你剩下的东西的每个列都是原始矩阵的对角线。抛出一些对称变换,你就可以轻松地枚举所有任意大小的方阵的对角线。用你的“长度为4的连续子序列”来解释每一个,鲍勃是你的叔叔!
对于那些可能被卡住的人来说更详细一点:
此问题的关键是组合。对角线分为四组。我的例子给出了一组。要获得其他三个,请将相同的功能应用于转置的镜像,转置和镜像。
Transpose是一个单行函数,无论如何都需要它来干净地恢复列。
镜像比转置更简单 - 想想你可以在Prelude中使用哪些功能。
对称方法将给出每个主要对角线两次;幸运的是,问题表明重复对角线是可以的。
答案 1 :(得分:2)
列表是这个问题的错误数据结构,因为它们不会在恒定时间内提供随机索引 - 它们偏向于线性遍历。因此,对于列表,您的对角线将总是更加烦人/更慢。
如何使用数组?例如。 parallel vectors或regular vectors。
答案 2 :(得分:2)
嗯,对于这个特殊问题,单个线性列表或数组实际上是最简单的结构!关键是要考虑将这些运行视为以给定的步幅跳过列表。如果网格的大小为 w × h ,那么
现在,对于四种运行中的每一种,您只需要计算可能的起点。像这样:
allRuns :: Int -> Int -> Int -> [a] -> [[a]]
allRuns n w h es = horiz ++ vert ++ acute ++ grave
where horiz = runs [0..w-n] [0..h-1] 1
vert = runs [0..w-1] [0..h-n] w
acute = runs [n-1..w-1] [0..h-n] (w-1)
grave = runs [0..w-n] [0..h-n] (w+1)
runs xs ys s = [run (x+y*w) s | x <- xs, y <- ys]
run i s = map (es!!) [i,i+s..i+(n-1)*s]
当然,在有效的实施中,您将[a]
替换为Data.Array Int a
和es!!
es!
答案 3 :(得分:1)
您可以使用!!
函数按索引检索列表中的元素。通过递增或递减索引的固定步骤可以获得对角线。
答案 4 :(得分:1)
所以你有一个NxN网格,你想要提取长度为M的所有水平,垂直和对角线,然后找到最大的产品。让我们举例说明一些示例4x4网格上的Haskell技术,线长为2:
[[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9,10,11,12],
[13,14,15,16]]
水平和垂直很容易,你只需要一个从列表中提取长度为M的块的函数:
chunks 2 [1,2,3,4] == [[1,2],[2,3],[3,4]]
此类函数的类型为[a] -> [[a]]
。这是一个与列表相关的功能,所以在重新发明轮子之前,让我们看看Data.List中是否有类似的东西。啊哈,tails
是相似的,它会从列表开头删除包含越来越多元素的列表:
tails [1,2,3,4] == [[1,2,3,4],[2,3,4],[3,4],[4],[]]
如果我们只能缩短子列表以使其长度为2.但我们可以通过使用map
函数将函数应用于列表的每个元素并返回一个新列表:
map (take n) (tails xs) -- [[1,2],[2,3],[3,4],[4],[]]
我不担心较小的线条,因为最初的任务是找到最大的产品,[15, N]
≥[15]
的乘积,N≥1的乘积。但是如果你想要的话为了摆脱它们,似乎长度为N的列表包含长度为M的N-M + 1个块,因此您可以将take (4-2+1)
应用于结果列表。或者,你可以简单地filter列表:
chunks n xs = filter ((==n) . length) $ map (take n) (tails xs)
-- [[1,2],[2,3],[3,4]]
好的,我们可以从列表中提取一个块列表,但是我们有一个2D网格,而不是一个平面列表! map
再次救我们:
map (chunks 2) grid -- [[[1,2],[2,3],[3,4]],[[5,6],[6,7],[7,8]],...]
但事实上,由此产生的代码将块放在单独的列表中,这使得事情复杂化,因为我们实际上并不关心,块从哪一行开始。因此,我们希望按concat . map
或等效concatMap
将结果列表展平为一级:
concatMap (chunks 2) grid -- [[1,2],[2,3],[3,4],[5,6],[6,7],[7,8],...]
现在,如何从网格中获取垂直块?听起来很可怕,直到你意识到你可以transpose整个网格,即将行和列转换成行,然后应用相同的代码:
concatMap (chunks 2) (transpose grid) -- [[1,5],[5,9],[9,13],[2,6],[6,10],...]
现在困难的部分:对角线。 Norman Ramsey给出了一个想法:如果你可以从第0行删除0个元素,从第1行删除1个元素,等等?对角线将成为垂直线,易于提取。您记得要将函数应用于使用map
的列表的每个元素,但是在这里您需要对每个元素应用不同的函数,即drop 0
,drop 1
,{{1等等drop 2
不适合。但是看,map
的第一个参数形成了连续数字的模式,可以表示为无限列表drop
。现在,如果我们可以从[0..]
中获取一个元素,我们需要的是一个从无限列表[0..]
中取一个数字并从网格中取一行的函数,并将[0..]
与此数字一起应用到了那一排。 zipWith
就是您所需要的:
drop
但我想要所有长度为2的对角线,而不仅仅是最大的对角线。那么看一下网格并想一想,你看到第0行的元素有哪些对角线? zipWith drop [0..] grid -- [[1,2,3,4],[6,7,8],[11,12],[16]]
map head $ zipWith drop [0..] grid -- [1,6,11,16]
。所以很明显,你只需要前两行并转置元素:
[1,6],[2,7],[3,8]
现在如何从其他行开始获取对角线?还记得我们的transpose $ zipWith drop [0,1] grid -- [[1,6],[2,7],[3,8],[4]]
诀窍吗?我们可以通过向tails
提供新功能并将其应用于concatMap
来获取所有对角线:
tails grid
但这些只是从左上角到右下角的对角线。那些从右上角到左下角的是什么?最简单的方法就是反转网格的行:
concatMap (transpose . zipWith drop [0,1]) (tails g)
-- [[1,6],[2,7],[3,8],[5,10],[6,11],...]
最后,您需要查找所有行的产品并选择最大的产品。最终代码如下:
concatMap (transpose . zipWith drop [0,1]) (tails $ reverse g)
-- [[13,10],[14,11],[15,12],[9,6],[10,7],...]
这段代码是否最优雅高效?没有,但它起作用并说明了函数式编程的某些思维模式。首先,你需要编写正常工作的代码,然后迭代重构它,直到你得到一个易于阅读和一般的解决方案。