如何在Haskell中实现带条件中断的循环

klr1opcd  于 2022-11-24  发布在  其他
关注(0)|答案(4)|浏览(174)

我想找出满足

的第一个n,如果我用另一种语言,比如c/c ++,这是一件简单而容易的事情,但是我不知道如何在Haskell中实现它。

#include <iostream>
long double term(int k) { return 1.0/(k*k+2.0*k); }
int main() {
    long double total = 0.0;
    for (int k=1;;k++) {
        total += term(k);
        if (total>=2.99/4.0) {
            std::cout << k << std::endl;
            break;
        }
    }
    return 0;
}

我使用dropWhile处理一个有序列表,并取1来获取第一个列表。

term k = 1.0/(k*k+2.0*k)
termSum n = sum $ take n $ map term [1..]
main = do
  let [(n,val)] = take 1 $ dropWhile (\(a,b)->b <= 2.99/4.0) $ map (\n->(n,termSum n)) [1..]
  print n

我知道这很可怕。什么是最好的和直观的方式来写这个?
回复:谢谢你的伟大的答案!一个使用修复功能似乎是最快的在我的机器(Redhat 6. 4 64bit/80GB内存)
method#0取1并dropWhile(我初始实现)

threshold=0.74999         n=99999     time=52.167 sec

方法#1使用修复函数

threshold=0.74999         n=99999     time=0.005 sec
threshold=0.74999999      n=101554197 time=1.077 sec
threshold=0.7499999936263 n=134217004 time=1.407 sec

方法#2反向工作

threshold=0.74999         n=99999     time=0.026 sec
threshold=0.74999999      n=101554197 time=21.523 sec
threshold=0.7499999936263 n=134217004 time=25.247 sec

方法#3命令式方法

threshold=0.74999         n=99999     time=0.008 sec
threshold=0.74999999      n=101554197 time=2.460 sec
threshold=0.7499999936263 n=134217004 time=3.254 sec

回复回复:我注意到无论我用什么实现(修复、命令式或递归方式),如果阈值大于0.7499999936264 ...则永远不会结束..为了使f(n)大于0.7499999936264,我以为我们只需要计算到150,000的项,000以来![f(n)=\frac_{3n^2 + 5n} ^{4n^2 + 12n +8}]。我使用Integer代替Int,但它也没有帮助。如果我将阈值设置为大于0.7499999936264 ...,是否有任何原因导致它无法完成?

ldxq2e6h

ldxq2e6h1#

在这种情况下,我喜欢倒推:

main = print k where
  k = 1 + length (takeWhile (< (2.99/4)) partialSums)
  partialSums = scanl1 (+) terms
  terms = [ 1.0/(k*k+2.0*k) | k <- [1..] ]

工作原理:
terms是一个无限列表,但由于Haskell很懒,我们只计算我们需要的每一项:

λ terms = [ 1.0/(k*k+2.0*k) | k <- [1..] ] :: [Double]
λ take 5 terms
[0.3333333333333333,0.125,6.666666666666667e-2,4.1666666666666664e-2,2.857142857142857e-2]
λ :p terms
terms = 0.3333333333333333 : 0.125 : 6.666666666666667e-2 :
        4.1666666666666664e-2 : 2.857142857142857e-2 : (_t5::[Double])

partialSums是另一个无限列表,它基于terms的内容(使用scanl1),它允许我们分摊计算termSum所做的工作:

λ partialSums = scanl1 (+) terms
λ take 5 partialSums 
[0.3333333333333333,0.4583333333333333,0.525,0.5666666666666667,0.5952380952380952]

然后takeWhile (< (2.99/4))确定我们需要生成partialSums的多少项,从而确定我们需要生成terms的多少项:

λ length (takeWhile (< (2.99/4)) partialSums)
398

如果我们检查一下,我们可以看到,第398个terms之和都小于2.99 / 4,但第399个却把它撞过来了:

λ sum (take 398 terms) < 2.99/4
True
λ sum (take 399 terms) < 2.99/4
False

或者,第397个部分和(从0开始的索引)小于目标,而第398个部分和不是:

λ partialSums !! 397 < 2.99/4
True
λ partialSums !! 398 < 2.99/4
False
q3aa0525

q3aa05252#

本质上是显式递归,但我喜欢fix for循环,如下所示:

import Data.Function (fix)

term k = 1.0 / (k*k+2.0*k)

main = print $ fix (\f total k ->
                      let new = total + term k
                      in if new >= 2.99/4.0 then k else f new (k+1)
                   ) 0 1
mm9b1k5b

mm9b1k5b3#

如果我使用另一种语言,比如c/c++,这是一件简单而容易的事情
好吧,那我们就用同样的方法。

import Prelude hiding (break)
import Data.IORef
import Control.Monad.Cont
import Data.Foldable
import Control.Monad (when)

-- long double term(int k) { return 1.0/(k*k+2.0*k); }
term :: Int -> Double
term k = 1.0/(k'*k'+2.0*k')
   where k' = fromIntegral k

-- int main() {
main :: IO ()
main = flip runContT return $ do
   -- long double total = 0.0;
   total <- lift $ newIORef (0.0 :: Double)
   -- for (int k=1;;k++) {
   callCC $ \break ->
      for_ [1..] $ \k -> do
         -- total += term(k);
         lift $ modifyIORef total (+ term k)
         -- if (total>=2.99/4.0) {
         totalV <- lift $ readIORef total
         when (totalV >= 2.99/4.0) $ do
            -- std::cout << k << std::endl;
            lift $ print k
            -- break;
            break ()

是的,上面的答案与其说是严肃的答案,不如说是一个玩笑。不过,很高兴看到,至少在理论上,用Haskell编写命令式代码是可能的。
这只会导致非惯用的Haskell,这并不比原始代码难读或难写多少。毕竟,朋友之间的一两次callCC又有什么关系呢?:-P

enyaitl3

enyaitl34#

我同意显式递归是最简单的编写方法,但是使用fix会让所有程序员感到困惑,除非是非常老练的程序员。

f :: Int       
f =
  f' 1 0
  where
    f' k tot
      | tot >= (2.99 / 4.0) = k - 1
      | otherwise           = f' (k + 1) (tot + term)
      where
        term = 1.0 / fromIntegral (k * k + 2 * k)

使用更高阶的函数直到可能被看作更接近C++代码。因此

fh :: Int       
fh =
  fk - 1
  where
    (fk, ft)     =  until ((>= (2.99 / 4.0)) . snd) fh' (1, 0)
    fh' (k, tot) = (k + 1, tot + term)
      where
        term = 1.0 / fromIntegral (k * k + 2 * k)

此外,C++代码中的中断也是多余的,因为该代码可以编写为

long double term(int k) { return 1.0/(k*k+2.0*k); }
int main() {
    long double total = 0.0;
    int k;
    for (k=1;total<2.99/4.0;k++) {
        total += term(k);
    }
    std::cout << k - 1 << std::endl;
    return 0;
}

最后,关于为什么计算150,000,000以下的项不会得到大于0.7499999936264的结果,这是浮点运算的固有特性。将一个非常小的项添加到一个相对较大的总数中不会改变总数:

λ> term k = 1.0 / fromIntegral (k * k + 2 * k)
λ> term 150000000
4.444444385185186e-17
λ> (2.99 / 4.0) + it == (2.99 / 4.0)
True

为了避免这种情况,您必须以相反的顺序添加项,从最小到最大。

相关问题