P93 - 算术谜题

2026-09-07 00:00    #Haskell   #99题   #搜索  

P93 - 算术谜题

An arithmetic puzzle

官方模块:Problems.P93 核心函数:arithmeticPuzzle


← P92 优雅树标号 | P94 正则图 →


题目描述

保持数字顺序,在相邻数字之间插入 + - * / = 和必要括号,使等号两侧按有理数运算后相等。等号恰好出现一次,不能除以零。

表达式生成

方法一:递归枚举区间切分

 1import Data.List (nub)
 2import Data.Ratio ((%))
 3
 4data Expr = Expr Rational String
 5
 6expressions :: [Integer] -> [Expr]
 7expressions [] = []
 8expressions [n] = [Expr (n % 1) (show n)]
 9expressions numbers = concat
10  [combine left right
11  | split <- [1 .. length numbers - 1]
12  , left  <- expressions (take split numbers)
13  , right <- expressions (drop split numbers)]
14
15combine (Expr a sa) (Expr b sb) =
16  [Expr (a+b) (paren sa "+" sb),
17   Expr (a-b) (paren sa "-" sb),
18   Expr (a*b) (paren sa "*" sb)]
19  ++ [Expr (a/b) (paren sa "/" sb) | b /= 0]
20  where paren l op r = "(" ++ l ++ op ++ r ++ ")"

构造等式

 1arithmeticPuzzle :: [Integer] -> [String]
 2arithmeticPuzzle numbers = nub
 3  [strip l ++ " = " ++ strip r
 4  | split <- [1 .. length numbers - 1]
 5  , Expr lv l <- expressions (take split numbers)
 6  , Expr rv r <- expressions (drop split numbers)
 7  , lv == rv]
 8  where
 9    strip text
10      | length text >= 2 && head text == '(' && last text == ')' = init (tail text)
11      | otherwise = text

为了让求值语义完全明确,这个实现保留了一些非必要括号;可在展示层根据运算符优先级进一步化简,不影响求解核心。

方法二:区间动态规划

 1import Data.Array (Array, (!), array)
 2
 3arithmeticPuzzleDP :: [Integer] -> [String]
 4arithmeticPuzzleDP numbers = nub
 5  [strip leftText ++ " = " ++ strip rightText
 6  | split <- [1 .. length numbers - 1]
 7  , Expr leftValue leftText <- table ! (0, split)
 8  , Expr rightValue rightText <- table ! (split, length numbers)
 9  , leftValue == rightValue]
10  where
11    table :: Array (Int, Int) [Expr]
12    table = array ((0, 1), (length numbers - 1, length numbers))
13      [((start, end), build start end)
14      | width <- [1..length numbers]
15      , start <- [0..length numbers - width]
16      , let end = start + width]
17
18    build start end
19      | end == start + 1 =
20          [Expr (numbers !! start % 1) (show (numbers !! start))]
21      | otherwise = concat
22          [combine left right
23          | split <- [start + 1 .. end - 1]
24          , left <- table ! (start, split)
25          , right <- table ! (split, end)]
26
27    strip text
28      | length text >= 2 && head text == '(' && last text == ')' =
29          init (tail text)
30      | otherwise = text

递归版本会在不同等号位置和括号结构中重复计算相同数字区间。动态规划表以半开区间 (start,end) 为键,每个区间的所有表达式只生成一次,再被更大区间复用。

方法对比

方法子区间结果
递归枚举每次遇到都重新生成
区间 DP缓存在数组中复用

测试

1>>> not (null (arithmeticPuzzle [2,3,5,7,11]))
2True
3>>> arithmeticPuzzle [1]
4[]
5>>> not (null (arithmeticPuzzleDP [2,3,5,7,11]))
6True

所有二叉括号结构与四种运算组合会指数增长,使用 Rational 则保证等值比较没有浮点误差。

参考


← P92 优雅树标号 | P94 正则图 →