【问题标题】:Improvement of the Greedy Algorithm贪心算法的改进
【发布时间】:2013-01-20 14:29:41
【问题描述】:

我一直在研究使用 Haskell 的抽象国际象棋算法(试图扩展我对不同范式的理解),并且遇到了一个我已经思考了数周的挑战。

问题来了:

给定一个棋盘(由整数列表表示;每个 整数表示后续点值),维度为 n x n, 确定提供最多点的路径。如果有领带 要获得最佳路径,请返回其中任何一个。

具体如下:

A = [[5,4,3,1],[10,2,1,0],[0,1,2,0],[2,3,4,20]] 

呈现为:

R1: 5  4  3  1, R2: 10 2 1 0, R3: 0 1 2 0, R4: 2 3 4 20.

规则是:

  1. 您可以从第一行的任意位置开始

  2. 您可以一次移动一个方格,可以是直线向下、左下(对角线)或右下(对角线)。

  3. 输出必须是整数元组。

第一个元素是表示列与行的列表,第二个元素是总点数。例如。对于上面的电路板,最好的解决方案是从左上角 (5) 开始,然后沿对角线走剩余的步骤(直到 20 点正方形)。这将产生元组([1,2,3,4], 29)

记住,这一切都在 Haskell 中,所以这是一个函数范式递归问题。一开始我在想用贪心算法,也就是选择r1中的最大值,然后通过比较接下来的3种可能性进行递归;选择 3 个中的最高值。但是,缺点是贪心算法没有能力在下一行之前看到潜力。

我该怎么做呢?我不是在寻找代码本身,因为我喜欢自己解决问题。但是,非常感谢伪代码或一些算法指导!

【问题讨论】:

  • 这可以使用 Dijkstras 算法的改编来完成吗?数字是图的节点,每个节点的值将是指向它的每条边的权重,而不是与较短的路径进行比较,我们可以与较长的路径进行比较。
  • 显然直觉对于一般情况是错误的,因为最长路径问题在一般图中是 NP-hard(例如参见 stackoverflow.com/questions/10462736/…)。但是(!),您的问题相当于在有向无环(!)图中找到最长的路径,最长和最短路径都可以通过在节点的拓扑顺序中找到最长/最短路径来计算。

标签: algorithm haskell optimization recursion greedy


【解决方案1】:

我选择了不同的路径,没有双关语的意思。我列出了允许的索引组合并将板映射到它们。也许有人可以找到一种方法将其推广到任何规模的董事会。

import Data.List
import Data.Ord
import Data.Maybe

a = [[5,4,3,1],[10,2,1,0],[0,1,2,0],[2,3,4,20]]
r1 = a !! 0
r2 = a !! 1
r3 = a !! 2
r4 = a !! 3

i = [0,1,2,3]
index_combinations = [[a,b,c,d] | a <- i, b <- i, c <- i, d <- i, 
                      abs (b-a) < 2, abs (c-b) < 2, abs (d-c) < 2]

mapR xs = [r1 !! (xs !! 0), r2 !! (xs !! 1), 
           r3 !! (xs !! 2), r4 !! (xs !! 3)]

r_combinations = map mapR index_combinations
r_combinations_summed = zip r_combinations $ map (foldr (+) 0) r_combinations

result = maximumBy (comparing snd) r_combinations_summed
path = index_combinations !! fromJust (elemIndex result r_combinations_summed)

【讨论】:

  • 很好,但是index_combinations 效率很低。例如,当a = 0b = 2 时,它将尝试cd 的所有组合,即使ab 之间的约束失败。在每个级别自定义允许范围不是更好(虽然更详细)[[a,b,c,d] | a &lt;- [0..3], b &lt;- [max 0 (a-1)..min 3 (a+1)], c &lt;- [max 0 (b-1)..min 3 (b+1)], d &lt;- [max 0 (c-1)..min 3 (c+1)]]
  • 看起来这对于使用此模型进行更繁重的计算可能是一个有用的调整——感谢您的建议,我学到了一些新东西!看到算法的基准测试会很有趣。
【解决方案2】:

最好的解决方案不是自上而下的贪心算法,而是从最后一行开始向上工作的方法:

import Data.Function
import Data.List

-- All elements of Board are lists of equal lengths
-- valid b = 1 == length (group (map length b))
type Value = Int
type Board = [[Value]]
type Index = Int
type Result = ([Index], Value)

p :: Board
p = [[5,4,3,1],[10,2,1,0],[0,1,2,0],[2,3,4,20]] 

best_from :: Board -> Result
best_from [] = undefined
best_from xs | any null xs = undefined
best_from b = best_of . best_list $ b

