Ruby - Project Euler#18 - 最大路径总和

时间:2014-11-12 17:46:08

标签: ruby max-path

我一直在研究一些Project Euler问题,而且在大多数情况下,我一直都做得很好。问题18虽然让我很难过。

从树的顶部开始,我应该找到导致最大总和的路径

      3
    7   4
  2   4   6
8   5   9   3

在这种情况下,有24条可能的路径,或4条!最好的路径是 3 - > 7 - > 4 - > 9 ,总计达到23。

我尝试通过复制示例来解决问题。

array = [[3],[7,4],[2,4,6],[8,5,9,3]]
array.each_slice(1){|s|p s}                          => This prints the tree

我得到了一个答案,在极少数情况下这是正确的,但这并不合法。

sum = []
array.each{|a|sum.push(a.sample)}
return sum

这种方法基本上只是选择一个随机路径,即使有这个简单的例子,仍然只有十分之一的机会才能做到正确。

我尝试过像

这样的事情
level_two = []
level_three = []
for i in 0..array.length-1
  for j in 0..array[1].length-1
    level_two.push([array[0], array[1][i])        => Now level 2 has 2 paths - [3,7] & [3,4]
    for k in 0..array[2].length-1
      level_three.push([level_two[0],array[2][k], [level_two[1], array[2][k])
    end
  end
end

但此时我甚至无法追踪我正在使用的变量。

我尝试过each_cons,组合和zip,但没有一个有效。

有谁知道解决这个问题的策略?

编辑:这不会给出问题的答案,而只是答案示例。然后,我将在主要问题上应用该设计模式。

4 个答案:

答案 0 :(得分:2)

此问题是递归的完美用例。对于术语,让我们说T[3][0]是从0开始计算的第3行中的第0个元素(在您的示例中为8)。

从树的顶部开始,导致最大总和的路径是什么?首先,让我们稍微改写一下:从T[0][0]开始的最大总和的路径是什么?那么T[0][0]加上从T[1][0]T[1][1]开始的路径(无论哪条路径更大)。寻找路径本身是偶然找到最大的总和,所以我们只关注它。

让我们说S[x][y]是从T[x][y]开始的路径的最大总和。然后从之前的观察我们得到

S[x][y] = T[x][y] + MAX(S[x + 1][y], S[x + 1][y + 1])

除非x是树的最后一行。让R成为树的最后一行,在这种情况下

S[R][y] = T[R][y]

在Ruby中,我们可以编写如下函数:

def largest_sum_path(tree, x = 0, y = 0)
  path = [tree[x][y]]

  return path if x == tree.length - 1

  left  = largest_sum_path(tree, x + 1, y)
  right = largest_sum_path(tree, x + 1, y + 1)

  return path.concat([left, right].max_by { |p| p.reduce(:+) })
end

然而,我怀疑这种方法对于大树来说非常慢。我会留给您看看如何优化它。

答案 1 :(得分:2)

对于这些类型的问题,我的第一步是理解数组索引之间的关系。给定此树的 n 级别的数组,数组级别 n + 1 的子元素可以是索引 i 的父元素(在数组中)级别 n )访问?答案是 i i + 1 的元素。

例如:设[2, 4, 6]2级别的数组,[8, 5, 9, 3]为级别3的数组。元素6(索引2)访问哪些子元素?可以访问9, 3,因为它们的索引为2, 3

现在我们可以为这个问题创建一个递归解决方案:

@res = [] # All paths will be stored here
def paths(arr, temp, lvl = 0, idx = 0)
  if arr[lvl] # we haven't hit end of path
    # loop through elements at i and i + 1
    arr[lvl][idx..idx+1].each_with_index do |x, i|
      # go one level deeper into tree
      paths(arr, temp.dup << x, lvl + 1, i)
    end
  else
    @res << temp # `temp` has complete path, push it into results
  end
end

paths(array, []) # initialize temporary path to empty array

@res.map { |x| x.reduce(:+) }.max # => 23

答案 2 :(得分:1)

从最后一行开始。将其替换为由每个连续对的最大值组成的行,因此8 5 9 3 - &gt; 8 9 9。将这些值添加到其上方的行中(2 4 6 - > 10 13 15)。重复直到第一行。(10 13 15 - &gt; 13 15,7 4→20 19 - > 20; 3 - &gt; 23.

答案 3 :(得分:1)

我就是这样做的。

<强>代码

def longest_path(arr)
  return nil if arr.empty?

  h = { len: arr.first.first, path: [] }
  return h if arr.size == 1

  arr[1..-1].reduce([h]) do |l,row|
    h = l.first
    left = { len: h[:len]+row.first, path: h[:path]+[:l] }
    mid = l.each_cons(2).to_a.zip(row[1..-2]).map do |(h1,h2),v|
            if h1[:len] >= h2[:len]
              { len: h1[:len]+v, path: h1[:path]+[:r] }
            else
              { len: h2[:len]+v, path: h2[:path]+[:l] }
            end
          end
    h = l.last
    right = { len: h[:len]+row.last, path: h[:path]+[:r] }
    [left, *mid, right]
  end.max_by { |h| h[:len] }
end

示例

a = [   [3],
       [7,4],
      [2,4,6],
     [8,5,9,3]]

longest_path a
  #=> {:len=>23, :path=>[:l, :r, :r]}

因此,最长路径的长度为23.从第一行中的3,第二行中的向下和向左(:l)到7,向下和向右({{ 1}})至第三行中的:r,以及最后一行中4的向下和向右:9

<强>解释

这个问题同样是算法的实现作为算法的选择。在我看来,后者是相当明显的:求解一行,用它来解决两行,依此类推。

考虑上面的示例数组3+7+4+9 => 23

a

作为

arr = a
arr.empty? #=> false, so continue

h = { len: arr.first.first, path: [] }
  #=> {:len=>3, :path=>[]}
return h if arr.size == 1 # arr.size => 4, so continue

arr[1..-1] => [[7, 4], [2, 4, 6], [8, 5, 9, 3]] reduce[h]传递到块中并分配块变量:

[7, 4]

然后计算:

l   = [{ len: arr.first.first, path: [] }]
row = [7, 4]

h = l.first #=> {:len=>3, :path=>[]} left = { len: h[:len]+row.first, path: h[:path]+[:l] } #=> {:len=>10, :path=>[:l]} mid = [] h = l.last #=> {:len=>3, :path=>[]} right = { len: h[:len]+row.last, path: h[:path]+[:r] } #=> {:len=>7, :path=>[:r]} [left, *mid, right] #=> [{:len=>10, :path=>[:l]}, {:len=>7, :path=>[:r]}] 因为mid => []在大小为1的数组上执行。

上面的最后一行提供了第二行中两个元素中每个元素的最长路径的信息。对于第一个元素(each_cons(2)),路径长度为7,并且从第一行中的唯一元素(10)开始,然后向下和“向左”({{1 }})给定的元素。

3块的最后一行计算:l时,块变量[left, *mid, right]被赋予该值以处理下一行reduce

l

接下来我们计算以下内容:

arr

l = [{:len=>10, :path=>[:l]}, {:len=>7, :path=>[:r]}] row = [2, 4, 6] left = { len: h[:len]+row.first, path: h[:path]+[:l] } #=> {:len=>5, :path=>[:l]} l.each_cons(2).to_a.zip(row[1..-2]).map do |(h1,h2),v| if h1[:len] >= h2[:len] { len: h1[:len]+v, path: h1[:path]+[:r] } else { len: h2[:len]+v, path: h2[:path]+[:l] } end end #=> [{:len=>14, :path=>[:l, :r]}] h = l.last #=> {:len=>7, :path=>[:r]} right = { len: h[:len]+row.last, path: h[:path]+[:r] } #=> {:len=>13, :path=>[:r, :r]} [left, *mid, right] #=> [{:len=>5, :path=>[:l]}, {:len=>14, :path=>[:l, :r]}, # {:len=>13, :path=>[:r, :r]}] 的计算与前一个left元素的计算类似。我们来看看right的计算:

arr

mid是一个包含一个元素的数组。该元素被传递到pairs = l.each_cons(2).to_a #=> [[{:len=>10, :path=>[:l]}, {:len=>7, :path=>[:r]}]] vals = pairs.zip(row[1..-2]) #=> pairs.zip([4]) #=> [[[{:len=>10, :path=>[:l]}, {:len=>7, :path=>[:r]}], 4]] ,被分解并分配给块变量:

vals

作为map,我们执行:

h1       = {:len=>10, :path=>[:l]}
h2       = {:len=> 7, :path=>[:r]}
v        = 4
h1[:len] #=> 10
h2[:len] #=> 7

这是10 > 7的值。 { len: h1[:len]+v, path: h1[:path]+[:r] } 的块值mid现已分配l的结果:

reduce

开始处理第三行。 [left, *mid, right]返回:

l = [{:len=> 5, :path=>[:l]}, {:len=>14, :path=>[:l, :r]},
     {:len=>13, :path=>[:r, :r]}]

提供描述最后一行每个元素的最长路径的信息。最后一步是:

reduce