【问题标题】:Recursion and Backtracking in HaskellHaskell 中的递归和回溯
【发布时间】:2021-04-03 16:31:36
【问题描述】:

我决定自学 Haskell,并尝试将一些代码从 Java 翻译成 Haskell,这样我就可以更熟悉递归、回溯和搜索树修剪。

Java 代码:

private static boolean isListOkay(ArrayList<Integer> numbers) {
    return listSum(numbers) == 8 && numbers.size() == 3;
}

private static int listSum(ArrayList<Integer> numbers) {

    int sum = 0;

    for (Integer number : numbers)
        sum += number;

    return sum;
}

public static ArrayList<Integer> sumTo8(ArrayList<Integer> numbers) {
    return sumTo8(numbers, 0, new ArrayList<Integer>());
}

private static ArrayList<Integer> sumTo8(ArrayList<Integer> numbers, int i, ArrayList<Integer> list) {

    if (isListOkay(list))
        return list;

    else if (i == numbers.size() && !isListOkay(list))
        return null;

    else if (listSum(list) > 8 || listSum(list) == 8 && list.size() != 3)
        return null;

    else {

        int currentNumber = numbers.get(i);

        ArrayList<Integer> pickIt = new ArrayList<>(list);
        pickIt.add(currentNumber);

        ArrayList<Integer> leaveIt = new ArrayList<>(list);

        ArrayList<Integer> pickItResult = sumTo8(numbers, i + 1, pickIt);

        if (pickItResult == null)
            return sumTo8(numbers, i + 1, leaveIt);

        return pickItResult;
    }


}

Haskell 代码:

listSumUtil :: [Int] -> Int -> Int
listSumUtil [] sum = sum
listSumUtil (x:xs) sum = x + y
  where y = listSumUtil xs sum

listSum :: [Int] -> Int
listSum list = listSumUtil list 0

sumTo8Util :: [Int] -> [Int] -> [Int]

sumTo8Util [] list
  | sum == 8 && listLength == 3 = list
  | otherwise = []
    where sum = listSum list
          listLength = length list

sumTo8Util (x:xs) l2 =
  if sum > 8 && listLength > 3 then []
  else if sum == 8 && listLength == 3 then l2
  else (if l3 == [] then l4 else l3)
    where sum = listSum l2
          listLength = length l2
          l3 = sumTo8Util xs pickIt
          pickIt = l2 ++ [x]
          l4 = sumTo8Util (x:xs) l2

sumTo8 :: [Int] -> [Int]
sumTo8 list = sumTo8Util list []

Java 代码正在运行,我能够编译 Haskell 代码。当我执行 main 时,虽然没有输出并且它一直在运行,所以某处必须有一个无限循环,这就是我需要你帮助的地方。如何在 Haskell 中实现准确的 Java 代码?我在实施中遗漏了什么吗?如您所见,我在 Haskell 代码中避免了语法糖,因为我刚刚开始并且还不能理解它。

更新 1:

在 Haskell 中添加 else if sum == 8 && listLength == 3 then l2 条件 代码,但仍然不起作用。

更新 2:

找到了一种方法。

工作代码:

listSum :: [Int] -> Int
listSum list =  foldl (+) 0 list
  
insertAtEnd :: [Int] -> Int -> [Int]
insertAtEnd [] c = [c]
insertAtEnd (h:t) c = h : insertAtEnd t c  
  
sumTo8Util :: [Int] -> Int -> [Int] -> [Int]
sumTo8Util lst i rlst 
  | (length rlst == 3) && (listSum rlst == 8) = rlst
  | (i == length lst) && ((listSum rlst /= 8) || (length rlst /= 3)) = []
  | otherwise = if (length pickIt == 0) then (sumTo8Util lst (i+1) rlst) else pickIt
    where number = lst !! i
          nrlst =  insertAtEnd rlst number
          pickIt = sumTo8Util lst (i+1) nrlst 
  
sumTo8 :: [Int] -> [Int]
sumTo8 list = sumTo8Util list 0 []   

基本上我尝试通过返回空列表来触发回溯。

如果有替代方案可以使用回溯并且比我的代码更有效*,请随时提出建议。

*肯定会像我这几天自学 Haskell 一样

【问题讨论】:

  • "如何在 Haskell 中实现准确的 Java 代码?"一个不会。它们是完全不同的语言,需要完全不同的心态。将代码“翻译”成另一种语言通常会在目标语言中产生错误/非惯用代码。从 OOP 转换为函数式编程会给你各种糟糕的代码。不要这样做。从教程开始学习 Haskell。
  • 作为一个例子来说明你的代码有多不习惯:你的ListSum 已经被提供为Prelude.sum = foldr (+) 0——这是一个非正式的定义,但优化/通用化的定义仍然是一行代码。该定义不需要模仿程序语言对累加器的破坏性分配。如果你研究一下 Haskell 的工作原理,你会更有效地学习 Haskell。

标签: haskell recursion functional-programming backtracking code-translation


【解决方案1】:

首先,无需重新发明最简单的轮子:

listSum :: [Int] -> Int
listSum = sum
  
insertAtEnd :: [Int] -> Int -> [Int]
insertAtEnd xs c = xs ++ [c]
  