best_list :: Board -> [Result]
best_list b = foldr1 layer (map label b)
  where label = zipWith (\index value -> ([index],value)) [1..]
        layer new rest =  zipWith (\(i1,v1) (i2,v2) -> (i1++i2, v1+v2)) new best
          where temp = head rest : map best_pair (zip rest (tail rest))
                best = map best_pair (zip temp (tail rest)) ++ [last temp]

best_pair :: (Result,Result) -> Result
best_pair (a@(_,a1), b@(_,b1)) | a1 >=b1 = a
                               | otherwise = b

best_of :: [Result] -> Result
best_of = maximumBy (compare `on` snd)

main = do
  print (best_from p)

如果有一行就很容易解决。因此,这会将每一行转换为具有简单 [#] 解决方案路径的 Result 列表。

鉴于new 行下方的puzzel 的rest 然后添加new 行是从rest 找到best 解决方案的问题(通过检查向下,左下,右下)和结合new 行。

这使得foldr 或这里的foldr1 成为自然结构。

【讨论】:

    【解决方案3】:

    保留到该单元格中得分最高的行中每一列的路径列表。

    您将从列表开始(在您的示例中)

    [([1],5), ([2],4), ([3],3), ([4],1)]
    

    然后,在检查下一行时,对于每一列,您选择前一行中可以到达该列的得分最高的路径,在这里,对于第二行,在第 1 列和第 2 列中,您将选择在上一行的第 1 列结束的路径,在第 3 列中,您将选择在上一行中以第 2 列结束的路径,在第 4 列中,在前一行中以第 3 列结束的路径,这样会给出你

    [([1,1],15), ([1,2],7), ([2,3],5), ([3,4],3)]
    

    对于第三行[0,1,2,0],您将再次为前两列选择以第 1 列结尾的路径,为第三列选择以第 2 列结尾的路径,为第四列选择以第 3 列结尾的路径,

    [([1,1,1],15), ([1,1,2],16), ([1,2,3],9), ([2,3,4],5)]
    

    对于第四行,[2,3,4,20],您将为前三列选择以第 2 列结尾的路径,为最后一列选择以第 3 列结尾的路径,

    [([1,1,2,1],18), ([1,1,2,2],19), ([1,1,2,3],20), ([1,2,3,4],29)]
    

    然后,当您到达最后一行时,选择总数最高的路径。

    为什么会这样:

    让得分最高的路径在c 列中结束。最后一列上方的部分必须是以倒数第二行的c-1, c, c+1 列之一结尾的得分最高的路径,因为最后一行的c 列只能从这些路径到达。

    【讨论】:

      【解决方案4】:

      我看到了您之前关于同一主题的问题,我开始着手解决。
      由于您不想要直接的解决方案,我可以为您提供我对您的问题的反思,我想它可以帮助您。

      一些基本属性:
      1.移动的次数总是等于列表的长度m = length A
      2.起点的个数等于链表头的长度n = length(head A)
      3.当前位置永远不会是负数,那么:
      - 如果当前位置等于 0,您可以向下或向右
      - 否则你可以向左、向下或向右

      这导致我们得到这个伪代码

      generate_path :: [[Int]] -> [[Int]]
      generate_path [] = [[]] 
      generate_path A =  ... -- You have to put something here
              where 
                    m = length A
                    n = length (head A)
      

      这东西应该看起来像这样

      move pos0 count0
          | count0 == 0 =   
              | pos0 == 0 = move (down count) ++ move (right count)  
              | otherwise = move (left count) ++ move (down count) ++ move (right count)  
                  where 
                      count = count0 - 1
                      down  = position0 
                      left  = position0 - 1
                      right = position0 + 1
      

      事实上,记住所有这些并添加 (!!) 运算符,我们不应该到目前为止解决方案。为了说服你玩 A + list comprehension + !!,作为

      [A !! x !! y | x <- [1..2], y <- [0..2]] -- I take random range 
      

      或者玩另一个版本:

      [[A !! x !! y | x <- [1..2]] | y <- [0..2]]] -- I take random range 
      

      实际上你有两个递归,主要的一个在参数 n = length (head A) 上工作,你在 (n-1) 处重复从 0 到 (n-1) 的相同动作,检索结果,这个递归嵌入另一个对 m 起作用的,从 0 到 (m-1) 重复相同的动作。

      希望对您有所帮助。 祝你好运。

      【讨论】:

        猜你喜欢
        • 2023-04-02
        • 2022-12-07
        • 2013-01-07
        • 2019-04-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多