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

Haskell中Fisher精确检验的高效实现方案求助

嘿,我懂你在Haskell里实现Fisher精确检验时想要更高效计算的需求——直接算大阶乘不仅慢,还容易碰到数值溢出的问题。结合你给出的公式,我来分享几个更优的实现思路,避开直接硬算阶乘的坑:

方案1:对数转换法(最快,适合浮点数结果)

直接计算阶乘的乘积很容易因为数值过大导致性能下降甚至溢出,我们可以用对数把乘法转换成加法,最后再通过指数运算还原结果。这种方法计算速度快,而且数值稳定性更好,适合不需要精确整数结果的场景:

import Data.List (foldl')
-- 可以使用number-theory库中的logFactorial,或者自己实现一个
import Math.NumberTheory.Logarithms (logFactorial)

fisherExactLog :: Integer -> Integer -> Integer -> Integer -> Double
fisherExactLog a b c d = exp $ 
  logFactorial (a+b) + logFactorial (a+c) + logFactorial (b+d) + logFactorial (c+d)
  - logFactorial a - logFactorial b - logFactorial c - logFactorial d
  - logFactorial (a+b+c+d)

如果不想依赖外部库,也可以自己实现对数阶乘的计算(用伽马函数或者递推求和),不过number-theory库的实现已经足够高效了。

方案2:质因数分解法(精确整数,支持极大数值)

如果需要精确的整数结果,而且输入的自然数可能很大,质因数分解是个好选择。我们先计算每个阶乘的质因数分解,然后把分子的质因数指数相加、分母的指数相加,再用分子的指数减去分母的指数,最后计算剩余质因数的乘积。这样完全避免了大整数的直接相乘,效率更高:

import Data.List (groupBy, sort)
import Data.Function (on)

-- 计算n!的质因数分解,返回[(质数, 指数)]
factorialPrimeFactors :: Integer -> [(Integer, Integer)]
factorialPrimeFactors n = concatMap countPrimeExponents $ primesUpTo n
  where
    countPrimeExponents p = 
      let total = sum [n `div` p^k | k <- [1..], p^k <= n]
      in (p, total)
    -- 埃氏筛法生成n以内的质数
    primesUpTo m = sieve [2..m]
    sieve [] = []
    sieve (x:xs) = x : sieve [y | y <- xs, y `mod` x /= 0]

-- 合并两个质因数分解列表,指数相加
mergeAdd :: [(Integer, Integer)] -> [(Integer, Integer)] -> [(Integer, Integer)]
mergeAdd a b = merge (sort a) (sort b)
  where
    merge [] ys = ys
    merge xs [] = xs
    merge ((p1,e1):xs) ((p2,e2):ys)
      | p1 == p2 = (p1, e1+e2) : merge xs ys
      | p1 < p2 = (p1,e1) : merge xs ((p2,e2):ys)
      | otherwise = (p2,e2) : merge ((p1,e1):xs) ys

-- 从第一个质因数分解中减去第二个的指数(只保留正指数)
mergeSubtract :: [(Integer, Integer)] -> [(Integer, Integer)] -> [(Integer, Integer)]
mergeSubtract a b = subtractFactors (sort a) (sort b)
  where
    subtractFactors [] _ = []
    subtractFactors xs [] = xs
    subtractFactors ((p1,e1):xs) ((p2,e2):ys)
      | p1 == p2 = if e1 > e2 then (p1, e1-e2) : subtractFactors xs ys else subtractFactors xs ys
      | p1 < p2 = (p1,e1) : subtractFactors xs ((p2,e2):ys)
      | otherwise = subtractFactors ((p1,e1):xs) ys

-- 计算质因数分解的乘积
computeProduct :: [(Integer, Integer)] -> Integer
computeProduct = foldl' (\acc (p,e) -> acc * p^e) 1

fisherExactPrimeFactor :: Integer -> Integer -> Integer -> Integer -> Integer
fisherExactPrimeFactor a b c d = computeProduct $ 
  numeratorFactors `mergeSubtract` denominatorFactors
  where
    numeratorFactors = foldl' mergeAdd [] 
      [factorialPrimeFactors (a+b), factorialPrimeFactors (a+c), 
       factorialPrimeFactors (b+d), factorialPrimeFactors (c+d)]
    denominatorFactors = foldl' mergeAdd []
      [factorialPrimeFactors a, factorialPrimeFactors b, factorialPrimeFactors c, 
       factorialPrimeFactors d, factorialPrimeFactors (a+b+c+d)]
方案3:组合数递推法(简洁高效,兼顾精确性)

观察你的公式可以发现,它可以转换成组合数的比值:

p = C(a+b, a) × C(c+d, c) / C(a+b+c+d, a+c)

其中C(n,k)是组合数“n选k”。组合数可以用递推乘法的方式高效计算,每一步都保持整数,而且避免了大阶乘的直接计算:

-- 高效计算组合数C(n,k),优先计算较小的k减少运算量
comb :: Integer -> Integer -> Integer
comb n k
  | k < 0 || k > n = 0
  | k == 0 || k == n = 1
  | k > n - k = comb n (n - k) -- 取较小的k,减少计算步骤
  | otherwise = foldl' (\acc i -> acc * (n - k + i) `div` i) 1 [1..k]

-- 返回Rational类型,保持精确的分数结果
fisherExactComb :: Integer -> Integer -> Integer -> Integer -> Rational
fisherExactComb a b c d = 
  let total = a + b + c + d
      c1 = comb (a+b) a
      c2 = comb (c+d) c
      c3 = comb total (a+c)
  in fromIntegral (c1 * c2) / fromIntegral c3

这个方案代码最简洁,而且计算效率很高,返回的Rational类型可以完全保留精确值,适合大多数常规场景。


内容的提问来源于stack exchange,提问作者Sebi Danila

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 08:53:13