在水平,垂直和对角线上乘以数字

时间:2010-05-07 23:25:42

标签: haskell functional-programming

我目前正在研究项目欧拉问题(www.projecteuler.net),但它有点绊脚石。其中一个问题是提供20x20的数字网格,并要求直线上4个数字的最大乘积。该线可以是水平线,垂直线或对角线。

使用程序语言我没有解决这个问题,但我首先要解决这些问题的部分动机是获得更多经验并学习更多Haskell。
截至目前,我正在网格中读取并将其转换为整数列表列表,例如 - [[Int]]。这使得水平乘法变得微不足道,并且通过对该网格进行转置,垂直也变得微不足道。

对角线是给我带来麻烦的。我想到了一些方法,我可以使用显式数组切片或索引,以获得一个解决方案,但它似乎过于复杂和hackey。我相信这里可能有一个优雅,实用的解决方案,而且我很想听听别人能想出什么。

5 个答案:

答案 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 vectorsregular vectors

答案 2 :(得分:2)

嗯,对于这个特殊问题,单个线性列表或数组实际上是最简单的结构!关键是要考虑将这些运行视为以给定的步幅跳过列表。如果网格的大小为 w × h ,那么

  • 横向跑步的步幅 1
  • 纵向跑步有 w
  • 的步幅
  • 一个对角线跑步的步幅为 w-1
  • 一个对角线跑步的步幅为 w + 1

现在,对于四种运行中的每一种,您只需要计算可能的起点。像这样:

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 aes!! 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 0drop 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],...]

这段代码是否最优雅高效?没有,但它起作用并说明了函数式编程的某些思维模式。首先,你需要编写正常工作的代码,然后迭代重构它,直到你得到一个易于阅读和一般的解决方案。