Haskell中n皇后问题如何优化?附实现代码解析
基于位运算的Haskell N皇后问题优化代码解析
我来帮你拆解这段用位运算高效实现的Haskell N皇后问题代码——它专门用来计算14皇后的解数量,还内置了运行时间统计,性能拉满!
1. 语言扩展:BangPatterns
开头的{-# LANGUAGE BangPatterns #-}是关键的性能优化开关:
- Haskell默认是惰性求值,会把很多计算延迟到必要时才执行,容易产生大量未求值的"计算块"(thunk),拖慢递归性能。
- 这个扩展允许我们在变量前加
!,强制严格求值,让递归过程中每一步的状态都立即计算出来,避免内存堆积,对这种深度递归的算法至关重要。
2. 模块与依赖导入
module Main where import Data.Bits import Data.Word import Control.Monad import System.CPUTime import Data.List
module Main where:声明这是可执行程序的主模块,程序入口main就在这里。Data.Bits:提供所有位运算相关的函数(比如shiftL、.&.、complement),是整个算法的核心依赖。Data.Word:引入Word64类型——这是一个无符号64位整数,足够处理最多64皇后的问题(每一位代表一列的状态)。System.CPUTime:用来获取CPU的精确时间戳,实现运行耗时统计。Data.List:用到foldl'(严格折叠函数)和map,前者避免惰性折叠的性能损耗。
3. 主函数:入口与耗时统计
main :: IO () main = do start <- getCPUTime print $ dame 14 end <- getCPUTime print $ "Needed " ++ (show ((fromIntegral (end - start)) / (10^12))) ++ " Seconds"
这段逻辑很清晰:
- 调用
getCPUTime记录开始时间(返回值是纳秒级的CPU时间)。 - 调用
dame 14计算14皇后的解数量,并打印结果。 - 再次调用
getCPUTime记录结束时间。 - 计算耗时:把结束时间减开始时间的差值(纳秒)除以
10^12,转换成秒后格式化输出。
4. 状态类型定义:BitState
type BitState = (Word64, Word64, Word64)
这个三元组是递归过程中传递的核心状态,三个Word64分别代表:
- 第一个值:已占用列的掩码——某一位为1,表示对应列已经放了皇后,不能再用。
- 第二个值:左下方对角线的掩码——左对角线的占位可以通过行-列的偏移计算,位运算里左移一位就对应下一行的左对角线占位。
- 第三个值:右下方对角线的掩码——右对角线的占位是行+列的偏移,位运算里右移一位对应下一行的右对角线占位。
用这个三元组打包状态,让递归函数的参数更简洁。
5. 核心算法:dame函数与递归逻辑
结合常见的位运算N皇后实现,补全并解析你的代码逻辑:
dame :: Int -> Int dame max = let fullMask = (1 `shiftL` max) - 1 -- 生成max位全1的掩码,代表所有列 go 0 _ _ _ = 1 -- 所有行都放完皇后,找到1个有效解 go !rows !cols !left !right = -- 计算当前行可用的列:列、左对角线、右对角线都未被占用的位置 let available = fullMask .&. complement (cols .|. left .|. right) -- 遍历所有可用列,累加解数 loop 0 = 0 loop !avail = -- 取最低位的1,代表当前选中的列位置 let pos = avail .&. (-avail) -- 更新状态:标记当前列为已占用 newCols = cols .|. pos -- 更新左对角线掩码:左移一位,对应下一行的左对角线占位 newLeft = (left .|. pos) `shiftL` 1 -- 更新右对角线掩码:右移一位,对应下一行的右对角线占位 newRight = (right .|. pos) `shiftR` 1 in -- 递归计算下一行的解数,加上当前列的其他可能解 go (rows - 1) newCols newLeft newRight + loop (avail .&. complement pos) in loop available in go max 0 0 0
这里的核心优化点全在位运算上:
fullMask = (1shiftLmax) - 1:快速生成max位的全1掩码,比如14皇后就是14位全1,代表所有列都可用。available = fullMask .&. complement (cols .|. left .|. right):一次位运算就算出当前行能放皇后的列——把已占用的列、左对角线、右对角线的掩码合并,取反后和全1掩码做与运算,剩下的1就是可用列。pos = avail .&. (-avail):位运算技巧,快速提取最低位的1,避免遍历所有位,大幅提升效率。- 递归过程中用
!标记参数(!rows !cols),强制严格求值,避免惰性求值的性能损耗。 - 用
foldl'(对应你代码里的实现)而不是foldl,严格折叠累加所有解数,避免内存泄漏。
这种位运算实现比传统的列表遍历快得多,因为位运算都是CPU级别的操作,效率极高,能轻松处理14甚至更大的N值。
内容的提问来源于stack exchange,提问作者steffmaster
相关产品推荐
相关产品推荐