sumTo8 :: [Int] -> [Int]
sumTo8 list  =  helper list 0 [] 
  where
  helper :: [Int] -> Int -> [Int] -> [Int]
  helper lst i rlst 
   | (length rlst == 3) && (sum rlst == 8) = rlst
   | (i == length lst) && ((sum rlst /= 8) || (length rlst /= 3)) = []
   | otherwise = if (length pickIt == 0) 
                 then (helper lst (i+1) rlst) 
                 else pickIt
     where number = lst !! i
           pickIt = helper lst (i+1) (rlst ++ [number])

其次,当我们已经确定test 为假时,无需确保not test 为真。并且计算整个列表的length 只是为了检查它是否为0 更好地测试列表是否为null

sumTo8 :: [Int] -> [Int]
sumTo8 list  =  g list 0 [] 
  where
  g lst i rlst 
   | (length rlst == 3) && (sum rlst == 8)  =  rlst
   | (i == length lst)   =  []
   | null pickIt         =  g lst (i+1) rlst
   | otherwise           =  pickIt
     where 
          pickIt = g lst (i+1) (rlst ++ [lst !! i])

第三,使用模式比调用函数更直观;最重要的是,从列表的开头访问ith 元素,以增加i,与沿列表前进并访问其head 相同——但后者更多高效(线性过程而不是二次过程):

sumTo8 :: [Int] -> [Int]
sumTo8 list  =  g list [] 
  where
  g _  rlst@[_,_,_] | sum rlst == 8  =  rlst
  g []     _             =  []
  g (n:ns) rlst 
   | null pickIt         =  g ns rlst
   | otherwise           =  pickIt
     where 
          pickIt = g ns (rlst ++ [n])

第四,对信号失败产生一个无效的答案是非常非Haskellish。

在 Haskell 中,让类型反映数据的真实性质是惯用的。

而不是从一个收缩为只产生正整数的函数返回-1;或者从一个应该生成三元素列表作为结果的函数返回一个空列表,在 Haskell 中,我们将该结果放入某种容器数据类型中,该数据类型以某种特定方式向我们表明其结果的性质。

例如,这种Maybe 类型要么将结果包装在其Just 数据构造函数(“标签”)下,要么使用Nothing 处理失败的特殊情况。因此,它相当于有一个解决方案,或者没有。

我们可以重组代码来做到这一点,但是,让我们注意到有一个或没有元素是有几个或没有元素的特殊情况;它由 list 数据类型捕获,因此[a] 可以表示有一个解决方案a[a,b] -- 两个解决方案ab[] 可以表示无解。

使用++ 将两个列表附加在一起,它只是将第二个的元素放在第一个的所有元素之后,在新的结果列表中。

确实,仅生成第一个解决方案与从 所有 个解决方案的 list 中获取第一个解决方案相同:

sumTo8 :: [Int] -> [[Int]]
sumTo8 list  =  take 1 (g list [])
  where
  g _  rlst@[_,_,_] | sum rlst == 8  =  [rlst]
  g []     _     =  []
  g (n:ns) rlst  =  g ns (rlst ++ [n])
                    ++
                    g ns rlst

第五,为什么要将自己限制在第一个,使用具有 惰性求值的语言

sumTo8 :: [Int] -> [[Int]]
sumTo8 list  =  g list []
  where
  g _  [a,b,c] | a+b+c == 8  =  [ [c,b,a] ]
  g []     _     =  []
  g (n:ns) rlst  =  g ns (n : rlst)   -- we either pick
                    ++                -- the current element
                    g ns rlst         -- or don't

那么当这个列表被懒惰地探索时,相当于深度优先搜索带回溯。

现在代码更加清晰,所以我们终于可以开始看到搜索修剪的新机会,例如保持当前选取元素的总和,在运行总数已经超过目标时提前中止无效分支;或者当我们已经拥有三个元素时停止选择新元素;等等

共同的主题是,逐步推进我们的知识,使我们在每个点上都有一些部分知识on our journey,而不是盲目地做出选择并仅在最后测试其有效性。

【讨论】:

  • 这些方法是否更有效?如果是,为什么?
  • 我希望您能完成每个转换,看看发生了什么。首先我们只是简化代码,删除多余的检查,用等效的 - 但在某种意义上更好 - 模式匹配替换一些测试,比如,length xs == 0 --> null xs --> 匹配[]。 (第一步很重要,第二步只是惯用的,让我们更清楚地看到代码)。然后,从 -&gt; [Int](产生 one 解决方案)更改为 -&gt; [[Int]](解决方案列表)。稍后我可能会添加一些措辞,请尝试查看每个过渡步骤。 :)
  • 为了效率,在列表末尾重复追加是二次的,用 cons 反向构建列表,最后只反转一次是线性的。但这是一个微优化,仍然有很多修剪的机会,比如在我们选择元素时计算运行总和(如果运行总和已经超过目标,我们可以中止这个分支)。但最重要的是,我们构建了 all 解决方案的列表;那么当这个列表被lazily探索时,它相当于带有回溯的深度优先搜索。这已经是的答案了。
猜你喜欢
  • 1970-01-01
  • 2021-12-28
  • 1970-01-01
  • 1970-01-01
  • 2014-12-27
  • 1970-01-01
  • 2018-06-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多