结合运算的所有括号化方案枚举及Haskell实现思路问询
结合运算的括号化方案:Catalan数关联与Haskell实现
Catalan数、括号化与Dyck word的关联
n个变量的结合运算括号化方案数是第n-1个Catalan数,这和Dyck word的数量一致,二者的对应关系可通过二叉树建立:
- 每个n个变量的括号化方案对应一棵含n个叶子节点的二叉树:叶子节点是变量,内部节点代表运算操作。比如
a + (b + c)对应根节点连接左叶子a和右子树(根节点连接b和c);(a + b) + c对应根节点连接左子树(a和b)和右叶子c。 - n-1对括号的Dyck word(合法括号序列)也和这类二叉树一一对应:将Dyck word的每个
(视为创建内部节点,)视为回溯到父节点,叶子节点对应变量。比如n=3时,Dyck word(())对应a + (b + c),()()对应(a + b) + c——括号序列的嵌套结构直接匹配运算的括号化逻辑。
Haskell实现思路与代码
核心思路:递归拆分与组合
对于元素列表,递归将其拆分为非空左右两部分,分别生成两部分的所有括号化方案,再用运算符组合左右结果。遍历所有可能的拆分点(从第1个元素后到第n-1个元素后),最终得到当前列表的全部括号化方案。
结构化实现(用Expr类型)
先定义表达式数据类型,结构化表示括号化结果(比字符串拼接更严谨):
data Expr = Var String | Add Expr Expr deriving (Show, Eq) -- 将Expr转为可读性字符串 exprToString :: Expr -> String exprToString (Var s) = s exprToString (Add e1 e2) = "(" ++ exprToString e1 ++ " + " ++ exprToString e2 ++ ")"
实现递归生成函数:
-- 生成n个变量的所有括号化方案(变量命名为a1,a2...an) parens :: Int -> [Expr] parens 1 = [Var "a1"] parens n = concat [ combine k | k <- [1..n-1] ] where combine k = [ Add l r | l <- parens k, r <- renameExprs k (parens (n - k)) ] -- 给右侧表达式的变量重命名(避免与左侧重复) renameExprs :: Int -> [Expr] -> [Expr] renameExprs offset (Var s : es) = Var ("a" ++ show (read (drop 1 s) + offset)) : renameExprs offset es renameExprs offset (Add e1 e2 : es) = Add (renameExprs offset e1) (renameExprs offset e2) : renameExprs offset es renameExprs _ [] = []
测试示例:
-- n=2的结果 map exprToString (parens 2) -- 输出 ["(a1 + a2)"] -- n=3的结果 map exprToString (parens 3) -- 输出 ["((a1 + a2) + a3)", "(a1 + (a2 + a3))"] -- n=4的结果(共5种,符合Catalan数C3=5) map exprToString (parens 4)
简化版(直接生成字符串)
若不需要结构化类型,可直接生成字符串形式的方案:
parensStr :: Int -> [String] parensStr 1 = ["a1"] parensStr n = concat [ [ "(" ++ l ++ " + " ++ r ++ ")" | l <- parensStr k, r <- renameStrs k (parensStr (n - k)) ] | k <- [1..n-1] ] where renameStrs offset = map (\s -> let num = read (drop 1 s) in "a" ++ show (num + offset))
关键说明
- 递归终止条件:单个变量的括号化方案就是变量本身,无需加括号。
- 变量重命名:拆分左右部分后,右侧变量需重新命名,避免重复使用a1、a2等标识。
- 拆分完整性:遍历所有可能的拆分点,确保不遗漏任何括号化组合。
内容的提问来源于stack exchange,提问作者J...S
相关产品推荐
相关产品推荐

