Haskell 中的递归和回溯

Recursion and Backtracking in Haskell

我决定自学 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

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

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])

第三,使用模式比调用函数更直观;最重要的是,从列表的开头访问第 i 个元素,以增加 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 来处理失败的特殊情况。因此,它相当于有一个解决方案,或者 none.

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

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

事实上,只生成第一个解决方案与从 列表 所有 个解决方案中获取第一个解决方案相同:

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,而不是盲目地做出选择并在最后才检验其有效性。