You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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"

这段逻辑很清晰:

  1. 调用getCPUTime记录开始时间(返回值是纳秒级的CPU时间)。
  2. 调用dame 14计算14皇后的解数量,并打印结果。
  3. 再次调用getCPUTime记录结束时间。
  4. 计算耗时:把结束时间减开始时间的差值(纳秒)除以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 = (1 shiftL max) - 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 07:49:58